view xml/ru/docs/njs/node_modules.xml @ 2629:5cd72684e5b8

Fixed typo and updated description in Using node modules.
author Yaroslav Zhuravlev <yar@nginx.com>
date Thu, 03 Dec 2020 12:28:46 +0000
parents fca42223b9fc
children
line wrap: on
line source

<?xml version="1.0"?>

<!--
  Copyright (C) Nginx, Inc.
  -->

<!DOCTYPE article SYSTEM "../../../../dtd/article.dtd">

<article name="Использование модулей Node.js в njs"
        link="/ru/docs/njs/node_modules.html"
        lang="en"
        rev="6">

<section id="intro">

<para>
Часто разработчику приходится использовать сторонний код и,
как правило, такой код доступен в виде библиотеки.
В JavaScript концепция модулей является новой и
до недавнего времени не была стандартизированa.
До сих пор множество платформ или браузеров не поддерживают модули,
по этой причине практически невозможно повторно использовать код.
В данной статье приводятся способы повторного использования
кода в njs при помощи <link url="https://nodejs.org/">Node.js</link>.
</para>

<note>
В примерах статьи используется функциональность
<link doc="index.xml">njs</link>
<link doc="changes.xml" id="njs0.3.8">0.3.8</link>
</note>

<para>
При добавлении стороннего кода в njs
может возникнуть несколько проблем:

<list type="bullet">

<listitem>
большое количество файлов, ссылающихся друг на друга, и их зависимости
</listitem>

<listitem>
платформозависимые API
</listitem>

<listitem>
языковые конструкции нового стандарта
</listitem>

</list>
</para>

<para>
Однако это не является чем-то новым или специфичным для njs.
Разработчикам JavaScript приходится часто иметь дело с подобными случаями,
например при поддержке нескольких несхожих платформ
с разными свойствами.
Данные проблемы можно разрешить при помощи следующих инструментов:

<list type="bullet">

<listitem>
Большое количество файлов, ссылающихся друг на друга, и их зависимости
<para>
Решение: слияние всего независимого кода в один файл.
Для этих целей могут использоваться утилиты
<link url="http://browserify.org/">browserify</link> или
<link url="https://webpack.js.org/">webpack</link>,
позволяющие преобразовать проект в один файл, содержащий
код и все зависимости.
</para>
</listitem>

<listitem>
Платформозависимые API
<para>
Решение: использование библиотек, реализующих подобные API
в платформонезависимом режиме, однако в ущерб производительности.
Определённая функциональность может быть также реализована при помощи
<link url="https://polyfill.io/v3/">polyfill</link>.
</para>
</listitem>

<listitem>
Языковые конструкции нового стандарта
<para>
Решение: трансплирование кода&mdash;
ряд преобразований,
заменяющих новые функции языка в соответствии со старым стандартом.
Для этих целей может использоваться
<link url="https://babeljs.io/"> babel</link>.
</para>
</listitem>

</list>
</para>

<para>
В статье также используются две относительно большие
библиотеки на основе npm:

<list type="bullet">

<listitem>
<link url="https://www.npmjs.com/package/protobufjs">protobufjs</link>&mdash;
библиотека для создания и парсинга protobuf-сообщений, используемая
протоколом <link url="https://grpc.io/">gRPC</link>
</listitem>

<listitem>
<link url="https://www.npmjs.com/package/dns-packet">dns-packet</link>&mdash;
библиотека для обработки пакетов протокола DNS
</listitem>

</list>
</para>

</section>


<section id="environment" name="Окружение">

<para>
<note>
В статье описываются общие принципы работы
и не ставится цель описания подробных сценариев работы с Node.js
и JavaScript.
Перед выполнением команд
необходимо ознакомиться с документацией соответствующих пакетов.
</note>
Сначала, предварительно установив и запустив Node.js, необходимо создать
пустой проект и установить зависимости;
для выполнения нижеперечисленных команд необходимо
находиться в рабочем каталоге:
<example>
$ mkdir my_project &amp;&amp; cd my_project
$ npx license choose_your_license_here > LICENSE
$ npx gitignore node

