Думаю, мы можем опять обнулить счетчик времени появления очередной JS библиотеки.
Все началось примерно 6 лет назад, когда я познакомился с node.js. Около 3 лет назад я начал использовать node.js на проектах вместе с замечательной библиотекой express.js (на wiki она названа каркасом приложений, хотя некоторые могут называть express фреймворком или даже пакетом). Express сочетает в себе node.js http сервер и систему промежуточного ПО, созданную по образу каркаса Sinatra из Ruby.
Все мы знаем о скорости создания новых библиотек и скорости развития JS. После разделения и объединения с IO.js node.js взяла себе лучшее из мира JS — ES6, а в апреле и ES7.
Об одном из этих изменений и хочу поговорить. А конкретно о async / await и Promise. Пытаясь использовать Promise в проектах на express, а после и async / await с флагом для node.js 7 --harmony, я наткнулся на интересный фреймворк нового поколения — koa.js, а конкретно на его вторую версию.
Первая версия была создана с помощью генераторов и библиотеки CO. Вторая версия обещает удобство при работе с Promise / async / await и ждет апрельского релиза node.js с поддержкой этих возможностей без флагов.
Мне стало интересно заглянуть в ядро koa и узнать, как реализована работа с Promise. Но я был удивлен, т.к. ядро осталось практически таким же, как в предыдущей версии. Авторы обеих библиотек express и koa одни и те же, неудивительно, что и подход остался таким же. Я имею ввиду структуру промежуточного ПО (middleware). Использовать подход из Ruby было полезно на этапе становления node.js, но современный node.js, как и JS, имеет свои преимущества, красоту, элегантность...
Немного теории.
Node.js http (https) сервер наследует net.Server, который реализовывает EventEmitter. И все библиотеки (express, koa...) по сути являются обработчиками события server.on('request').
Например:
const http = require('http');
const server = http.createServer((request, response) => {
// обработка события
});
Или
const server = http.createServer();
server.on('request', (request, response) => {
// такая же обработка события
});
И я представил, как должен выглядеть действительно "фреймворк нового поколения":
const server = http.createServer( (req, res) => {
Promise.resolve({ req, res }).then(ctx => {
ctx.res.writeHead(200, {'Content-Type': 'text/plain'});
ctx.res.end('OK');
return ctx;
});
});
Это дает отличную возможность избавиться от callback hell и постоянной обработки ошибок на всех уровнях, как, например, реализовано в express. Также, это позволяет применить Promise.all() для "параллельного" выполнения промежуточного ПО вместо последовательного.
И так появилась еще одна библиотека: YEPS — Yet Another Event Promised Server.
Синтаксис YEPS передает всю простоту и элегантность архитектуры, основанной на обещаниях (promise based design), например, параллельная обработка промежуточного ПО:
const App = require('yeps');
const app = new App();
const error = require('yeps-error');
const logger = require('yeps-logger');
app.all([
logger(),
error()
]);
app.then(async ctx => {
ctx.res.writeHead(200, {'Content-Type': 'text/plain'});
ctx.res.end('Ok');
});
app.catch(async (err, ctx) => {
ctx.res.writeHead(500);
ctx.res.end(err.message);
});
Или
app.all([
logger(),
error()
]).then(async ctx => {
ctx.res.writeHead(200, {'Content-Type': 'text/plain'});
ctx.res.end('Ok');
}).catch(async (err, ctx) => {
ctx.res.writeHead(500);
ctx.res.end(err.message);
});
Для примера есть пакеты error, logger, redis.
Но самым удивительным была скорость работы. Можно запустить сравнительный тест производительности — yeps-benchmark, где сравнивается производительность работы YEPS с express, koa2 и даже node.js http.
Как видим, параллельное выполнение показывает интересные результаты. Хотя этого можно достичь в любом проекте, этот подход должен быть заложен в архитектуру, в саму идею — не делать ни одного шага без тестирования производительности. Например, ядро библиотеки — yeps-promisify, использует array.slice(0) — наиболее быстрый метод копирования массива.
Возможность параллельного выполнения промежуточного ПО натолкнула на мысль создания маршрутизатора (router, роутер), полностью созданного на Promise.all(). Сама идея поймать (catch) нужный маршрут (route), нужное правило и соответственно вернуть нужный обработчик лежит в основе Promise.all().
const Router = require('yeps-router');
const router = new Router();
router.catch({ method: 'GET', url: '/' }).then(async ctx => {
ctx.res.writeHead(200);
ctx.res.end('homepage');
});
router.get('/test').then(async ctx => {
ctx.res.writeHead(200);
ctx.res.end('test');
}).post('/test/:id').then(async ctx => {
ctx.res.writeHead(200);
ctx.res.end(ctx.request.params.id);
});
app.then(router.resolve());
Вместо последовательного перебора всех правил можно одновременно запустить проверку всех. Этот момент не остался без тестирования производительности и результаты не заставили себя ждать.
Поиск первого правила был на примерно 10% быстрее. Последнее правило срабатывало ровно с той же скоростью, что примерно в 4 раза быстрее остальных библиотек (здесь речь идет о 10 маршрутах). Больше не нужно собирать и анализировать статистику, думать какое правило поднять вверх,.
Но для полноценной production ready работы необходимо было решить проблему "курицы и яйца" — никто не будет использовать библиотеку без дополнительных пакетов и никто не будет писать пакеты к неиспользуемой библиотеке. Здесь помогла обертка (wrapper), позволяющая использовать промежуточное ПО от express, например body-parser или serve-favicon…
const error = require('yeps-error');
const wrapper = require('yeps-express-wrapper');
const bodyParser = require('body-parser');
const favicon = require('serve-favicon');
const path = require('path');
app.then(
wrapper(favicon(path.join(__dirname, 'public', 'favicon.ico')))
).all([
error(),
wrapper(bodyParser.json()),
]);
Так же есть шаблон приложения — yeps-boilerplate, позволяющий запустить новое приложение, просмотреть код, примеры…
Надеюсь это исследование и результат будет полезен, может даже даже вдохновит на создание красивых, быстрых, возможно даже элегантных решений. И конечно же идея тестировать производительность каждого шага должна лечь в основу любого нового и существующего проекта.
P.S.: Надеюсь на советы, идеи и конструктивную критику в комментариях.
Автор: evheniy