<?xml version="1.0"?>
<!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>
Решение: трансплирование кода—
ряд преобразований,
заменяющих новые функции языка в соответствии со старым стандартом.
Для этих целей может использоваться
<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>—
библиотека для создания и парсинга protobuf-сообщений, используемая
протоколом <link url="https://grpc.io/">gRPC</link>
</listitem>
<listitem>
<link url="https://www.npmjs.com/package/dns-packet">dns-packet</link>—
библиотека для обработки пакетов протокола DNS
</listitem>
</list>
</para>
</section>
<section id="environment" name="Окружение">
<para>
<note>
В статье описываются общие принципы работы
и не ставится цель описания подробных сценариев работы с Node.js
и JavaScript.
Перед выполнением команд
необходимо ознакомиться с документацией соответствующих пакетов.
</note>
Сначала, предварительно установив и запустив Node.js, необходимо создать
пустой проект и установить зависимости;
для выполнения нижеперечисленных команд необходимо
находиться в рабочем каталоге:
<example>
$ mkdir my_project && cd my_project
$ npx license choose_your_license_here > LICENSE
$ npx gitignore node
$ cat > package.json <<EOF
{
"name": "foobar",
"version": "0.0.1",
"description": "",
"main": "index.js",
"keywords": [],
"author": "somename <[email protected]> (https://example.com)",
"license": "some_license_here",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && 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 & 0xFF000000) >>> 24; // длина: uint32 в сетевом порядке байт
frame[2] = (n & 0x00FF0000) >>> 16;
frame[3] = (n & 0x0000FF00) >>> 8;
frame[4] = (n & 0x000000FF) >>> 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-кода, который предполагается запускать в браузере,
то есть сразу после загрузки.
Однако необходимо получить другой результат—
экспортируемую функцию, на которую
можно сослаться из конфигурации 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 & 0xFF000000) >>> 24; // длина: uint32 в сетевом порядке байт
frame[2] = (n & 0x00FF0000) >>> 16;
frame[3] = (n & 0x0000FF00) >>> 8;
frame[4] = (n & 0x000000FF) >>> 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 < 5) {
throw 'message too short';
}
// первые 5 байт являются фреймом gRPC (сжатие + длина)
var head = bytes.splice(0, 5);
// проверка правильной длины сообщения
var len = (head[1] << 24)
+ (head[2] << 16)
+ (head[3] << 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>