Mercurial > hg > nginx-site
comparison xml/en/docs/njs/node_modules.xml @ 2480:73d254c3376d
Added the "Using node modules with njs" article.
author | Vladimir Homutov <vl@nginx.com> |
---|---|
date | Tue, 21 Jan 2020 17:10:35 +0300 |
parents | |
children | 8ed243471444 |
comparison
equal
deleted
inserted
replaced
2479:b5138cd3321d | 2480:73d254c3376d |
---|---|
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="Using node modules with njs" | |
10 link="/en/docs/njs/node_modules.html" | |
11 lang="en" | |
12 rev="3"> | |
13 | |
14 <section id="intro" name="Introduction"> | |
15 | |
16 <para> | |
17 Often, a developer wants to use 3rd-party code, usually available as a library | |
18 of some kind. | |
19 In the Javascript world, the concept of a module is relatively new, so there | |
20 was no standard until recently. | |
21 Many platforms (browsers) still don't support modules, which makes code | |
22 reuse harder. | |
23 The njs does not (yet) support modules, too. | |
24 This article describes ways to overcome this limitation, using the | |
25 <link url="https://nodejs.org/">Node.js</link> ecosystem as an example. | |
26 </para> | |
27 | |
28 <note> | |
29 Examples in this article use features that appeared in | |
30 <link doc="index.xml">njs</link> | |
31 <link doc="changes.xml" id="njs0.3.8">0.3.8</link> | |
32 </note> | |
33 | |
34 <para> | |
35 There is a number of issues that may arise when 3rd-party code is added to njs: | |
36 | |
37 <list type="bullet"> | |
38 | |
39 <listitem>Multiple files that reference each other, and their | |
40 dependencies</listitem> | |
41 | |
42 <listitem>Platform-specific APIs</listitem> | |
43 | |
44 <listitem>Modern standard language constructions</listitem> | |
45 | |
46 </list> | |
47 | |
48 </para> | |
49 | |
50 <para> | |
51 The good news is that such problems are not something new or | |
52 specific to njs. | |
53 Javascript developers face them daily when trying to support multiple | |
54 disparate platforms with very different properties. | |
55 There are instruments designed to resolve the above-mentioned issues. | |
56 </para> | |
57 | |
58 <para> | |
59 | |
60 <list type="bullet"> | |
61 | |
62 <listitem> | |
63 Multiple files that reference each other, and their dependencies | |
64 <para> | |
65 This can be solved by merging all the interdependent code into a single file. | |
66 Tools like | |
67 <link url="http://browserify.org/">browserify</link> or | |
68 <link url="https://webpack.js.org/">webpack</link> | |
69 accept an entire project and produce a single file containing | |
70 your code and all the dependencies. | |
71 </para> | |
72 </listitem> | |
73 | |
74 <listitem> | |
75 Platform-specific APIs | |
76 <para> | |
77 You can use multiple libraries that implement such APIs in a platform-agnostic | |
78 manner (at the expense of performance, though). | |
79 Particular features can also be implemented using the | |
80 <link url="https://polyfill.io/v3/">polyfill</link> approach. | |
81 </para> | |
82 </listitem> | |
83 | |
84 <listitem> | |
85 Modern standard language constructions | |
86 <para> | |
87 Such code can be transpiled: this means performing a number of transformations | |
88 that rewrite newer language features in accordance with an older standard. | |
89 For example, <link url="https://babeljs.io/"> babel</link> project can | |
90 be used to this purpose. | |
91 </para> | |
92 </listitem> | |
93 | |
94 </list> | |
95 | |
96 </para> | |
97 | |
98 | |
99 <para> | |
100 In this guide, we will use two relatively large npm-hosted libraries: | |
101 | |
102 <list type="bullet"> | |
103 | |
104 <listitem> | |
105 <link url="https://www.npmjs.com/package/protobufjs">protobufjs</link> - | |
106 a library for creating and parsing protobuf messages used by the | |
107 <link url="https://grpc.io/">gRPC</link> protocol. | |
108 | |
109 </listitem> | |
110 | |
111 <listitem> | |
112 <link url="https://www.npmjs.com/package/dns-packet">dns-packet</link> - | |
113 a library for processing DNS protocol packets. | |
114 </listitem> | |
115 | |
116 </list> | |
117 | |
118 </para> | |
119 | |
120 </section> | |
121 | |
122 <section id="environment" name="Environment"> | |
123 | |
124 <para> | |
125 | |
126 <note> | |
127 This document mostly employs a generic approach and AVOIDS specific best | |
128 practice advices concerning Node.js and the rapidly evolving JavaScript | |
129 ecosystem. | |
130 Make sure to consult the corresponding package's manual BEFORE following the | |
131 steps suggested here. | |
132 </note> | |
133 | |
134 First (assuming Node.js is installed and operational), let's create an | |
135 empty project and install some dependencies; the commands below assume we're | |
136 in the working directory: | |
137 | |
138 <example> | |
139 $ mkdir my_project && cd my_project | |
140 $ npx license choose_your_license_here > LICENSE | |
141 $ npx gitignore node | |
142 | |
143 $ cat > package.json <<EOF | |
144 { | |
145 "name": "foobar", | |
146 "version": "0.0.1", | |
147 "description": "", | |
148 "main": "index.js", | |
149 "keywords": [], | |
150 "author": "somename <some.email@example.com> (https://example.com)", | |
151 "license": "some_license_here", | |
152 "private": true, | |
153 "scripts": { | |
154 "test": "echo \"Error: no test specified\" && exit 1" | |
155 } | |
156 } | |
157 EOF | |
158 $ npm init -y | |
159 $ npm install browserify | |
160 </example> | |
161 </para> | |
162 | |
163 </section> | |
164 | |
165 <section id="protobuf" name="Protobujfs"> | |
166 | |
167 <para> | |
168 The library provides a parser for the <literal>.proto</literal> interface | |
169 definitions and a code generator for message parsing and generation. | |
170 </para> | |
171 | |
172 <para> | |
173 In this example, we will use the | |
174 <link url="https://github.com/grpc/grpc/blob/master/examples/protos/helloworld.proto">helloworld.proto</link> | |
175 file from the gRPC examples. | |
176 Our goal is to create two messages: <literal>HelloRequest</literal> and | |
177 <literal>HelloResponse</literal>. | |
178 We will use the | |
179 <link url="https://github.com/protobufjs/protobuf.js/blob/master/README.md#reflection-vs-static-code">static</link> | |
180 mode of protobufjs instead of dynamically generating classes, because | |
181 njs doesn't support adding new functions dynamically due to security | |
182 considerations. | |
183 </para> | |
184 | |
185 <para> | |
186 Next, the library is installed and javascript code implementing | |
187 message marshalling is generated from the protocol definition: | |
188 <example> | |
189 $ npm install protobufjs | |
190 $ npx pbjs -t static-module helloworld.proto > static.js | |
191 </example> | |
192 | |
193 </para> | |
194 | |
195 <para> | |
196 Thus, the <literal>static.js</literal> file becomes our new dependency, storing | |
197 all the code we need to implement message processing. | |
198 The <literal>set_buffer()</literal> function contains code that uses the | |
199 library to create a buffer with the serialized <literal>HelloRequest</literal> | |
200 message. | |
201 The code resides in the <literal>code.js</literal> file: | |
202 | |
203 <example> | |
204 var pb = require('./static.js'); | |
205 | |
206 // Example usage of protobuf library: prepare a buffer to send | |
207 function set_buffer(pb) | |
208 { | |
209 // set fields of gRPC payload | |
210 var payload = { name: "TestString" }; | |
211 | |
212 // create an object | |
213 var message = pb.helloworld.HelloRequest.create(payload); | |
214 | |
215 // serialize object to buffer | |
216 var buffer = pb.helloworld.HelloRequest.encode(message).finish(); | |
217 | |
218 var n = buffer.length; | |
219 | |
220 var frame = new Uint8Array(5 + buffer.length); | |
221 | |
222 frame[0] = 0; // 'compressed' flag | |
223 frame[1] = (n & 0xFF000000) >>> 24; // length: uint32 in network order | |
224 frame[2] = (n & 0x00FF0000) >>> 16; | |
225 frame[3] = (n & 0x0000FF00) >>> 8; | |
226 frame[4] = (n & 0x000000FF) >>> 0; | |
227 | |
228 frame.set(buffer, 5); | |
229 | |
230 return frame; | |
231 } | |
232 | |
233 var frame = set_buffer(pb); | |
234 </example> | |
235 </para> | |
236 | |
237 <para> | |
238 To ensure it works, we execute the code using node: | |
239 | |
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 | |
249 You can see thet this got us a properly encoded <literal>gRPC</literal> frame. | |
250 Now let's run it with njs: | |
251 | |
252 <example> | |
253 $ njs ./code.js | |
254 Thrown: | |
255 Error: Cannot find module "./static.js" | |
256 at require (native) | |
257 at main (native) | |
258 </example> | |
259 </para> | |
260 | |
261 <para> | |
262 Modules are not supported, so we've received an exception. | |
263 To overcome this issue, let's use <literal>browserify</literal> | |
264 or other similar tool. | |
265 </para> | |
266 | |
267 <para> | |
268 An attempt to process our existing <literal>code.js</literal> file will result | |
269 in a bunch of JS code that is supposed to run in a browser, i.e. immediately | |
270 upon loading. | |
271 This isn't something we actually want. | |
272 Instead, we want to have an exported function that | |
273 can be referenced from the nginx configuration. | |
274 This requires some wrapper code. | |
275 | |
276 <note> | |
277 In this guide, we use njs cli in all examples for the sake of simplicity. | |
278 In real life, you will be using nginx njs module to run your code. | |
279 </note> | |
280 | |
281 </para> | |
282 | |
283 <para> | |
284 The <literal>load.js</literal> file contains the library-loading code that | |
285 stores its handle in the global namespace: | |
286 <example> | |
287 global.hello = require('./static.js'); | |
288 </example> | |
289 This code will be replaced with merged content. | |
290 Our code will be using the "<literal>global.hello</literal>" handle to access | |
291 the library. | |
292 </para> | |
293 | |
294 <para> | |
295 Next, we process it with browserify to get all dependencies into a single file: | |
296 <example> | |
297 $ npx browserify load.js -o bundle.js -d | |
298 </example> | |
299 | |
300 The result is huge file that contains all our dependencies: | |
301 | |
302 <example> | |
303 (function(){function...... | |
304 ... | |
305 ... | |
306 },{"protobufjs/minimal":9}]},{},[1]) | |
307 //# sourceMappingURL.............. | |
308 </example> | |
309 | |
310 To get final "<literal>njs_bundle.js</literal>" file we concatenate | |
311 "<literal>bundle.js</literal>" and the following code: | |
312 | |
313 <example> | |
314 // Example usage of protobuf library: prepare a buffer to send | |
315 function set_buffer(pb) | |
316 { | |
317 // set fields of gRPC payload | |
318 var payload = { name: "TestString" }; | |
319 | |
320 // create an object | |
321 var message = pb.helloworld.HelloRequest.create(payload); | |
322 | |
323 // serialize object to buffer | |
324 var buffer = pb.helloworld.HelloRequest.encode(message).finish(); | |
325 | |
326 var n = buffer.length; | |
327 | |
328 var frame = new Uint8Array(5 + buffer.length); | |
329 | |
330 frame[0] = 0; // 'compressed' flag | |
331 frame[1] = (n & 0xFF000000) >>> 24; // length: uint32 in network order | |
332 frame[2] = (n & 0x00FF0000) >>> 16; | |
333 frame[3] = (n & 0x0000FF00) >>> 8; | |
334 frame[4] = (n & 0x000000FF) >>> 0; | |
335 | |
336 frame.set(buffer, 5); | |
337 | |
338 return frame; | |
339 } | |
340 | |
341 // functions to be called from outside | |
342 function setbuf() | |
343 { | |
344 return set_buffer(global.hello); | |
345 } | |
346 | |
347 // call the code | |
348 var frame = setbuf(); | |
349 console.log(frame); | |
350 </example> | |
351 | |
352 Let's run the file using node to make sure things still work: | |
353 <example> | |
354 $ node ./njs_bundle.js | |
355 Uint8Array [ | |
356 0, 0, 0, 0, 12, 10, | |
357 10, 84, 101, 115, 116, 83, | |
358 116, 114, 105, 110, 103 | |
359 ] | |
360 </example> | |
361 | |
362 Now let's proceed further with njs: | |
363 | |
364 <example> | |
365 $ /njs ./njs_bundle.js | |
366 Uint8Array [0,0,0,0,12,10,10,84,101,115,116,83,116,114,105,110,103] | |
367 </example> | |
368 | |
369 The last thing will be to use njs-specific API to convert | |
370 array into byte string, so it could be usable by nginx module. | |
371 We can add the following snippet before the last line: | |
372 <example> | |
373 if (global.njs) { | |
374 return String.bytesFrom(frame) | |
375 } | |
376 </example> | |
377 | |
378 Finally, we got it working: | |
379 | |
380 <example> | |
381 $ njs ./njs_bundle.js |hexdump -C | |
382 00000000 00 00 00 00 0c 0a 0a 54 65 73 74 53 74 72 69 6e |.......TestStrin| | |
383 00000010 67 0a |g.| | |
384 00000012 | |
385 </example> | |
386 This is the intended result. | |
387 Response parsing can be implemented similarly: | |
388 <example> | |
389 function parse_msg(pb, msg) | |
390 { | |
391 // convert byte string into integer array | |
392 var bytes = msg.split('').map(v=>v.charCodeAt(0)); | |
393 | |
394 if (bytes.length < 5) { | |
395 throw 'message too short'; | |
396 } | |
397 | |
398 // first 5 bytes is gRPC frame (compression + length) | |
399 var head = bytes.splice(0, 5); | |
400 | |
401 // ensure we have proper message length | |
402 var len = (head[1] << 24) | |
403 + (head[2] << 16) | |
404 + (head[3] << 8) | |
405 + head[4]; | |
406 | |
407 if (len != bytes.length) { | |
408 throw 'header length mismatch'; | |
409 } | |
410 | |
411 // invoke protobufjs to decode message | |
412 var response = pb.helloworld.HelloReply.decode(bytes); | |
413 | |
414 console.log('Reply is:' + response.message); | |
415 </example> | |
416 </para> | |
417 | |
418 </section> | |
419 | |
420 <section id="dnspacket" name="DNS-packet"> | |
421 | |
422 <para> | |
423 This example uses a library for generation and parsing of DNS packets. | |
424 This a case worth considering because the library and its dependencies | |
425 use modern language constructions not yet supported by njs. | |
426 In turn, this requires from us an extra step: transpiling the source code. | |
427 </para> | |
428 | |
429 <para> | |
430 Additional node packages are needed: | |
431 <example> | |
432 $ npm install @babel/core @babel/cli @babel/preset-env babel-loader | |
433 $ npm install webpack webpack-cli | |
434 $ npm install buffer | |
435 $ npm install dns-packet | |
436 </example> | |
437 | |
438 The configuration file, webpack.config.js: | |
439 <example> | |
440 const path = require('path'); | |
441 | |
442 module.exports = { | |
443 entry: './load.js', | |
444 mode: 'production', | |
445 output: { | |
446 filename: 'wp_out.js', | |
447 path: path.resolve(__dirname, 'dist'), | |
448 }, | |
449 optimization: { | |
450 minimize: false | |
451 }, | |
452 node: { | |
453 global: true, | |
454 }, | |
455 module : { | |
456 rules: [{ | |
457 test: /\.m?js$$/, | |
458 exclude: /(bower_components)/, | |
459 use: { | |
460 loader: 'babel-loader', | |
461 options: { | |
462 presets: ['@babel/preset-env'] | |
463 } | |
464 } | |
465 }] | |
466 } | |
467 }; | |
468 </example> | |
469 Note we are using "<literal>production</literal>" mode. | |
470 In this mode webpack does not use "<literal>eval</literal>" construction | |
471 not supported by njs. | |
472 | |
473 The referenced <literal>load.js</literal> file is our entry point: | |
474 <example> | |
475 global.dns = require('dns-packet') | |
476 global.Buffer = require('buffer/').Buffer | |
477 </example> | |
478 | |
479 We start the same way, by producing a single file for the libraries: | |
480 | |
481 <example> | |
482 $ npx browserify load.js -o bundle.js -d | |
483 </example> | |
484 | |
485 Next, we process the file with webpack, which itself invokes babel: | |
486 | |
487 <example> | |
488 $ npx webpack --config webpack.config.js | |
489 </example> | |
490 | |
491 This command produces the <literal>dist/wp_out.js</literal> file, which is a | |
492 transpiled version of <literal>bundle.js</literal>. | |
493 | |
494 We need to concatenate it with <literal>code.js</literal> | |
495 that stores our code: | |
496 | |
497 <example> | |
498 function set_buffer(dnsPacket) | |
499 { | |
500 // create DNS packet bytes | |
501 var buf = dnsPacket.encode({ | |
502 type: 'query', | |
503 id: 1, | |
504 flags: dnsPacket.RECURSION_DESIRED, | |
505 questions: [{ | |
506 type: 'A', | |
507 name: 'google.com' | |
508 }] | |
509 }) | |
510 | |
511 return buf; | |
512 } | |
513 </example> | |
514 | |
515 Note that in this example generated code is not wrapped into function and we | |
516 do not need to call it explicitely. | |
517 The result is in the "<literal>dist</literal>" directoty: | |
518 | |
519 <example> | |
520 $ cat dist/wp_out.js code.js > njs_dns_bundle.js | |
521 </example> | |
522 | |
523 Let's call our code at the end of a file: | |
524 <example> | |
525 var b = setbuf(1); | |
526 console.log(b); | |
527 </example> | |
528 | |
529 And execute it using node: | |
530 | |
531 <example> | |
532 $ node ./njs_dns_bundle_final.js | |
533 Buffer [Uint8Array] [ | |
534 0, 1, 1, 0, 0, 1, 0, 0, | |
535 0, 0, 0, 0, 6, 103, 111, 111, | |
536 103, 108, 101, 3, 99, 111, 109, 0, | |
537 0, 1, 0, 1 | |
538 ] | |
539 </example> | |
540 | |
541 Make sure this works as expected, and then run it with njs: | |
542 <example> | |
543 $ njs ./njs_dns_bundle_final.js | |
544 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] | |
545 </example> | |
546 | |
547 </para> | |
548 | |
549 <para> | |
550 The response can be parsed as follows: | |
551 <example> | |
552 function parse_response(buf) | |
553 { | |
554 var bytes = buf.split('').map(v=>v.charCodeAt(0)); | |
555 | |
556 var b = global.Buffer.from(bytes); | |
557 | |
558 var packet = dnsPacket.decode(b); | |
559 | |
560 var resolved_name = packet.answers[0].name; | |
561 | |
562 // expected name is 'google.com', according to our request above | |
563 } | |
564 </example> | |
565 | |
566 </para> | |
567 | |
568 </section> | |
569 | |
570 </article> |