Pull to refresh

Сюрреализм на JavaScript. Советы по разработке на NodeJS

Reading time 9 min
Views 41K
Привет, Хабра!

Пол года назад я подумал: «А может книгу написать?», и таки написал.



Все документы оформлены, страницы сверстаны, а тираж — отпечатан. Я не буду клянчить у вас деньги на кикстартере или предлагать что-либо купить, а вместо этого попытаюсь заинтриговать советами по разработке на NodeJS в целях пиара и привлечения внимания к книге.

Совет 1. SQL запросы лучше хранить отформатированными


SQL запросы лучше хранить отформатированными, т.к. код в этом случае гораздо проще читать и править. Т.к. SQL-запросы обычно довольно длинные, то лучше разбивать их на несколько строк, а строки в JavaScript — лучше всего выглядят в массиве.

До:
var query = "SELECT g.id, g.title, g.description, g.link, g.icon, t.id as tag_id, c.title as comment_title FROM games AS g LEFT JOIN tags AS t ON t.game_id = g.id LEFT JOIN comments AS c ON c.game_id = g.id WHERE g.id = $1";

После:
var query = [
	"SELECT ",
	"    g.id, g.title, g.description, g.link, g.icon, ",
	"    t.id as tag_id, c.title as comment_title ",
	"  FROM games AS g ",
	"    LEFT JOIN tags AS t ON  t.game_id = g.id ",
	"    LEFT JOIN comments AS c ON  c.game_id = g.id ",
	"  WHERE ",
	"    g.id = $1"
];

Согласитесь, что во втором случае запрос гораздо понятнее для читателя. Кроме того, если вы перед запросом в базу выводите запрос в консоль, то массив, прокинутый в console.dir(), опять таки гораздо понятнее, чем строка, прокинутая в console.log().



Совет 2. Жизнь становится проще, когда API различных компонентов принимает на вход любой формат


Предположим мы создали клиента к базе данных и хотим выполнить некий SQL-запрос. На входе мы ожидаем строку и параметры. Т.к. воспользовавшись прошлым советом мы решили хранить длинные запросы в массивах, то хотелось бы забыть про его преобразование в строку на каждом запросе.

До:
dataBase(query.join(""), parameters, callback);

После:
dataBase(query, parameters, callback);

Код становится проще, когда функция запроса к базе (в данном случае dataBase), сама проверяет в каком виде ей передали запрос, и если это массив — сама делает ему join().

Совет 3. Разукрасьте консоль и отформатируйте вывод информации


Дебажить программы на NodeJS трудно, т.к. очень сильно не хватает стандартной консоли разработчика со всеми фишками, типа «точек остановки». Все данные пишутся в консоль, и хочется сделать её более понятной. Если у вас нода крутится где-то на сервере, да ещё и в несколько инстансов, да ещё и несколько разных сервисов на каждой инстансе висит, а доступ вы имеете только по SSH, то консоль может реально заставить страдать.

Если в NodeJS вывести в консоль строку вида «Hello world!» с управляющими ANSI-символами, она будет окрашена в разные цвета. Пример использования управляющих ANSI-символов:



Чтобы не запоминать подобные хаки, вы можете подключить модуль colors и использовать следующий синтаксис:

console.log("Error! Parameter ID not found.".red);

Строка будет выведена красным цветом. При разработке с этим модулем вы можете раскрасить сообщения в консоли в различные цвета:
  • Красный (ошибка).
  • Желтый (предупреждение).
  • Зеленый (все хорошо).
  • Серый. Им можно выводить какие-либо параметры (например, параметры запроса), на которые можно не обращать внимания, пока не поймаете ошибку.

Консоль до цветового выделения (при быстром просмотре информация воспринимается с трудом):



Консоль после цветового выделения (при быстром просмотре информация воспринимается достаточно быстро):



Благодаря раскрашенной консоли вам будет гораздо легче следить за состоянием сервера. Кроме того, если на одной инстансе у вас висит сразу несколько сервисов, которые работают с разными процессами, вы также должны писать в модулях отдельные методы для вывода в консоль. Например:

