Spec-Zone.ru › nginx

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

  • Окружение
  • Protobufjs
  • DNS-пакет

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

Примеры в этой статье используют функции, появившиеся в njs 0.3.8

Возникает ряд проблем при добавлении стороннего кода в njs:

  • Несколько файлов, ссылающихся друг на друга и на свои зависимости
  • Платформенно-специфичные API
  • Современные стандартные конструкции языка

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

  • Несколько файлов, ссылающихся друг на друга и на свои зависимости

    Это можно решить, объединив весь взаимозависимый код в один файл. Инструменты, такие как browserify или webpack, принимают весь проект и создают один файл, содержащий ваш код и все зависимости.

  • Платформенно-специфичные API

    Можно использовать несколько библиотек, которые реализуют такие API в платформенно-независимом формате (хотя это может повлиять на производительность). Отдельные функции также могут быть реализованы с помощью подхода polyfill.

  • Современные стандартные конструкции языка

    Такой код можно транспилировать: это означает выполнение ряда преобразований, которые переписывают новые возможности языка в соответствии со старым стандартом. Например, для этой цели можно использовать проект babel.

В этом руководстве мы будем использовать две относительно большие библиотеки, размещенные в npm:

  • protobufjs — библиотека для создания и разбора сообщений protobuf, используемых протоколом gRPC
  • dns-packet — библиотека для обработки пакетов протокола DNS

Окружение

Этот документ в основном использует общий подход и избегает конкретных рекомендаций по лучшим практикам для 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');

// Example usage of protobuf library: prepare a buffer to send
function set_buffer(pb)
{
    // set fields of gRPC payload
    var payload = { name: "TestString" };

    // create an object
    var message = pb.helloworld.HelloRequest.create(payload);

    // serialize object to buffer
    var buffer = pb.helloworld.HelloRequest.encode(message).finish();

    var n = buffer.length;

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

    frame[0] = 0;                        // 'compressed' flag
    frame[1] = (n & 0xFF000000) >>> 24;  // length: uint32 in network byte order
    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 cli во всех примерах ради простоты. В реальной жизни вы будете использовать модуль nginx njs для выполнения своего кода.

Файл 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" и следующий код:

// Example usage of protobuf library: prepare a buffer to send
function set_buffer(pb)
{
    // set fields of gRPC payload
    var payload = { name: "TestString" };

    // create an object
    var message = pb.helloworld.HelloRequest.create(payload);

    // serialize object to buffer
    var buffer = pb.helloworld.HelloRequest.encode(message).finish();

    var n = buffer.length;

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

    frame[0] = 0;                        // 'compressed' flag
    frame[1] = (n & 0xFF000000) >>> 24;  // length: uint32 in network byte order
    frame[2] = (n & 0x00FF0000) >>> 16;
    frame[3] = (n & 0x0000FF00) >>>  8;
    frame[4] = (n & 0x000000FF) >>>  0;

    frame.set(buffer, 5);

    return frame;
}

// functions to be called from outside
function setbuf()
{
    return set_buffer(global.hello);
}

// call the code
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]

Последним шагом будет использование API njs для преобразования массива в строку байтов, чтобы ее можно было использовать модулем 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)
{
    // convert byte string into integer array
    var bytes = msg.split('').map(v=>v.charCodeAt(0));

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

    // first 5 bytes is gRPC frame (compression + length)
    var head = bytes.splice(0, 5);

    // ensure we have proper message length
    var len = (head[1] << 24)
              + (head[2] << 16)
              + (head[3] << 8)
              + head[4];

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

    // invoke protobufjs to decode message
    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". В этом режиме webpack не использует конструкцию "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;

    // expected name is 'google.com', according to our request above
}

© 2002-2021 Igor Sysoev
© 2011-2024 Nginx, Inc.
Licensed under the BSD License.
https://nginx.org/en/docs/njs/node_modules.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API