在 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 <some.email@example.com> (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
}