Привет!
Пол года назад я подумал: «А может книгу написать?», и таки написал.
Все документы оформлены, страницы сверстаны, а тираж — отпечатан. Я не буду клянчить у вас деньги на кикстартере или предлагать что-либо купить, а вместо этого попытаюсь заинтриговать советами по разработке на 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. Например:
- Вы написали кривой модуль
- Кто-то дернул его по URL`у
- У вас упал сервер
- 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.
Если советы выше пришлись вам по вкусу, то хочу сразу предупредить, что книга совсем про другое. А ещё там в конце список разных умных людей, которые внесли неоценимый вклад сами того не подозревая, и которых точно следует найти и прочитать по отдельности.
Автор: bakhirev