Node.JS
February 2017 24

Очередная node.js-библиотека…

From Sandbox

Думаю, мы можем опять обнулить счетчик времени появления очередной 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.: Надеюсь на советы, идеи и конструктивную критику в комментариях.


+15
13.1k 57
Comments 40