在 njs 中使用 node 模組
環境 Protobufjs DNS-packet |
開發人員通常會想使用第三方程式碼,通常以某種形式的函式庫提供。在 JavaScript 世界中,模組的概念相對較新,因此直到最近才出現標準。許多平台(瀏覽器)仍然不支援模組,這使得程式碼重複使用變得更加困難。本文介紹在 njs 中重複使用 Node.js 程式碼的方法。
本文中的範例使用了 njs 0.3.8 版本中出現的功能
當將第三方程式碼新增到 njs 時,可能會出現許多問題
- 彼此參照的多個檔案及其相依性
- 平台特定的 API
- 現代標準語言結構
好消息是,這些問題並非新的或 njs 特有的問題。JavaScript 開發人員在嘗試支援多個具有非常不同屬性的分散平台時,每天都會面臨這些問題。有一些工具旨在解決上述問題。
- 彼此參照的多個檔案及其相依性
這可以透過將所有相互依賴的程式碼合併到單一檔案中來解決。諸如 browserify 或 webpack 之類的工具會接受整個專案,並產生一個包含您的程式碼和所有相依性的單一檔案。
- 平台特定的 API
您可以使用多個以平台無關方式實作此類 API 的函式庫(但會犧牲效能)。也可以使用 polyfill 方法來實作特定功能。
- 現代標準語言結構
此類程式碼可以進行轉譯:這表示執行許多轉換,根據較舊的標準重寫較新的語言功能。例如, babel 專案可以用於此目的。
在本指南中,我們將使用兩個相對較大的 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 檔案。我們的目標是建立兩個訊息:HelloRequest
和 HelloResponse
。我們將使用 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 }