njsでのNodeモジュールの使用
環境 Protobufjs DNS-packet |
開発者はしばしば、サードパーティ製のコード(通常はライブラリとして提供される)を使用したいと考えます。JavaScriptの世界では、モジュールの概念は比較的新しいものであり、最近までは標準がありませんでした。多くのプラットフォーム(ブラウザ)はまだモジュールをサポートしておらず、コードの再利用が困難になっています。この記事では、Node.jsコードをnjsで再利用する方法について説明します。
この記事の例では、njs 0.3.8以降に追加された機能を使用しています。
サードパーティのコードをnjsに追加すると、いくつかの問題が発生する可能性があります。
- 相互参照する複数のファイルとその依存関係
- プラットフォーム固有のAPI
- 最新の標準言語構文
良いニュースは、このような問題はnjs特有のものでも、新しいものでもないということです。JavaScript開発者は、非常に異なる特性を持つ複数の異なるプラットフォームをサポートしようとする際に、毎日このような問題に直面しています。上記の課題を解決するために設計されたツールがあります。
- 相互参照する複数のファイルとその依存関係
これは、すべての相互依存コードを単一のファイルにマージすることで解決できます。browserifyやwebpackなどのツールは、プロジェクト全体を受け入れ、コードとすべての依存関係を含む単一のファイルを生成します。
- プラットフォーム固有のAPI
プラットフォーム非依存の方法で、そのようなAPIを実装する複数のライブラリを使用できます(ただし、パフォーマンスの低下を伴います)。特定の機能は、polyfillアプローチを使用して実装することもできます。
- 最新の標準言語構文
そのようなコードはトランスパイルできます。これは、より新しい言語機能を古い標準に従って書き換えるいくつかの変換を実行することを意味します。たとえば、babelプロジェクトをこの目的で使用できます。
このガイドでは、npmでホストされている比較的大きな2つのライブラリを使用します。
- 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ファイルを使用します。目標は、`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 }