comparison xml/ru/docs/njs/node_modules.xml @ 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 xml/en/docs/njs/node_modules.xml@4c8d0b37932d
children fca42223b9fc
comparison
equal deleted inserted replaced
2576:4c8d0b37932d 2577:67fd664e2612
1 <?xml version="1.0"?>
2
3 <!--
4 Copyright (C) Nginx, Inc.
5 -->
6
7 <!DOCTYPE article SYSTEM "../../../../dtd/article.dtd">
8
9 <article name="Использование модулей Node.js в njs"
10 link="/ru/docs/njs/node_modules.html"
11 lang="en"
12 rev="4">
13
14 <section id="intro">
15
16 <para>
17 Часто разработчику приходится использовать сторонний код и,
18 как правило, такой код доступен в виде библиотеки.
19 В JavaScript концепция модулей является новой и
20 до недавнего времени не была стандартизированa.
21 До сих пор множество платформ или браузеров не поддерживают модули,
22 по этой причине практически невозможно повторно использовать код.
23 В данной статье приводятся способы повторного использования
24 кода в njs при помощи <link url="https://nodejs.org/">Node.js</link>.
25 </para>
26
27 <note>
28 В примерах статьи используется функциональность
29 <link doc="index.xml">njs</link>
30 <link doc="changes.xml" id="njs0.3.8">0.3.8</link>
31 </note>
32
33 <para>
34 При добавлении стороннего кода в njs
35 может возникнуть несколько проблем:
36
37 <list type="bullet">
38
39 <listitem>
40 большое количество файлов, ссылающихся друг на друга, и их зависимости
41 </listitem>
42
43 <listitem>
44 платформозависимые API
45 </listitem>
46
47 <listitem>
48 языковые конструкции нового стандарта
49 </listitem>
50
51 </list>
52 </para>
53
54 <para>
55 Однако это не является чем-то новым или специфичным для njs.
56 Разработчикам JavaScript приходится часто иметь дело с подобными случаями,
57 например при поддержке нескольких несхожих платформ
58 с разными свойствами.
59 Данные проблемы можно разрешить при помощи следующих инструментов:
60
61 <list type="bullet">
62
63 <listitem>
64 Большое количество файлов, ссылающихся друг на друга, и их зависимости
65 <para>
66 Решение: слияние всего независимого кода в один файл.
67 Для этих целей могут использоваться утилиты
68 <link url="http://browserify.org/">browserify</link> или
69 <link url="https://webpack.js.org/">webpack</link>,
70 позволяющие преобразовать проект в один файл, содержащий
71 код и все зависимости.
72 </para>
73 </listitem>
74
75 <listitem>
76 Платформозависимые API
77 <para>
78 Решение: использование библиотек, реализующих подобные API
79 в платформонезависимом режиме, однако в ущерб производительности.
80 Определённая функциональность может быть также реализована при помощи
81 <link url="https://polyfill.io/v3/">polyfill</link>.
82 </para>
83 </listitem>
84
85 <listitem>
86 Языковые конструкции нового стандарта
87 <para>
88 Решение: трансплирование кода&mdash;
89 ряд преобразований,
90 заменяющих новые функции языка в соответствии со старым стандартом.
91 Для этих целей может использоваться
92 <link url="https://babeljs.io/"> babel</link>.
93 </para>
94 </listitem>
95
96 </list>
97 </para>
98
99 <para>
100 В статье также используются две относительно большие
101 библиотеки на основе npm:
102
103 <list type="bullet">
104
105 <listitem>
106 <link url="https://www.npmjs.com/package/protobufjs">protobufjs</link>&mdash;
107 библиотека для создания и парсинга protobuf-сообщений, используемая
108 протоколом <link url="https://grpc.io/">gRPC</link>
109 </listitem>
110
111 <listitem>
112 <link url="https://www.npmjs.com/package/dns-packet">dns-packet</link>&mdash;
113 библиотека для обработки пакетов протокола DNS
114 </listitem>
115
116 </list>
117 </para>
118
119 </section>
120
121
122 <section id="environment" name="Окружение">
123
124 <para>
125 <note>
126 В статье описываются общие принципы работы
127 и не ставится цель описания подробных сценариев работы с Node.js
128 и JavaScript.
129 Перед выполнением команд
130 необходимо ознакомиться с документацией соответствующих пакетов.
131 </note>
132 Сначала, предварительно установив и запустив Node.js, необходимо создать
133 пустой проект и установить зависимости;
134 для выполнения нижеперечисленных команд необходимо
135 находиться в рабочем каталоге:
136 <example>
137 $ mkdir my_project &amp;&amp; cd my_project
138 $ npx license choose_your_license_here > LICENSE
139 $ npx gitignore node
140
141 $ cat &gt; package.json &lt;&lt;EOF
142 {
143 "name": "foobar",
144 "version": "0.0.1",
145 "description": "",
146 "main": "index.js",
147 "keywords": [],
148 "author": "somename &lt;some.email@example.com&gt; (https://example.com)",
149 "license": "some_license_here",
150 "private": true,
151 "scripts": {
152 "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
153 }
154 }
155 EOF
156 $ npm init -y
157 $ npm install browserify
158 </example>
159 </para>
160
161 </section>
162
163
164 <section id="protobuf" name="Protobufjs">
165
166 <para>
167 Библиотека предоставляет парсер
168 для определения интерфейса <literal>.proto</literal>,
169 а также генератор кода для парсинга и генерации сообщений.
170 </para>
171
172 <para>
173 В данном примере используется
174 файл
175 <link url="https://github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto">helloworld.proto</link>
176 из примеров gRPC.
177 Целью является создание двух сообщений:
178 <literal>HelloRequest</literal> и
179 <literal>HelloResponse</literal>.
180 Также используется
181 <link url="https://github.com/protobufjs/protobuf.js/blob/master/README.md#reflection-vs-static-code">статический</link>
182 режим protobufjs вместо динамически генерируемых классов, так как
183 njs не поддерживает динамическое добавление новых функций
184 из соображений безопасности.
185 </para>
186
187 <para>
188 Затем устанавливается библиотека,
189 из определения протокола генерируется код JavaScript,
190 реализующий маршалинг сообщений:
191 <example>
192 $ npm install protobufjs
193 $ npx pbjs -t static-module helloworld.proto > static.js
194 </example>
195 </para>
196
197 <para>
198 Таким образом файл <literal>static.js</literal> становится новой зависимостью,
199 хранящей необходимый код для реализации обработки сообщений.
200 Функция <literal>set_buffer()</literal> содержит код, использующий
201 библиотеку для создания буфера с сериализованным
202 сообщением <literal>HelloRequest</literal>.
203 Код находится в файле <literal>code.js</literal>:
204 <example>
205 var pb = require('./static.js');
206
207 // Пример использования библиотеки protobuf: подготовка буфера к отправке
208 function set_buffer(pb)
209 {
210 // назначение полей gRPC payload
211 var payload = { name: "TestString" };
212
213 // создание объекта
214 var message = pb.helloworld.HelloRequest.create(payload);
215
216 // сериализация объекта в буфер
217 var buffer = pb.helloworld.HelloRequest.encode(message).finish();
218
219 var n = buffer.length;
220
221 var frame = new Uint8Array(5 + buffer.length);
222
223 frame[0] = 0; // флаг 'compressed'
224 frame[1] = (n &amp; 0xFF000000) &gt;&gt;&gt; 24; // длина: uint32 в сетевом порядке байт
225 frame[2] = (n &amp; 0x00FF0000) &gt;&gt;&gt; 16;
226 frame[3] = (n &amp; 0x0000FF00) &gt;&gt;&gt; 8;
227 frame[4] = (n &amp; 0x000000FF) &gt;&gt;&gt; 0;
228
229 frame.set(buffer, 5);
230
231 return frame;
232 }
233
234 var frame = set_buffer(pb);
235 </example>
236 </para>
237
238 <para>
239 Для проверки работоспособности необходимо выполнить код при помощи node:
240 <example>
241 $ node ./code.js
242 Uint8Array [
243 0, 0, 0, 0, 12, 10,
244 10, 84, 101, 115, 116, 83,
245 116, 114, 105, 110, 103
246 ]
247 </example>
248 Результатом является закодированный фрейм <literal>gRPC</literal>.
249 Теперь фрейм можно запустить с njs:
250 <example>
251 $ njs ./code.js
252 Thrown:
253 Error: Cannot find module "./static.js"
254 at require (native)
255 at main (native)
256 </example>
257 </para>
258
259 <para>
260 Так как модули не поддерживаются, то операция завершается получением исключения.
261 В этом случае можно использовать утилиту <literal>browserify</literal>
262 или другую подобную утилиту.
263 </para>
264
265 <para>
266 Попытка обработки файла <literal>code.js</literal> завершится
267 большим количеством JS-кода, который предполагается запускать в браузере,
268 то есть сразу после загрузки.
269 Однако необходимо получить другой результат&mdash;
270 экспортируемую функцию, на которую
271 можно сослаться из конфигурации nginx.
272 Для этого потребуется создание кода-обёртки.
273 <note>
274 В целях упрощения в примерах данной статьи
275 используется <link doc="cli.xml">интерфейс комадной строки</link> njs.
276 На практике для запуска кода обычно используется njs-модуль для nginx.
277 </note>
278 </para>
279
280 <para>
281 Файл <literal>load.js</literal> содержит код, загружающий библиотеку,
282 храняющую дескриптор в глобальном пространстве имён:
283 <example>
284 global.hello = require('./static.js');
285 </example>
286 Данный код будет заменён объединённым содержимым.
287 Код будет использовать дескриптор "<literal>global.hello</literal>" для доступа
288 к библиотеке.
289 </para>
290
291 <para>
292 Затем для получения всех зависимостей в один файл
293 код обрабатыается утилитой <literal>browserify</literal>:
294 <example>
295 $ npx browserify load.js -o bundle.js -d
296 </example>
297 В результате генерируется объёмный файл, содержащий все зависимости:
298 <example>
299 (function(){function......
300 ...
301 ...
302 },{"protobufjs/minimal":9}]},{},[1])
303 //# sourceMappingURL..............
304 </example>
305 Для получения результирующего файла "<literal>njs_bundle.js</literal>"
306 необходимо объединить "<literal>bundle.js</literal>" и следующий код:
307 <example>
308 // Пример использования библиотеки protobuf: подготовка буфера к отправке
309 function set_buffer(pb)
310 {
311 // назначение полей gRPC payload
312 var payload = { name: "TestString" };
313
314 // создание объекта
315 var message = pb.helloworld.HelloRequest.create(payload);
316
317 // сериализация объекта в буфер
318 var buffer = pb.helloworld.HelloRequest.encode(message).finish();
319
320 var n = buffer.length;
321
322 var frame = new Uint8Array(5 + buffer.length);
323
324 frame[0] = 0; // флаг 'compressed'
325 frame[1] = (n &amp; 0xFF000000) &gt;&gt;&gt; 24; // длина: uint32 в сетевом порядке байт
326 frame[2] = (n &amp; 0x00FF0000) &gt;&gt;&gt; 16;
327 frame[3] = (n &amp; 0x0000FF00) &gt;&gt;&gt; 8;
328 frame[4] = (n &amp; 0x000000FF) &gt;&gt;&gt; 0;
329
330 frame.set(buffer, 5);
331
332 return frame;
333 }
334
335 // функции, вызываемые снаружи
336 function setbuf()
337 {
338 return set_buffer(global.hello);
339 }
340
341 // вызов кода
342 var frame = setbuf();
343 console.log(frame);
344 </example>
345 Для проверки работоспособности необходимо запустить файл при помощи node:
346 <example>
347 $ node ./njs_bundle.js
348 Uint8Array [
349 0, 0, 0, 0, 12, 10,
350 10, 84, 101, 115, 116, 83,
351 116, 114, 105, 110, 103
352 ]
353 </example>
354 Дальнейшие шаги выполняются при помощи njs:
355 <example>
356 $ /njs ./njs_bundle.js
357 Uint8Array [0,0,0,0,12,10,10,84,101,115,116,83,116,114,105,110,103]
358 </example>
359 Теперь необходимо задействовать njs API для преобразования
360 массива в байтовую строку для дальнейшего использования модулем nginx.
361 Данный код необходимо добавить перед последней строкой:
362 <example>
363 if (global.njs) {
364 return String.bytesFrom(frame)
365 }
366 </example>
367 Проверка работоспособности:
368 <example>
369 $ njs ./njs_bundle.js |hexdump -C
370 00000000 00 00 00 00 0c 0a 0a 54 65 73 74 53 74 72 69 6e |.......TestStrin|
371 00000010 67 0a |g.|
372 00000012
373 </example>
374 Экспортируемая функция получена.
375 Парсинг ответа может быть сделан аналогичным способом:
376 <example>
377 function parse_msg(pb, msg)
378 {
379 // преобразование байтовой строки в массив целых чисел
380 var bytes = msg.split('').map(v=>v.charCodeAt(0));
381
382 if (bytes.length &lt; 5) {
383 throw 'message too short';
384 }
385
386 // первые 5 байт являются фреймом gRPC (сжатие + длина)
387 var head = bytes.splice(0, 5);
388
389 // проверка правильной длины сообщения
390 var len = (head[1] &lt;&lt; 24)
391 + (head[2] &lt;&lt; 16)
392 + (head[3] &lt;&lt; 8)
393 + head[4];
394
395 if (len != bytes.length) {
396 throw 'header length mismatch';
397 }
398
399 // вызов protobufjs для декодирования сообщения
400 var response = pb.helloworld.HelloReply.decode(bytes);
401
402 console.log('Reply is:' + response.message);
403 }
404 </example>
405 </para>
406
407 </section>
408
409
410 <section id="dnspacket" name="Пакет DNS">
411
412 <para>
413 В примере используется библиотека для создания и парсинга пакетов DNS.
414 Эта библиотека, а также её зависимости,
415 использует современные языковые конструкции, не поддерживаемые в njs.
416 Для поддержки таких конструкций
417 потребуется дополнительный шаг: транспилирование исходного кода.
418 </para>
419
420 <para>
421 Необходимо установить дополнительные пакеты node:
422 <example>
423 $ npm install @babel/core @babel/cli @babel/preset-env babel-loader
424 $ npm install webpack webpack-cli
425 $ npm install buffer
426 $ npm install dns-packet
427 </example>
428 Файл конфигурации webpack.config.js:
429 <example>
430 const path = require('path');
431
432 module.exports = {
433 entry: './load.js',
434 mode: 'production',
435 output: {
436 filename: 'wp_out.js',
437 path: path.resolve(__dirname, 'dist'),
438 },
439 optimization: {
440 minimize: false
441 },
442 node: {
443 global: true,
444 },
445 module : {
446 rules: [{
447 test: /\.m?js$$/,
448 exclude: /(bower_components)/,
449 use: {
450 loader: 'babel-loader',
451 options: {
452 presets: ['@babel/preset-env']
453 }
454 }
455 }]
456 }
457 };
458 </example>
459 В данном случае используется режим "<literal>production</literal>".
460 Конструкция "<literal>eval</literal>" не используется, так как
461 не поддерживается njs.
462 Точкой входа является файл <literal>load.js</literal>:
463 <example>
464 global.dns = require('dns-packet')
465 global.Buffer = require('buffer/').Buffer
466 </example>
467 Сначала необходимо создать единый файл для библиотек, как в предыдущих примерах:
468 <example>
469 $ npx browserify load.js -o bundle.js -d
470 </example>
471 Затем необходимо обработать утилитой webpack, что также запускает babel:
472 <example>
473 $ npx webpack --config webpack.config.js
474 </example>
475 Команда создаёт файл <literal>dist/wp_out.js</literal>, являющийся
476 трансплицированной версией <literal>bundle.js</literal>.
477 Далее необходимо объединить этот файл с <literal>code.js</literal>,
478 хранящим код:
479 <example>
480 function set_buffer(dnsPacket)
481 {
482 // create DNS packet bytes
483 var buf = dnsPacket.encode({
484 type: 'query',
485 id: 1,
486 flags: dnsPacket.RECURSION_DESIRED,
487 questions: [{
488 type: 'A',
489 name: 'google.com'
490 }]
491 })
492
493 return buf;
494 }
495 </example>
496 В данном примере генерируемый код не обёрнут в функцию,
497 явного вызова не требуется.
498 Результат доступен в каталоге "<literal>dist</literal>":
499 <example>
500 $ cat dist/wp_out.js code.js > njs_dns_bundle.js
501 </example>
502 Далее осуществляется вызов кода в конце файла:
503 <example>
504 var b = setbuf(1);
505 console.log(b);
506 </example>
507 И затем выполнение кода при помощи node:
508 <example>
509 $ node ./njs_dns_bundle_final.js
510 Buffer [Uint8Array] [
511 0, 1, 1, 0, 0, 1, 0, 0,
512 0, 0, 0, 0, 6, 103, 111, 111,
513 103, 108, 101, 3, 99, 111, 109, 0,
514 0, 1, 0, 1
515 ]
516 </example>
517 Тестирование и запуск кода вместе с njs:
518 <example>
519 $ njs ./njs_dns_bundle_final.js
520 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]
521 </example>
522 </para>
523
524 <para>
525 Ответ можно распарсить следующим способом:
526 <example>
527 function parse_response(buf)
528 {
529 var bytes = buf.split('').map(v=>v.charCodeAt(0));
530
531 var b = global.Buffer.from(bytes);
532
533 var packet = dnsPacket.decode(b);
534
535 var resolved_name = packet.answers[0].name;
536
537 // ожидаемое имя 'google.com', согласно запросу выше
538 }
539 </example>
540
541 </para>
542
543 </section>
544
545 </article>