Appearance
在 njs 中使用 Node 模块
提示
来自deepseek解释
原文链接:https://nginx.org/en/docs/njs/node_modules.html
开发者经常需要使用第三方代码,通常以某种库的形式提供。在 JavaScript 世界中,模块的概念相对较新,直到最近才出现标准。许多平台(如浏览器)仍然不支持模块,这使得代码复用更加困难。本文介绍在 njs 中复用 Node.js 代码的方法。
将第三方代码引入 njs 时可能出现的问题包括:
- 多个文件相互引用及其依赖关系
- 平台特定的 API
- 现代标准语言结构
好消息是,这些问题并非 njs 特有。JavaScript 开发者在尝试支持具有不同特性的多个平台时,每天都会面临这些问题。有一些工具可以解决上述问题:
- 多个文件相互引用及其依赖关系:可以通过将所有相互依赖的代码合并到一个文件中来解决。像 browserify 或 webpack 这样的工具可以接受整个项目,并生成一个包含你的代码和所有依赖的单个文件。
- 平台特定的 API:你可以使用多个以平台无关方式实现此类 API 的库(尽管会牺牲性能)。特定功能也可以使用 polyfill 方法来实现。
- 现代标准语言结构:这类代码可以进行转译(transpiled),即执行一系列转换,将较新的语言特性重写为符合较旧标准的形式。例如,可以使用 babel 项目来实现此目的。
在本指南中,我们将使用两个相对较大的 npm 托管库:
- protobufjs —— 用于创建和解析 protobuf 消息的库,gRPC 协议使用该库
- dns-packet —— 用于处理 DNS 协议包的库
环境准备
本文档主要采用通用方法,避免涉及有关 Node.js 和 JavaScript 的具体最佳实践建议。在按照此处建议的步骤操作之前,请务必查阅相应软件包的手册。
首先(假设 Node.js 已安装并可运行),创建一个空项目并安装一些依赖项;以下命令假设我们在工作目录中:
bash
$ mkdir my_project && cd my_project
$ npx license choose_your_license_here > LICENSE
$ npx gitignore node
$ cat > package.json << EOF
{
"name": "my_project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://example.com"
},
"license": "some_license_here",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
EOF
$ npm init -y
$ npm install browserifyProtobufjs 示例
该库提供了 .proto 接口定义文件的解析器,以及用于消息解析和生成代码的生成器。
在本示例中,我们将使用 gRPC 示例中的 helloworld.proto 文件。我们的目标是创建两条消息:HelloRequest 和 HelloResponse。
我们将使用 protobufjs 的静态(static)模式,而不是动态生成类,因为出于安全考虑,njs 不支持动态添加新函数。
接下来,安装库并从协议定义生成实现消息封送处理的 JavaScript 代码:
bash
$ npm install protobufjs
$ npx pbjs -t static-module helloworld.proto > static.js这样,static.js 文件就成了我们的新依赖,存储了实现消息处理所需的所有代码。
set_buffer() 函数包含使用该库创建包含序列化 HelloRequest 消息的缓冲区的代码。该代码位于 code.js 文件中:
js
var pb = require('./static.js');
// protobuf 库使用示例:准备要发送的缓冲区
function set_buffer(pb) {
// 设置 gRPC 载荷的字段
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);为了确保其正常工作,我们使用 Node.js 执行该代码:
bash
$ node ./code.js
Uint8Array [ 0, 0, 0, 0, 12, 10, 10, 84, 101, 115, 116, 83, 116, 114, 105, 110, 103 ]可以看到,我们得到了一个正确编码的 gRPC 帧。
现在用 njs 运行它:
bash
$ 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 文件包含库加载代码,将其句柄存储在全局命名空间中:
js
global.hello = require('./static.js');此代码将被替换为合并后的内容。我们的代码将使用 global.hello 句柄来访问库。
接下来,我们使用 browserify 处理它以将所有依赖项合并到一个文件中:
bash
$ npx browserify load.js -o bundle.js -d结果是一个包含所有依赖项的大文件。
为了得到最终的 njs_bundle.js 文件,我们将 bundle.js 与以下代码连接起来:
js
// protobuf 库使用示例:准备要发送的缓冲区
function set_buffer(pb) {
// 设置 gRPC 载荷的字段
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);使用 Node.js 运行该文件以确保一切正常:
bash
$ node ./njs_bundle.js
Uint8Array [ 0, 0, 0, 0, 12, 10, 10, 84, 101, 115, 116, 83, 116, 114, 105, 110, 103 ]现在继续使用 njs:
bash
$ 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; 行之前添加以下代码片段:
js
if (global.njs) {
return String.bytesFrom(frame)
}最终,我们成功了:
bash
$ 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这就是我们期望的结果。
响应解析可以类似地实现:
js
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);
}DNS-packet 示例
本例使用一个用于生成和解析 DNS 数据包的库。这是一个值得考虑的案例,因为该库及其依赖项使用了 njs 尚不支持的现代语言结构。因此,我们需要额外执行一个步骤:转译源代码。
需要额外的 Node 包:
bash
$ 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:
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 文件是我们的入口点:
js
global.dns = require('dns-packet')
global.Buffer = require('buffer/').Buffer我们以同样的方式开始,为库生成单个文件:
bash
$ npx browserify load.js -o bundle.js -d接下来,我们使用 webpack 处理该文件,webpack 本身会调用 babel:
bash
$ npx webpack --config webpack.config.js此命令生成 dist/wp_out.js 文件,这是 bundle.js 的转译版本。我们需要将其与存储我们代码的 code.js 连接起来:
js
function set_buffer(dnsPacket) {
// 创建 DNS 数据包字节
var buf = dnsPacket.encode({
type: 'query',
id: 1,
flags: dnsPacket.RECURSION_DESIRED,
questions: [{
type: 'A',
name: 'google.com'
}]
})
return buf;
}注意,在此示例中,生成的代码没有包装在函数中,我们也不需要显式调用它。
结果在 dist 目录中:
bash
$ cat dist/wp_out.js code.js > njs_dns_bundle.js让我们在文件末尾调用代码:
js
var b = set_buffer(global.dns);
console.log(b);使用 Node.js 执行:
bash
$ 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 运行:
bash
$ 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]响应可以按如下方式解析:
js
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'
}