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

в 14:04, , рубрики: async, await, express.js, koa.js, node.js, promise

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js