njsを使ったnodeモジュールの使い方
Environment Protobufjs DNS-packet |
多くの場合、開発者はサードパーティのコードを使うことを望みます。これは通常何らかのライブラリとして利用できます。JavaScriptの世界では、モジュールの概念は比較的新しく、最近まで標準がありませんでした。多くのプラットフォーム(ブラウザ)はまだモジュールをサポートしません。これはコードの再利用を難しくします。この記事では、njsでNode.jsコードを再利用する方法を説明します。
この記事での例はnjs 0.3.8で登場した機能を使います。
サードパーティのコードがnjsに追加された時に発生する可能性のある多くの問題があります:
- 相互に参照する複数のファイルとその依存関係
- プラットフォーム固有のAPI
- 現在の標準言語構成
良いニュースは、そのような問題は新しいものでは無く、njsに固有のものではないということです。JavaScriptの開発者は、非常に異なるプロパティを持つ複数の異なるプラットフォームをサポートしようとする時に、それらに日々直面します。上述の問題を解決するために設計された手段があります。
-
お互いに参照する複数のファイルと、それらの依存関係
これは相互依存する全てのコードを1つのファイルにマージすることで解決します。browserify あるいは webpack のようなツールはプロジェクト全体を受け入れ、コードと全ての依存関係を含む1つのファイルを生成します。
-
プラットフォーム固有のAPI
このようなAPIを実装する複数のライブラリを、プラットフォームに依存しない方法で使えます(ただしパフォーマンスは低下します)。特定の機能はpolyfillのやり方を使って実装することもできます。
-
現在の標準言語構成
このようなコードはトランスパイルすることができます: これは、古い標準に従って新しい言語機能を置き換える多数の変換を実行することを意味します。例えば、 babel プロジェクトはこの目的に使えます。
このガイドでは、2つの比較的大規模なnpmがホストするライブラリを使います:
- protobufjs — gRPCプロトコルで使われるprotobufメッセージを作成および解析するためのライブラリ
- dns-packet — DNSプロトコルのパケットを処理するためのライブラリ
環境
このドキュメントは主に一般的なやり方を採用しており、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つのメッセージを作成することです: HelloRequest
と HelloResponse
。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 }