Mercurial > hg > nginx-site
view xml/ru/docs/njs/node_modules.xml @ 2643:4849fa0fd4b4
Removed URL Decoding/Encoding examples from njs.
author | Yaroslav Zhuravlev <yar@nginx.com> |
---|---|
date | Mon, 25 Jan 2021 19:13:09 +0000 |
parents | 5cd72684e5b8 |
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> Решение: трансплирование кода— ряд преобразований, заменяющих новые функции языка в соответствии со старым стандартом. Для этих целей может использоваться <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 <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 </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>