
Привет, Хабр! В этой статье я пошагово рассмотрю создание простого веб-приложения — сокращателя ссылок на 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.jsvar 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