Привет, Хабр! В этой статье я пошагово рассмотрю создание простого веб-приложения — сокращателя ссылок на node.js, используя mysql-libmysqlclient, MooTools на сервере и jQuery на клиенте. Статья предполагает, что читатель уже прошёл упражнение «Hello world» и разобрался в самых основах node.js.
Сразу предупрежу — я не считаю себя экспертом в node.js. Предлагаю обсудить код и вместе приблизиться к идеалу. Возможно, мы вместе сделаем отличный пример для новичков. Потому — конструктивная критика привествуется.
Серверный MooTools
Я не хочу холиварить на тему нужно-ненужно, я предпочёл использовать, вы можете решить иначе.
Сходу подключить MooTools не удалось — нельзя просто взять, скачать серверный файл и сделать require('./MooTools').
Мне помог твит MooTools от 12 июля и ссылка davidwalsh.name/mootools-nodejs. В итоге, библиотека подключилась так:
require('./Lib/MooTools').apply(GLOBAL);
MySQL
Для данного приложения больше подходит какая-нибудь NoSQL база данных, но моей целью было именно научится работать с MySQL в node.js.
Я достал из закладок топик про Node-mysql-libmysqlclient, взял всю необходимую инфу и по мануалам без проблем собрал клиент.
Сам скрипт я закинул в папку
Lib
в проекте, потому он легко инициируется так:var conn = require('./Lib/mysql/mysql-libmysqlclient').createConnectionSync();
conn.connectSync('localhost', 'NAME', 'PASS', 'nodejs');
Расширяем ServerResponse
Для того, чтобы было удобно задавать хедеры в приложении и редиректить я решил расширить прототип ServerResponse(считаю, что такой подход соответствует идеологии JavaScript) с помощью метода MooTools implements:
var http = require('http');
http.ServerResponse.implement({
// code - какой код вернула страница. Это может быть, например 200 (Ok) или 404 (Not Found)
// plain - отдаем просто текст если true, иначе - html
header : function (code, plain) {
this.writeHead(code, {
'Content-Type': plain ? 'text/plain' : 'text/html'
});
},
// response.redirect('http://example.com') перенесет нас на соответствующий адрес
redirect : function (url, status) {
this.writeHead(status || 302, {
'Content-Type' : 'text/plain',
'Location' : url
});
this.write('Redirecting to ' + url);
this.end();
}
});
Каркас приложения
http.createServer(function (req, res) {
// code will be here
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');
Ссылки, файл Link.js
Итак, ссылка у нас имеет три свойства — id в базе данных, url, на который она ведет и code, который показывается в нашем сокращателе.
Мы не будем писать отдельно code в базу, а будем получать его на базе id, переводя его в 36-ритчную систему счисления ([0-9a-z]).
Link.getCode = function (id) {
return id ? id.toString(36) : null;
};
Link.fromCode = function (code) {
return code ? parseInt(code, 36) : null;
};
На базе этого построим сам класс-ссылки, который разместим перед статическими методами:
Link = new Class({
initialize : function (obj) {
// если не получилось установить айди - пытаемся получить его из кода
this.setId(obj.id).id || this.setCode(obj.code);
this.setUrl(obj.url);
},
setId : function (id) {
// id должен быть исключительно int
this.id = (isNaN(id) || id <= 0) ? null : parseInt(id);
return this;
},
getId : function () {
return this.id;
},
setUrl : function (url) {
this.url = url || null;
return this;
},
getUrl : function () {
return this.url;
},
setCode : function (code) {
this.id = Link.fromCode(code);
return this;
},
getCode : function () {
return Link.getCode(this.id);
}
});
В базе создадим простую таблицу и напишем модель для получения и вставки ссылки:
CREATE TABLE `shortLinks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` varchar(512) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Model = new Class({
initialize : function (conn) {
// ссылка на соединение с базой данных
this.conn = conn;
},
create : function (args) {
// так будет легче создавать ссылки, в следующем файле увидим
return new Link(args);
},
get : function (link, fn) {
// нам не нужны неверные
if (!link.getId()) throw 'EmptyId';
// простой запрос. мы знаем, что link.id может быть только int
var q = 'SELECT * FROM `shortLinks` WHERE `id` = ' + link.getId();
// запросы необходимо делать асинхронно. такая идеология языка.
this.conn.query(q, function (err, res) {
if (err) throw err;
res.fetchAll(function (err, rows) {
if (err) throw err;
// по завершению мы вызовем ф-цию, переданную в метод
fn(rows.length ? link.setUrl(rows[0].url) : null);
res.freeSync();
});
});
},
put : function (link, fn) {
// url - небезопасен, потому !обязательно! необходимо его экранировать
var q = 'INSERT INTO `shortLinks` (`id`, `url`) ' +
'VALUES (NULL , "' + this.conn.escapeSync(link.getUrl()) + '");'
this.conn.query(q, function (err, res) {
if (err) throw err;
// указываем новый айди и передаем ссылку
fn(link.setId(
// таким способом можно получить айди вставки
this.conn.lastInsertIdSync()
));
}.bind(this));
}
});
// Эти классы будут экспортироваться
exports.Link = Link;
exports.Model = Model;
В инициализацию можно добавить следующую строку:
var linkModel = new (require('./Link').Model)(conn);
Обратите внимание, скобки вокруг
require('./Link').Model
— обязательны, иначе оно постарается создать объект require
.Рендерер, файл Renderer.js
// не забываем во всех файлах подключать Мутулз
require('./Lib/MooTools').apply(GLOBAL);
// нам понадобится обработка ссылок
var url = require('url');
// и работа с файловой системой
var fs = require('fs');
exports.Renderer = new Class({
initialize : function (linkModel) {
this.link = linkModel;
}
});
Renderer, как и Link.Model будет создаваться только один раз при инициализации приложения и каждый запрос использоваться один и тот же, за счет чего мы не теряем драгоценные миллисекунды.
Основой будет метод run:
run : function (req, res) {
var path = url.parse(req.url, true);
if (path.query && 'add' in path.query) {
var addUrl = path.query.add;
// пользователь может передать ссылку без протокола, например, "example.com"
// Чтобы она не вела нас на внутреннюю страницу, необходимо добавить default-протокол
if (!url.parse(addUrl).protocol) {
addUrl = 'http://' + addUrl;
}
// добавляем новую ссылку
this.add(res, addUrl);
// если это сокращенная ссылка (формат url/!abc12 )
} else if (path.pathname.test(/^\/![0-9a-z]+$/)) {
// отрезаем первых два служебных символа (/!) и ссылаем пользователя
this.send(res, path.pathname.substr(2));
} else {
// во всех остальных случаях рендерим главную страницу
this.index(res);
}
},
Пока не забыли — немного отредактируем
./init.js
var linkModel = new (require('./Link').Model)(conn);
var renderer = new (require('./Renderer').Renderer)(linkModel);
http.createServer(function (req, res) {
renderer.run(req, res);
}).listen(8124, "127.0.0.1");
Т.к. ссылки у нас будут добавлятся аджаксом, все, что нам надо — это записать ссылку в базу и вернуть её код:
add : function (res, url) {
res.header(200);
this.link.put(
this.link.create({ url : url }),
function (link) {
res.end(link.getCode());
}
);
},
Если пользователь хочет перейти по сокращенной ссылке — шлем его куда надо, или сообщаем, что такой ссылки нету
send : function (res, code) {
this.link.get(
this.link.create({ code : code }),
function (link) {
if (link) {
res.redirect(link.getUrl());
} else {
res.header(404, true);
res.end('There is not such url');
}
}
);
},
Для главной страницы — просто выводим файл index.html. Обратите внимание, я рекомендую обязательно указывать путь с __dirname вначале, т.к. иначе вы можете наткнуться на неприятности
index : function (res) {
fs.readFile(__dirname + '/index.html', function (err, data) {
if (err) throw err;
res.header(200);
res.end(data);
})
}
Главная страница
На главной странице мы поступим просто — подключим с репов гугл джиквери, добавим поле ввода и когда пользователь захочет сократить ссылку — мы аджаксом получив код новой ссылки, красиво выведем её под полем ввода.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Сокращатель ссылок на node.js</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script>
$(function () {
$('input[type=submit]').click(function() {
var input = $('input[type=text]');
var url = input.val();
input.val('');
url && $.ajax({
url : './',
data: ({ add : url }),
success : function (data) {
var result = location.protocol + '//' + location.host + '/!' + data;
$('#url')
.prepend(
$('<dd>').append(
$('<a>')
.text(result)
.attr('href', result)
)
.hide()
.fadeIn()
)
.prepend(
$('<dt>')
.text(url)
.hide()
.fadeIn()
);
}
});
})
});
</script>
</head>
<body>
<div id="form">
<input type="text" />
<input type="submit" />
</div>
<dl id="url"></dl>
</body>
</html>
Все.
Результат
Сразу предупрежу — сохранность ваших ссылок не гарантирую, сервис исключительно демонстративный.
Весь код в одном месте на pastebin