var book = {
    console: function(message, color) {
        console.log(("Book API: " + (message || ""))[(color || white")]);
    }
}


Если все сообщения в консоли подписаны модулями, которые их отправляют, вы не только сможете сделать выборку по конкретному модулю, но и моментально найдете источник бага в критической ситуации. Форматирование текста также упрощает восприятие информации:



Таким образом, вся информация в консоли будет подписана, и вы легко сможете понять, какие события происходят в тех или иных модулях. Опять же, цветовое выделение помогает в стрессовых ситуациях, когда отказала та или иная система и нужно срочно исправить ошибку, и вчитываться в логи нет времени (я не призываю вас дебажить на продакшне, просто всякое бывает). В своих проектах я решил переписать модуль консоли, чтобы иметь возможность раскрашивать не только строки, но и массивы и объекты, а также автоматически подписывать все инстансы. Поэтому при подключении модуля я передаю ему имя пакета, которым следует подписывать сообщения. Пример использования нового модуля консоли:

var console = require("./my_console")("Scoring SDK");
console.green("Request for DataBase.");
console.grey([
    "SELECT *",
    "  FROM \"ScoringSDK__game_list\"",
    "  WHERE key = $1;"
]);

Пример вывода данных в консоль:



Совет 4. Оборачивайте все API в try/catch


У нас на работе используется фреймворк express. Каково же было мое удивление, когда я узнал, что в стандартном объекте роутера нет обертки try/catch. Например:
  1. Вы написали кривой модуль
  2. Кто-то дернул его по URL`у
  3. У вас упал сервер
  4. WTF!?


Поэтому всегда оборачивайте внешнее API модулей в try/catch. Если что-то пойдет не так, ваш кривой модуль, по крайней мере, не завалит всю систему. Та же ситуация на клиенте с шаблоном «медиатор» (его ещё называют «слушатели и публикующие»). Например:
  • Модуль А опубликовал сообщение.
  • Модули Б и В должны услышать его и отреагировать.
  • Система в цикле начинает перебирать подписчиков.
  • Модуль Б падает с ошибкой.
  • Цикл обрывается и callback-функция модуля В не вызывается.


Гораздо лучше делать перебор в try/catch и если модуль Б действительно упадет с ошибкой, то по крайней мере не убьет систему и модуль В выполнит свою работу услышав событие.

Т.к. при написании API модулей мне приходилось вновь и вновь отделять приватные и публичные методы, а после оборачивать все публичные методы в try/catch, я решил это дело автоматизировать и написал небольшой модуль для автогенерации API. Например, кидаем в него объект вида:

var a = {
	_b: function() { ... },
	_c: function() { ... },
	d:  function() { ... }
}


Из именования методов ясно, что первые два — приватные, а последний — публичный. Модуль создаст обертку для вызова последнего, вида:

var api = {
	d: function() {
		try {
			return a.d();
		} catch(e) {
			return false;
		}
	}
};

Таким образом, я стал генерировать обертку для API всех модулей, которая в случае возникновения ошибки не пропускала её дальше. Это сделало код более стабильным, т.к. ошибка отдельного разработчика, слитая в продакшн, уже не могла уронить весь сервер со всем его функционалом.

Пример генерации API:
var a = {
	_b: function() { ... },
	_c: function() { ... },
	d:  function() { ... }
}

var api = api.create(a);

api.d(); // пример вызова


Совет 5. Собирайте запросы в конфиги


Я думаю, у каждого веб-разработчика была ситуация, когда был какой-либо жирный клиент, которому нужно было небольшое API для работы с базой данных на сервере. Пару запросов на чтение, пару на запись и ещё несколько для удаления информации. Логики в таком сервере обычно нет, и он представляет собой просто набор запросов.

Чтобы не писать каждый раз обертки для таких операций, я решил вынести все запросы в JSON, а сервер — оформить в виде небольшого модуля, который предоставляет мне API для работы с этим JSON`ом.

Пример такого модуля под express:

var fastQuery = require("./fastQuery"),
	API = fastQuery({
		scoring: {
			get: {
				query: "SELECT * FROM score LIMIT $1, $2;"
				parameters: [ "limit", "offset" ]
			},
			set: {
				query: "INSERT INTO score (user_id, score, date) VALUES ...",
				parameters: [ "id", "score" ]
			}
		},
		profile: {
			get: {
				query: "SELECT * FROM users WHERE id = $1;",
				parameters: [ "id" ]
			}
		}
	});

Наверное, вы уже догадались, что модуль будет пробегать по JSON`у и искать объекты со свойствами query и parameters. Если такие объекты будут найдены, то он создаст для них функцию, которая будет проверять параметры, ходить в базу с запросами, и посылать клиенту результат. На выходе мы получим такое API:

API.scoring.get();
API.scoring.set();
API.profile.get();

И уже его привяжем к объекту роутера:

exports.initRoutes = function (app) {
    app.get("/scoring/get", API.scoring.get);
    app.put("/scoring/set", API.scoring.set);
    app.get("/profile/get", API.profile.get);
}

Я не буду впраривать свой фреймворк для этой цели, т.к. стек технологий на сервере разный в разных фирмах. Для работы с подобным объектом вам в любом случае понадобится писать небольшую обвязку, поверх чего-либо, для обработки запросов и работы с базой. Кроме того, возможно, в момент, когда вы будете читать эти строки, уже будет несколько готовых фреймворков для этой задачи.

А теперь представьте, что у вас есть ещё два серверных разработчика. Один пишет на PHP, а второй на Java. Если у вас вся серверная логика ограничивается только таким JSON`ом со списком запросов к базе, то вы можете моментально перенести/развернуть аналогичное API не только на другой машине, но и на абсолютно другом языке (при условии, что общение с клиентом стандартизировано и все общаются по REST API).

Совет 6. Выносите все в конфиги


Т.к. писать конфиги я люблю, у меня неоднократно возникала ситуация, когда у системы есть стандартные настройки, настройки для конкретного случая и настройки, возникшие в данные момент времени. Мне приходилось делать mix разных JSON объектов. Я решил выделить отдельный модуль для этих целей, а заодно добавил в него возможность брать JSON объекты из файла, т.к. хранить настройки в отдельном json-файле тоже очень удобно. Таким образом, теперь, когда мне нужно задать настройки для чего-либо я пишу:

var data = config.get("config.json", "save.json", {
	name: "Petr",
	age: 12
});

Как вы уже могли догадаться, модуль перебирает переданные ему аргументы. Если это строка — то он пытается открыть файл и прочитать настройки из него, если это объект — то он сразу пытается скрестить его с предыдущими.

В примере выше мы сначала берем некие стандартные настройки из файла config.json, потом накладываем на них сохраненные настройки из файла save.json, а потом добавляем настройки, которые актуальны в данный момент времени. На выходе мы получим mix из трех JSON объектов. Количество аргументов переданных модулю может быть любым. Например, мы можем попросить пригнать только настройки по умолчанию:

var data = config.get("config.json");


Совет 7. Работа с файлами и Модуль Social Link для СЕО


Одна из главных фич, которые мне нравятся в NodeJS, возможность работать с файлами и писать парсеры на JavaScript. При том API NodeJS предоставляет множество методов и способов для решения задач, но на практике — нужно совсем не много. За полгода активной работы с парсерами я использовал только две команды — прочитать и записать в файл. Притом, чтобы не страдать с callback-функциями и различными проблемами асинхронности, всю работу с файлами я всегда делал в синхронном режиме. Так появился небольшой модуль работы с файлами, API которого очень напоминало localStorage:

var file = requery("./utils.file.js"), // подключили модуль
	text = file.get("text.txt);        // прочитали текст в файле
	
file.set("text.txt", "Hello world!");  // записали текст в файл

На основание этого модуля работы с файлами, стали появятся другие модули. Например, модуль для СЕО. В одной из прошлых статей я уже писал, что существует огромное количество различных meta-тегов связанных с СЕО. Когда я начинал писать систему сборку для HTML приложений, СЕО я уделил особое внимание.

Суть заключается в том, что у нас есть небольшой текстовый файл с описанием сайта/приложения/игры и непосредственно HTML файл для разбора. Модуль Social Link должен найти все meta-теги связанные с СЕО в HTML файле и заполнить их. Внешнее API модуля ожидает на входе текст из файла. Это сделано для того, чтобы была возможность подключать его к системам сборки и прогонять через него текст нескольких файлов не вызывая каждый раз лишнюю процедуру чтения/записи в файл.

Например, до модуля:
<title></title>
<meta property="og:title" content=""/>
<meta name="twitter:title" content=""/>

После модуля:
<title>Некий заголовок</title>
<meta property="og:title" content="Некий заголовок"/>
<meta name="twitter:title" content="Некий заголовок"/>

Список и описание всех meta-тегов для СЕО и не только, вы можете посмотреть в книге http://bakhirev.biz/.

Совет 8. Без callback`ов жизнь проще

Многие разработчики жалуются на бесконечные цепочки callback`ов при написании сервера на NodeJS. На самом деле вы не всегда обязаны их писать и часто можно выстроить архитектуру, при которой такие цепочки будут минимальны. Рассмотрим небольшую задачу.

Задача:
Перегнать файлы с сервера А на сервер Б, получить некоторую информацию из базы данных, обновить эту информацию, отправить данные на сервер В.

Решение:
Это довольно рутинная процедура, которую мне неоднократно приходилось выполнять для решения каких-либо задач по сортировке / обработке контента. Обычно разработчики создают цикл и некую callback-функцию с методом nextFile(). Когда на очередной итерации мы вызываем nextFile(), механизм callback`ов начинается с начала, и мы обрабатываем следующий файл. Как правило, требуется в один момент времени обрабатывать только один файл и при удачном завершении процедуры переходить к обработке следующего файла. Упростить вложенность нам поможет код вида:

var locked = false,
	index = 0,
	timer = setInterval(function() {
		if(locked) return;
		locked = true;
		nextFile(index++);
	}, 1000);

Теперь мы будем раз в секунду пытаться начать обработку файла. Если программа освободится, то она выставит locked в значение false и мы сможем запустить следующий файл на обработку. Такие конструкции очень часто помогают уменьшать вложенность, распределить нагрузку по времени (т.к. очередная итерация обработки у нас запускается не чаще, чем один раз в секунду) и хоть немного сползать с бесконечных callback`ов.

Итого
Файлы с модулями можно скачать тут: http://bakhirev.biz/_node.zip (сейчас 2 часа ночи и мне лень разбираться с GitHub`ом и приводить код в человеческий вид).
Книга тут: http://bakhirev.biz/
На случай хабро-эффекта тут в PDF.

Если советы выше пришлись вам по вкусу, то хочу сразу предупредить, что книга совсем про другое. А ещё там в конце список разных умных людей, которые внесли неоценимый вклад сами того не подозревая, и которых точно следует найти и прочитать по отдельности.
Tags:
Hubs:
+36
Comments 36
Comments Comments 36

Articles