在 NJS 中使用节点模块

Introduction

通常,开发人员想使用第三方代码,通常以某种形式的库形式提供。在 Javascript 世界中,模块的概念相对较新,因此直到最近才出现标准。许多平台(浏览器)仍然不支持模块,这使得代码重用变得更加困难。 njs 还不支持模块。本文以Node.js生态系统为例,介绍了克服此限制的方法。

Note

本文中的示例使用的是njs 0.3.8中出现的功能

将第三方代码添加到 njs 时,可能会出现许多问题:

  • 相互引用的多个文件及其依赖性

  • Platform-specific APIs

  • 现代标准语言结构

好消息是,这些问题不是 njs 的新问题或特定问题。当试图支持具有完全不同特性的多个不同平台时,JavaScript 开发人员每天都会面对它们。有些工具旨在解决上述问题。

  • 相互引用的多个文件及其依赖性

这可以通过将所有相互依赖的代码合并到一个文件中来解决。 browserifywebpack之类的工具可以接受整个项目,并生成一个包含您的代码和所有依赖项的文件。

  • Platform-specific APIs

您可以使用多个以平台无关的方式实现此类 API 的库(但是以性能为代价)。还可以使用polyfill方法实现特定功能。

  • 现代标准语言结构

可以编译这样的代码:这意味着执行许多转换,以根据较旧的标准重写较新的语言功能。例如,babel项目可用于此目的。

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

  • protobufjs-一个用于创建和解析gRPC协议使用的 protobuf 消息的库。

  • dns-packet-用于处理 DNS 协议数据包的库。

Environment

Note

本文档主要采用通用方法以及有关 Node.js 和快速 Developing 的 JavaScript 生态系统的 AVOIDS 特定的最佳实践建议。在执行此处建议的步骤之前,请确保先查阅相应的包装手册。

首先(假设 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 的static模式,而不是动态生成类,因为出于安全考虑,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 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 代码在浏览器中运行,即在加载后立即运行。这不是我们 true 想要的东西。相反,我们希望有一个可以从 nginx 配置中引用的导出函数。这需要一些包装器代码。

Note

在本指南中,为简单起见,我们在所有示例中均使用 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 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
]

现在让我们 continue 使用 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 模块使用。我们可以在最后一行之前添加以下代码段:

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 尚不支持的现代语言构造。反过来,这要求我们采取额外的步骤:编译源代码。

需要其他节点程序包:

$ 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 = setbuf(1);
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
}