Использование модулей Node.js в njs

Окружение
Protobufjs
Пакет DNS

Часто разработчику приходится использовать сторонний код и, как правило, такой код доступен в виде библиотеки. В JavaScript концепция модулей является новой и до недавнего времени не была стандартизированa. До сих пор множество платформ или браузеров не поддерживают модули, по этой причине практически невозможно повторно использовать код. В данной статье приводятся способы повторного использования кода в njs при помощи Node.js.

В примерах статьи используется функциональность njs 0.3.8

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

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

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

Окружение

В статье описываются общие принципы работы и не ставится цель описания подробных сценариев работы с Node.js и JavaScript. Перед выполнением команд необходимо ознакомиться с документацией соответствующих пакетов.

Сначала, предварительно установив и запустив Node.js, необходимо создать пустой проект и установить зависимости; для выполнения нижеперечисленных команд необходимо находиться в рабочем каталоге:

$ 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 <some.email@example.com> (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

Protobufjs

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

В данном примере используется файл helloworld.proto из примеров gRPC. Целью является создание двух сообщений: HelloRequest и HelloResponse. Также используется статический режим protobufjs вместо динамически генерируемых классов, так как njs не поддерживает динамическое добавление новых функций из соображений безопасности.

Затем устанавливается библиотека, из определения протокола генерируется код JavaScript, реализующий маршалинг сообщений:

$ npm install protobufjs
$ npx pbjs -t static-module helloworld.proto > static.js

Таким образом файл static.js становится новой зависимостью, хранящей необходимый код для реализации обработки сообщений. Функция set_buffer() содержит код, использующий библиотеку для создания буфера с сериализованным сообщением HelloRequest. Код находится в файле code.js:

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);

Для проверки работоспособности необходимо выполнить код при помощи node:

$ node ./code.js
Uint8Array [
    0,   0,   0,   0,  12, 10,
   10,  84, 101, 115, 116, 83,
  116, 114, 105, 110, 103
]

Результатом является закодированный фрейм gRPC. Теперь фрейм можно запустить с njs:

$ njs ./code.js
Thrown:
Error: Cannot find module "./static.js"
    at require (native)
    at main (native)

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

Попытка обработки файла code.js завершится большим количеством JS-кода, который предполагается запускать в браузере, то есть сразу после загрузки. Однако необходимо получить другой результат — экспортируемую функцию, на которую можно сослаться из конфигурации nginx. Для этого потребуется создание кода-обёртки.

В целях упрощения в примерах данной статьи используется интерфейс комадной строки njs. На практике для запуска кода обычно используется njs-модуль для nginx.

Файл load.js содержит код, загружающий библиотеку, храняющую дескриптор в глобальном пространстве имён:

global.hello = require('./static.js');

Данный код будет заменён объединённым содержимым. Код будет использовать дескриптор "global.hello" для доступа к библиотеке.

Затем для получения всех зависимостей в один файл код обрабатыается утилитой browserify:

$ npx browserify load.js -o bundle.js -d

В результате генерируется объёмный файл, содержащий все зависимости:

(function(){function......
...
...
},{"protobufjs/minimal":9}]},{},[1])
//# sourceMappingURL..............

Для получения результирующего файла "njs_bundle.js" необходимо объединить "bundle.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;
}

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

// вызов кода
var frame = setbuf();
console.log(frame);

Для проверки работоспособности необходимо запустить файл при помощи node:

$ node ./njs_bundle.js
Uint8Array [
    0,   0,   0,   0,  12, 10,
   10,  84, 101, 115, 116, 83,
  116, 114, 105, 110, 103
]

Дальнейшие шаги выполняются при помощи njs:

$ njs ./njs_bundle.js
Uint8Array [0,0,0,0,12,10,10,84,101,115,116,83,116,114,105,110,103]

Теперь необходимо задействовать njs API для преобразования массива в байтовую строку для дальнейшего использования модулем nginx. Данный код необходимо добавить перед строкой return frame; }:

if (global.njs) {
    return String.bytesFrom(frame)
}

Проверка работоспособности:

$ 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

Экспортируемая функция получена. Парсинг ответа может быть сделан аналогичным способом:

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);
}

Пакет DNS

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

Необходимо установить дополнительные пакеты node:

$ npm install @babel/core @babel/cli @babel/preset-env babel-loader
$ npm install webpack webpack-cli
$ npm install buffer
$ npm install dns-packet

Файл конфигурации webpack.config.js:

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']
                }
            }
        }]
    }
};

В данном случае используется режим "production". Конструкция "eval" не используется, так как не поддерживается njs. Точкой входа является файл load.js:

global.dns = require('dns-packet')
global.Buffer = require('buffer/').Buffer

Сначала необходимо создать единый файл для библиотек, как в предыдущих примерах:

$ npx browserify load.js -o bundle.js -d

Затем необходимо обработать утилитой webpack, что также запускает babel:

$ npx webpack --config webpack.config.js

Команда создаёт файл dist/wp_out.js, являющийся трансплицированной версией bundle.js. Далее необходимо объединить этот файл с code.js, хранящим код:

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;
}

В данном примере генерируемый код не обёрнут в функцию, явного вызова не требуется. Результат доступен в каталоге "dist":

$ cat dist/wp_out.js code.js > njs_dns_bundle.js

Далее осуществляется вызов кода в конце файла:

var b = set_buffer(global.dns);
console.log(b);

И затем выполнение кода при помощи node:

$ 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
]

Тестирование и запуск кода вместе с njs:

$ 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]

Ответ можно распарсить следующим способом:

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', согласно запросу выше
}