njsを使ったnodeモジュールの使い方

Environment
Protobufjs
DNS-packet

多くの場合、開発者はサードパーティのコードを使うことを望みます。これは通常何らかのライブラリとして利用できます。JavaScriptの世界では、モジュールの概念は比較的新しく、最近まで標準がありませんでした。多くのプラットフォーム(ブラウザ)はまだモジュールをサポートしません。これはコードの再利用を難しくします。この記事では、njsでNode.jsコードを再利用する方法を説明します。

この記事での例はnjs 0.3.8で登場した機能を使います。

サードパーティのコードがnjsに追加された時に発生する可能性のある多くの問題があります:

良いニュースは、そのような問題は新しいものでは無く、njsに固有のものではないということです。JavaScriptの開発者は、非常に異なるプロパティを持つ複数の異なるプラットフォームをサポートしようとする時に、それらに日々直面します。上述の問題を解決するために設計された手段があります。

このガイドでは、2つの比較的大規模な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ファイルを使います。私たちの目標は、2つのメッセージを作成することです: HelloRequestHelloResponse。njsはセキュリティ上の理由から新しい関数の動的な追加をサポートしないため、動的なクラスの生成の代わりにprotobufjsのstaticモードを使います。

次に、ライブラリがインストールされ、メッセージのマーシャルを実装する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を使って全ての依存関係を1つのファイルにまとめます:

$ 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パケット

この例は、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

同じ方法で、ライブラリ用の1つのファイルを生成します:

$ npx browserify load.js -o bundle.js -d

次に、それ自体がbabelを呼び出すwebpackを使ってファイルを処理します:

$ 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
}

TOP
inserted by FC2 system