在 njs 中使用 Node 模块
环境 Protobufjs DNS-packet |
通常,开发者希望使用第三方代码,这些代码通常以某种库的形式提供。在 JavaScript 世界中,模块的概念相对较新,因此直到最近才出现标准。许多平台(浏览器)仍然不支持模块,这使得代码重用变得更加困难。本文介绍了如何在 njs 中重用 Node.js 代码。
本文中的示例使用了出现在 njs 0.3.8 中的功能。
将第三方代码添加到 njs 时,可能会出现一些问题。
- 相互引用及其依赖项的多个文件
- 特定于平台的 API
- 现代标准语言结构
好消息是,这些问题并不是什么新鲜事,也不是 njs 特有的。JavaScript 开发人员在尝试支持具有不同属性的多个不同平台时,每天都会遇到这些问题。有一些工具旨在解决上述问题。
- 相互引用及其依赖项的多个文件
这可以通过将所有相互依赖的代码合并到单个文件中来解决。像 browserify 或 webpack 这样的工具可以接受整个项目并生成一个包含您的代码和所有依赖项的单个文件。
- 特定于平台的 API
您可以使用多个库以平台无关的方式实现此类 API(尽管会牺牲性能)。还可以使用 polyfill 方法实现特定功能。
- 现代标准语言结构
此类代码可以进行转译:这意味着执行一系列转换,根据旧标准重写较新的语言特性。例如,babel 项目可以用于此目的。
在本指南中,我们将使用两个相对较大的 npm 托管库。
- protobufjs — 用于创建和解析 gRPC 协议使用的 protobuf 消息的库。
- 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 <[email protected]> (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
接口定义提供了一个解析器,并为消息解析和生成提供了一个代码生成器。
在本例中,我们将使用 gRPC 示例中的 helloworld.proto 文件。我们的目标是创建两个消息: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]
最后一步将是使用 njs 特定的 API 将数组转换为字节字符串,以便 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-packet
此示例使用一个用于生成和解析 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 不使用 njs 不支持的“eval
”结构。引用的 load.js
文件是我们的入口点。
global.dns = require('dns-packet') global.Buffer = require('buffer/').Buffer
我们从生成库的单个文件开始,方式相同。
$ npx browserify load.js -o bundle.js -d
接下来,我们使用 webpack 处理该文件,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 }