njsでのNodeモジュールの使用

環境
Protobufjs
DNS-packet

開発者はしばしば、サードパーティ製のコード(通常はライブラリとして提供される)を使用したいと考えます。JavaScriptの世界では、モジュールの概念は比較的新しいものであり、最近までは標準がありませんでした。多くのプラットフォーム(ブラウザ)はまだモジュールをサポートしておらず、コードの再利用が困難になっています。この記事では、Node.jsコードをnjsで再利用する方法について説明します。

この記事の例では、njs 0.3.8以降に追加された機能を使用しています。

サードパーティのコードをnjsに追加すると、いくつかの問題が発生する可能性があります。

良いニュースは、このような問題はnjs特有のものでも、新しいものでもないということです。JavaScript開発者は、非常に異なる特性を持つ複数の異なるプラットフォームをサポートしようとする際に、毎日このような問題に直面しています。上記の課題を解決するために設計されたツールがあります。

このガイドでは、npmでホストされている比較的大きな2つのライブラリを使用します。

環境

このドキュメントでは、主に一般的なアプローチを採用し、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ファイルを使用します。目標は、`HelloRequest`と`HelloResponse`の2つのメッセージを作成することです。セキュリティ上の理由からnjsは動的に新しい関数を追加できないため、protobufjsの静的モードを使用します。

次に、ライブラリをインストールし、プロトコル定義からメッセージのマーシャリングを実装する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]

最後に、nginxモジュールで使用できるように、配列をバイト文字列に変換するためにnjs固有のAPIを使用します。`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

このコマンドは、`bundle.js`のトランスパイルされたバージョンである`dist/wp_out.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
}