Pull to refresh

node.js сокращатель ссылок

Reading time6 min
Views8K

Привет, Хабр! В этой статье я пошагово рассмотрю создание простого веб-приложения — сокращателя ссылок на 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
Tags:
Hubs:
Total votes 56: ↑52 and ↓4+48
Comments60

Articles