njs와 함께 node 모듈 사용
대부분, 개발자는 일반적으로 일종의 라이브러리로 사용할 수 있는 타사 코드를 사용하려고 합니다. JavaScript 영역에서 모듈의 개념은 상대적으로 새롭고 따라서 최근까지 표준이 없었습니다. 많은 플랫폼(브라우저)이 아직 모듈을 지원하지 않아서 코드를 재사용하기가 더 어렵습니다. 이 문서에서는 njs에서 Node.js 코드를 재사용하는 방법을 설명합니다.
이 문서의 예는 njs 0.3.8에 표시된 기능을 사용합니다
타사 코드가 njs에 추가된 경우 발생할 수 있는 문제는 다음과 같습니다.
- 여러 파일이 서로 참조하고 다른 파일의 종속성을 참조함
- 플랫폼별 API
- 최신 표준 언어 구성
그러한 문제가 njs에 새로운 또는 njs에만 해당하는 문제가 아니라는 점은 희소식입니다. JavaScript 개발자가 매우 다른 속성을 지닌 다른 여러 플랫폼을 지원하려 할 때 매일 부딪히는 문제입니다. 위에 언급한 문제를 해결하도록 설계된 방법이 있습니다.
- 여러 파일이 서로 참조하고 다른 파일의 종속성을 참조함
이 문제는 상호 의존하는 모든 코드를 단일 파일로 병합하여 해결할 수 있습니다. browserify 또는 webpack과 같은 도구가 전체 프로젝트를 받아서 코드와 모든 종속성을 포함하는 단일 파일을 생성합니다.
- 플랫폼별 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의 정적 모드를 사용합니다.
다음으로, 라이브러리가 설치되고 메시지 마샬링을 구현하는 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 ./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 ./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-패킷
이 예에서는 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
라이브러리에 대한 단일 파일을 생성하여 동일한 방식으로 시작합니다.
$ npx browserify load.js -o bundle.js -d
다음으로 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 ./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
}