$ cat &gt; package.json &lt;&lt;EOF
{
  "name":        "foobar",
  "version":     "0.0.1",
  "description": "",
  "main":        "index.js",
  "keywords":    [],
  "author":      "somename &lt;some.email@example.com&gt; (https://example.com)",
  "license":     "some_license_here",
  "private":     true,
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  }
}
EOF
$ npm init -y
$ npm install browserify
</example>
</para>

</section>


<section id="protobuf" name="Protobufjs">

<para>
Библиотека предоставляет парсер
для определения интерфейса <literal>.proto</literal>,
а также генератор кода для парсинга и генерации сообщений.
</para>

<para>
В данном примере используется
файл
<link url="https://github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto">helloworld.proto</link>
из примеров gRPC.
Целью является создание двух сообщений:
<literal>HelloRequest</literal> и
<literal>HelloResponse</literal>.
Также используется
<link url="https://github.com/protobufjs/protobuf.js/blob/master/README.md#reflection-vs-static-code">статический</link>
режим protobufjs вместо динамически генерируемых классов, так как
njs не поддерживает динамическое добавление новых функций
из соображений безопасности.
</para>

<para>
Затем устанавливается библиотека,
из определения протокола генерируется код JavaScript,
реализующий маршалинг сообщений:
<example>
$ npm install protobufjs
$ npx pbjs -t static-module helloworld.proto > static.js
</example>
</para>

<para>
Таким образом файл <literal>static.js</literal> становится новой зависимостью,
хранящей необходимый код для реализации обработки сообщений.
Функция <literal>set_buffer()</literal> содержит код, использующий
библиотеку для создания буфера с сериализованным
сообщением <literal>HelloRequest</literal>.
Код находится в файле <literal>code.js</literal>:
<example>
var pb = require('./static.js');

// Пример использования библиотеки protobuf: подготовка буфера к отправке
function set_buffer(pb)
{
    // назначение полей gRPC payload
    var payload = { name: "TestString" };

    // создание объекта
    var message = pb.helloworld.HelloRequest.create(payload);

    // сериализация объекта в буфер
    var buffer = pb.helloworld.HelloRequest.encode(message).finish();

    var n = buffer.length;

    var frame = new Uint8Array(5 + buffer.length);

    frame[0] = 0;                        // флаг 'compressed'
    frame[1] = (n &amp; 0xFF000000) &gt;&gt;&gt; 24;  // длина: uint32 в сетевом порядке байт
    frame[2] = (n &amp; 0x00FF0000) &gt;&gt;&gt; 16;
    frame[3] = (n &amp; 0x0000FF00) &gt;&gt;&gt;  8;
    frame[4] = (n &amp; 0x000000FF) &gt;&gt;&gt;  0;

    frame.set(buffer, 5);

    return frame;
}

var frame = set_buffer(pb);
</example>
</para>

<para>
Для проверки работоспособности необходимо выполнить код при помощи node:
<example>
$ node ./code.js
Uint8Array [
    0,   0,   0,   0,  12, 10,
   10,  84, 101, 115, 116, 83,
  116, 114, 105, 110, 103
]
</example>
Результатом является закодированный фрейм <literal>gRPC</literal>.
Теперь фрейм можно запустить с njs:
<example>
$ njs ./code.js
Thrown:
Error: Cannot find module "./static.js"
    at require (native)
    at main (native)
</example>
</para>

<para>
Так как модули не поддерживаются, то операция завершается получением исключения.
В этом случае можно использовать утилиту <literal>browserify</literal>
или другую подобную утилиту.
</para>

