- PVSM.RU - https://www.pvsm.ru -

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

Думаю, мы можем опять обнулить счетчик времени появления очередной JS библиотеки.

Все началось примерно 6 лет назад, когда я познакомился с node.js [1]. Около 3 лет назад я начал использовать node.js на проектах вместе с замечательной библиотекой express.js [2] (на wiki она названа каркасом приложений, хотя некоторые могут называть express фреймворком или даже пакетом). Express сочетает в себе node.js http сервер и систему промежуточного ПО, созданную по образу каркаса Sinatra [3] из Ruby.

Все мы знаем о скорости создания новых библиотек и скорости развития JS. После разделения и объединения с IO.js node.js взяла себе лучшее из мира JS — ES6, а в апреле и ES7.

Об одном из этих изменений и хочу поговорить. А конкретно о async / await [4] и Promise [5]. Пытаясь использовать Promise в проектах на express, а после и async / await с флагом для node.js 7 [6] --harmony, я наткнулся на интересный фреймворк нового поколения [7] — koa.js, а конкретно на его вторую версию [8].

Первая версия была создана с помощью генераторов [9] и библиотеки CO [10]. Вторая версия обещает удобство при работе с Promise / async / await и ждет апрельского релиза node.js с поддержкой этих возможностей без флагов.

Мне стало интересно заглянуть в ядро koa [11] и узнать, как реализована работа с Promise. Но я был удивлен, т.к. ядро осталось практически таким же, как в предыдущей версии. Авторы обеих библиотек express и koa одни и те же, неудивительно, что и подход остался таким же. Я имею ввиду структуру промежуточного ПО (middleware) [12]. Использовать подход из Ruby было полезно на этапе становления node.js, но современный node.js, как и JS, имеет свои преимущества, красоту, элегантность...

Немного теории.

Node.js http [13] (https [14]) сервер наследует net.Server [15], который реализовывает EventEmitter [16]. И все библиотеки (express, koa...) по сути являются обработчиками события server.on('request') [17].
Например:

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 [18] и постоянной обработки ошибок на всех уровнях, как, например, реализовано в express. Также, это позволяет применить Promise.all() [19] для "параллельного" выполнения промежуточного ПО вместо последовательного.

И так появилась еще одна библиотека: YEPS [20] — 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 [21], logger [22], redis [23].

Но самым удивительным была скорость работы. Можно запустить сравнительный тест производительности — yeps-benchmark [24], где сравнивается производительность работы YEPS [25] с express [26], koa2 [27] и даже node.js http [28].

Как видим, параллельное выполнение показывает интересные результаты. Хотя этого можно достичь в любом проекте, этот подход должен быть заложен в архитектуру, в саму идею — не делать ни одного шага без тестирования производительности. Например, ядро библиотеки — yeps-promisify [29], использует array.slice(0) — наиболее быстрый метод копирования массива [30].

Возможность параллельного выполнения промежуточного ПО натолкнула на мысль создания маршрутизатора (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());

Вместо последовательного перебора всех правил можно одновременно запустить проверку всех. Этот момент не остался без тестирования производительности и результаты [24] не заставили себя ждать.

Поиск первого [31] правила был на примерно 10% быстрее. Последнее [32] правило срабатывало ровно с той же скоростью, что примерно в 4 раза быстрее остальных библиотек (здесь речь идет о 10 маршрутах). Больше не нужно собирать и анализировать статистику, думать какое правило поднять вверх,.

Но для полноценной production ready работы необходимо было решить проблему "курицы и яйца [33]" — никто не будет использовать библиотеку без дополнительных пакетов и никто не будет писать пакеты к неиспользуемой библиотеке. Здесь помогла обертка [34] (wrapper), позволяющая использовать промежуточное ПО от express, например body-parser [35] или serve-favicon [36]

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 [37], позволяющий запустить новое приложение, просмотреть код, примеры…

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

P.S.: Надеюсь на советы, идеи и конструктивную критику в комментариях.

Автор: evheniy

Источник [38]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/node-js/246118

Ссылки в тексте:

[1] node.js: https://nodejs.org/

[2] express.js: https://ru.wikipedia.org/wiki/Express.js

[3] Sinatra: http://www.sinatrarb.com/intro-ru.html

[4] async / await: https://habrahabr.ru/post/282477/

[5] Promise: https://habrahabr.ru/post/209662/

[6] node.js 7: https://habrahabr.ru/post/313658/

[7] фреймворк нового поколения: https://habrahabr.ru/post/301126/

[8] вторую версию: https://github.com/koajs/koa/tree/v2.x

[9] генераторов: https://habrahabr.ru/post/277033/

[10] CO: https://github.com/tj/co

[11] ядро koa: https://github.com/koajs/compose/tree/next

[12] промежуточного ПО (middleware): http://expressjs.com/ru/guide/using-middleware.html

[13] http: https://nodejs.org/api/http.html#http_class_http_server

[14] https: https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener

[15] net.Server: https://nodejs.org/api/net.html#net_class_net_server

[16] EventEmitter: https://nodejs.org/api/events.html#events_class_eventemitter

[17] server.on('request'): https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/

[18] callback hell: https://habrahabr.ru/post/319094/

[19] Promise.all(): https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

[20] YEPS: https://github.com/evheniy/yeps

[21] error: https://github.com/evheniy/yeps-error

[22] logger: https://github.com/evheniy/yeps-logger

[23] redis: https://github.com/evheniy/yeps-redis

[24] yeps-benchmark: https://github.com/evheniy/yeps-benchmark

[25] YEPS: https://raw.githubusercontent.com/evheniy/yeps-benchmark/master/reports/yeps_middleware.txt

[26] express: https://raw.githubusercontent.com/evheniy/yeps-benchmark/master/reports/express_middleware.txt

[27] koa2: https://raw.githubusercontent.com/evheniy/yeps-benchmark/master/reports/koa2_middleware.txt

[28] node.js http: https://raw.githubusercontent.com/evheniy/yeps-benchmark/master/reports/http_middleware.txt

[29] yeps-promisify: https://github.com/evheniy/yeps-promisify

[30] быстрый метод копирования массива: http://stackoverflow.com/questions/3978492/javascript-fastest-way-to-duplicate-an-array-slice-vs-for-loop

[31] первого: https://raw.githubusercontent.com/evheniy/yeps-benchmark/master/reports/yeps_route_first.txt

[32] Последнее: https://raw.githubusercontent.com/evheniy/yeps-benchmark/master/reports/yeps_route_last.txt

[33] курицы и яйца: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B1%D0%BB%D0%B5%D0%BC%D0%B0_%D0%BA%D1%83%D1%80%D0%B8%D1%86%D1%8B_%D0%B8_%D1%8F%D0%B9%D1%86%D0%B0

[34] обертка: https://github.com/evheniy/yeps-express-wrapper

[35] body-parser: https://github.com/expressjs/body-parser

[36] serve-favicon: https://github.com/expressjs/serve-favicon

[37] yeps-boilerplate: https://github.com/evheniy/yeps-boilerplate

[38] Источник: https://habrahabr.ru/post/322526/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best