在 njs 中使用 Node 模块

环境
Protobufjs
DNS-packet

通常,开发者希望使用第三方代码,这些代码通常以某种库的形式提供。在 JavaScript 世界中,模块的概念相对较新,因此直到最近才出现标准。许多平台(浏览器)仍然不支持模块,这使得代码重用变得更加困难。本文介绍了如何在 njs 中重用 Node.js 代码。

本文中的示例使用了出现在 njs 0.3.8 中的功能。

将第三方代码添加到 njs 时,可能会出现一些问题。

好消息是,这些问题并不是什么新鲜事,也不是 njs 特有的。JavaScript 开发人员在尝试支持具有不同属性的多个不同平台时,每天都会遇到这些问题。有一些工具旨在解决上述问题。

在本指南中,我们将使用两个相对较大的 npm 托管库。

环境

本文档主要采用通用方法,并避免提供有关 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 文件。我们的目标是创建两个消息:HelloRequestHelloResponse。我们将使用 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
}