<para>
Попытка обработки файла <literal>code.js</literal> завершится
большим количеством JS-кода, который предполагается запускать в браузере,
то есть сразу после загрузки.
Однако необходимо получить другой результат&mdash;
экспортируемую функцию, на которую
можно сослаться из конфигурации nginx.
Для этого потребуется создание кода-обёртки.
<note>
В целях упрощения в примерах данной статьи
используется <link doc="cli.xml">интерфейс комадной строки</link> njs.
На практике для запуска кода обычно используется njs-модуль для nginx.
</note>
</para>

<para>
Файл <literal>load.js</literal> содержит код, загружающий библиотеку,
храняющую дескриптор в глобальном пространстве имён:
<example>
global.hello = require('./static.js');
</example>
Данный код будет заменён объединённым содержимым.
Код будет использовать дескриптор "<literal>global.hello</literal>" для доступа
к библиотеке.
</para>

<para>
Затем для получения всех зависимостей в один файл
код обрабатыается утилитой <literal>browserify</literal>:
<example>
$ npx browserify load.js -o bundle.js -d
</example>
В результате генерируется объёмный файл, содержащий все зависимости:
<example>
(function(){function......
...
...
},{"protobufjs/minimal":9}]},{},[1])
//# sourceMappingURL..............
</example>
Для получения результирующего файла "<literal>njs_bundle.js</literal>"
необходимо объединить "<literal>bundle.js</literal>" и следующий код:
<example>
// Пример использования библиотеки protobuf: подготовка буфера к отправке
function set_buffer(pb)
{
    // назначение полей gRPC payload
    var payload = { name: "TestString" };

    // создание объекта
    var message = pb.helloworld.HelloRequest.create(payload);

    // сериализация объекта в буфер
    var buffer = pb.helloworld.HelloRequest.encode(message).finish();

    var n = buffer.length;

    var frame = new Uint8Array(5 + buffer.length);

    frame[0] = 0;                        // флаг 'compressed'
    frame[1] = (n &amp; 0xFF000000) &gt;&gt;&gt; 24;  // длина: uint32 в сетевом порядке байт
    frame[2] = (n &amp; 0x00FF0000) &gt;&gt;&gt; 16;
    frame[3] = (n &amp; 0x0000FF00) &gt;&gt;&gt;  8;
    frame[4] = (n &amp; 0x000000FF) &gt;&gt;&gt;  0;

    frame.set(buffer, 5);

    return frame;
}

// функции, вызываемые снаружи
function setbuf()
{
    return set_buffer(global.hello);
}

// вызов кода
var frame = setbuf();
console.log(frame);
</example>
Для проверки работоспособности необходимо запустить файл при помощи node:
<example>
$ node ./njs_bundle.js
Uint8Array [
    0,   0,   0,   0,  12, 10,
   10,  84, 101, 115, 116, 83,
  116, 114, 105, 110, 103
]
</example>
Дальнейшие шаги выполняются при помощи njs:
<example>
$ njs ./njs_bundle.js
Uint8Array [0,0,0,0,12,10,10,84,101,115,116,83,116,114,105,110,103]
</example>
Теперь необходимо задействовать njs API для преобразования
массива в байтовую строку для дальнейшего использования модулем nginx.
Данный код необходимо добавить перед строкой
<literal>return frame; }</literal>:
<example>
if (global.njs) {
    return String.bytesFrom(frame)
}
</example>
Проверка работоспособности:
<example>
$ njs ./njs_bundle.js |hexdump -C
00000000  00 00 00 00 0c 0a 0a 54  65 73 74 53 74 72 69 6e  |.......TestStrin|
00000010  67 0a                                             |g.|
00000012
</example>
Экспортируемая функция получена.
Парсинг ответа может быть сделан аналогичным способом:
<example>
function parse_msg(pb, msg)
{
    // преобразование байтовой строки в массив целых чисел
    var bytes = msg.split('').map(v=>v.charCodeAt(0));

    if (bytes.length &lt; 5) {
        throw 'message too short';
    }

    // первые 5 байт являются фреймом gRPC (сжатие + длина)
    var head = bytes.splice(0, 5);

    // проверка правильной длины сообщения
    var len = (head[1] &lt;&lt; 24)
              + (head[2] &lt;&lt; 16)
              + (head[3] &lt;&lt; 8)
              + head[4];

    if (len != bytes.length) {
        throw 'header length mismatch';
    }

    // вызов protobufjs для декодирования сообщения
    var response = pb.helloworld.HelloReply.decode(bytes);

    console.log('Reply is:' + response.message);
}
</example>
</para>

