Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
nginx
GitHub Repository: nginx/nginx.org
Path: blob/main/xml/ru/docs/njs/node_modules.xml
1 views
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="6">
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;[email protected]&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
<literal>return frame; }</literal>:
363
<example>
364
if (global.njs) {
365
return String.bytesFrom(frame)
366
}
367
</example>
368
Проверка работоспособности:
369
<example>
370
$ njs ./njs_bundle.js |hexdump -C
371
00000000 00 00 00 00 0c 0a 0a 54 65 73 74 53 74 72 69 6e |.......TestStrin|
372
00000010 67 0a |g.|
373
00000012
374
</example>
375
Экспортируемая функция получена.
376
Парсинг ответа может быть сделан аналогичным способом:
377
<example>
378
function parse_msg(pb, msg)
379
{
380
// преобразование байтовой строки в массив целых чисел
381
var bytes = msg.split('').map(v=>v.charCodeAt(0));
382
383
if (bytes.length &lt; 5) {
384
throw 'message too short';
385
}
386
387
// первые 5 байт являются фреймом gRPC (сжатие + длина)
388
var head = bytes.splice(0, 5);
389
390
// проверка правильной длины сообщения
391
var len = (head[1] &lt;&lt; 24)
392
+ (head[2] &lt;&lt; 16)
393
+ (head[3] &lt;&lt; 8)
394
+ head[4];
395
396
if (len != bytes.length) {
397
throw 'header length mismatch';
398
}
399
400
// вызов protobufjs для декодирования сообщения
401
var response = pb.helloworld.HelloReply.decode(bytes);
402
403
console.log('Reply is:' + response.message);
404
}
405
</example>
406
</para>
407
408
</section>
409
410
411
<section id="dnspacket" name="Пакет DNS">
412
413
<para>
414
В примере используется библиотека для создания и парсинга пакетов DNS.
415
Эта библиотека, а также её зависимости,
416
использует современные языковые конструкции, не поддерживаемые в njs.
417
Для поддержки таких конструкций
418
потребуется дополнительный шаг: транспилирование исходного кода.
419
</para>
420
421
<para>
422
Необходимо установить дополнительные пакеты node:
423
<example>
424
$ npm install @babel/core @babel/cli @babel/preset-env babel-loader
425
$ npm install webpack webpack-cli
426
$ npm install buffer
427
$ npm install dns-packet
428
</example>
429
Файл конфигурации webpack.config.js:
430
<example>
431
const path = require('path');
432
433
module.exports = {
434
entry: './load.js',
435
mode: 'production',
436
output: {
437
filename: 'wp_out.js',
438
path: path.resolve(__dirname, 'dist'),
439
},
440
optimization: {
441
minimize: false
442
},
443
node: {
444
global: true,
445
},
446
module : {
447
rules: [{
448
test: /\.m?js$$/,
449
exclude: /(bower_components)/,
450
use: {
451
loader: 'babel-loader',
452
options: {
453
presets: ['@babel/preset-env']
454
}
455
}
456
}]
457
}
458
};
459
</example>
460
В данном случае используется режим "<literal>production</literal>".
461
Конструкция "<literal>eval</literal>" не используется, так как
462
не поддерживается njs.
463
Точкой входа является файл <literal>load.js</literal>:
464
<example>
465
global.dns = require('dns-packet')
466
global.Buffer = require('buffer/').Buffer
467
</example>
468
Сначала необходимо создать единый файл для библиотек, как в предыдущих примерах:
469
<example>
470
$ npx browserify load.js -o bundle.js -d
471
</example>
472
Затем необходимо обработать утилитой webpack, что также запускает babel:
473
<example>
474
$ npx webpack --config webpack.config.js
475
</example>
476
Команда создаёт файл <literal>dist/wp_out.js</literal>, являющийся
477
трансплицированной версией <literal>bundle.js</literal>.
478
Далее необходимо объединить этот файл с <literal>code.js</literal>,
479
хранящим код:
480
<example>
481
function set_buffer(dnsPacket)
482
{
483
// create DNS packet bytes
484
var buf = dnsPacket.encode({
485
type: 'query',
486
id: 1,
487
flags: dnsPacket.RECURSION_DESIRED,
488
questions: [{
489
type: 'A',
490
name: 'google.com'
491
}]
492
})
493
494
return buf;
495
}
496
</example>
497
В данном примере генерируемый код не обёрнут в функцию,
498
явного вызова не требуется.
499
Результат доступен в каталоге "<literal>dist</literal>":
500
<example>
501
$ cat dist/wp_out.js code.js > njs_dns_bundle.js
502
</example>
503
Далее осуществляется вызов кода в конце файла:
504
<example>
505
var b = set_buffer(global.dns);
506
console.log(b);
507
</example>
508
И затем выполнение кода при помощи node:
509
<example>
510
$ node ./njs_dns_bundle_final.js
511
Buffer [Uint8Array] [
512
0, 1, 1, 0, 0, 1, 0, 0,
513
0, 0, 0, 0, 6, 103, 111, 111,
514
103, 108, 101, 3, 99, 111, 109, 0,
515
0, 1, 0, 1
516
]
517
</example>
518
Тестирование и запуск кода вместе с njs:
519
<example>
520
$ njs ./njs_dns_bundle_final.js
521
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]
522
</example>
523
</para>
524
525
<para>
526
Ответ можно распарсить следующим способом:
527
<example>
528
function parse_response(buf)
529
{
530
var bytes = buf.split('').map(v=>v.charCodeAt(0));
531
532
var b = global.Buffer.from(bytes);
533
534
var packet = dnsPacket.decode(b);
535
536
var resolved_name = packet.answers[0].name;
537
538
// ожидаемое имя 'google.com', согласно запросу выше
539
}
540
</example>
541
542
</para>
543
544
</section>
545
546
</article>
547
548