changeset 2577:67fd664e2612

Translated "Using node modules with njs" into Russian.
author Yaroslav Zhuravlev <yar@nginx.com>
date Thu, 06 Aug 2020 14:49:00 +0100
parents 4c8d0b37932d
children fafb7767c128
files xml/ru/GNUmakefile xml/ru/docs/njs/index.xml xml/ru/docs/njs/node_modules.xml
diffstat 3 files changed, 547 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/xml/ru/GNUmakefile	Thu Aug 06 14:46:58 2020 +0100
+++ b/xml/ru/GNUmakefile	Thu Aug 06 14:49:00 2020 +0100
@@ -112,6 +112,7 @@
 		njs/examples						\
 		njs/install						\
 		njs/reference						\
+		njs/node_modules					\
 		njs/typescript						\
 
 TOP =									\
--- a/xml/ru/docs/njs/index.xml	Thu Aug 06 14:46:58 2020 +0100
+++ b/xml/ru/docs/njs/index.xml	Thu Aug 06 14:49:00 2020 +0100
@@ -89,7 +89,7 @@
 </listitem>
 
 <listitem>
-<link doc="node_modules.xml">Использование модулей Node.js в njs</link>
+<link doc="node_modules.xml"/>
 </listitem>
 
 </list>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/xml/ru/docs/njs/node_modules.xml	Thu Aug 06 14:49:00 2020 +0100
@@ -0,0 +1,545 @@
+<?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="4">
+
+<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.
+Данный код необходимо добавить перед последней строкой:
+<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 = setbuf(1);
+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>