</section>


<section id="dnspacket" name="Пакет DNS">

<para>
В примере используется библиотека для создания и парсинга пакетов DNS.
Эта библиотека, а также её зависимости,
использует современные языковые конструкции, не поддерживаемые в njs.
Для поддержки таких конструкций
потребуется дополнительный шаг: транспилирование исходного кода.
</para>

<para>
Необходимо установить дополнительные пакеты node:
<example>
$ npm install @babel/core @babel/cli @babel/preset-env babel-loader
$ npm install webpack webpack-cli
$ npm install buffer
$ npm install dns-packet
</example>
Файл конфигурации webpack.config.js:
<example>
const path = require('path');

module.exports = {
    entry: './load.js',
    mode: 'production',
    output: {
        filename: 'wp_out.js',
        path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
        minimize: false
    },
    node: {
        global: true,
    },
    module : {
        rules: [{
            test: /\.m?js$$/,
            exclude: /(bower_components)/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            }
        }]
    }
};
</example>
В данном случае используется режим "<literal>production</literal>".
Конструкция "<literal>eval</literal>" не используется, так как
не поддерживается njs.
Точкой входа является файл <literal>load.js</literal>:
<example>
global.dns = require('dns-packet')
global.Buffer = require('buffer/').Buffer
</example>
Сначала необходимо создать единый файл для библиотек, как в предыдущих примерах:
<example>
$ npx browserify load.js -o bundle.js -d
</example>
Затем необходимо обработать утилитой webpack, что также запускает babel:
<example>
$ npx webpack --config webpack.config.js
</example>
Команда создаёт файл <literal>dist/wp_out.js</literal>, являющийся
трансплицированной версией <literal>bundle.js</literal>.
Далее необходимо объединить этот файл с <literal>code.js</literal>,
хранящим код:
<example>
function set_buffer(dnsPacket)
{
    // create DNS packet bytes
    var buf = dnsPacket.encode({
        type: 'query',
        id: 1,
        flags: dnsPacket.RECURSION_DESIRED,
        questions: [{
            type: 'A',
            name: 'google.com'
        }]
    })

    return buf;
}
</example>
В данном примере генерируемый код не обёрнут в функцию,
явного вызова не требуется.
Результат доступен в каталоге "<literal>dist</literal>":
<example>
$ cat dist/wp_out.js code.js > njs_dns_bundle.js
</example>
Далее осуществляется вызов кода в конце файла:
<example>
var b = set_buffer(global.dns);
console.log(b);
</example>
И затем выполнение кода при помощи node:
<example>
$ node ./njs_dns_bundle_final.js
Buffer [Uint8Array] [
    0,   1,   1, 0,  0,   1,   0,   0,
    0,   0,   0, 0,  6, 103, 111, 111,
  103, 108, 101, 3, 99, 111, 109,   0,
    0,   1,   0, 1
]
</example>
Тестирование и запуск кода вместе с njs:
<example>
$ njs ./njs_dns_bundle_final.js
Uint8Array [0,1,1,0,0,1,0,0,0,0,0,0,6,103,111,111,103,108,101,3,99,111,109,0,0,1,0,1]
</example>
</para>

<para>
Ответ можно распарсить следующим способом:
<example>
function parse_response(buf)
{
    var bytes = buf.split('').map(v=>v.charCodeAt(0));

    var b = global.Buffer.from(bytes);

    var packet = dnsPacket.decode(b);

    var resolved_name = packet.answers[0].name;

    // ожидаемое имя 'google.com', согласно запросу выше
}
</example>

</para>

</section>

</article>