njs 模組介紹
njs(Nginx JavaScript)是 Nginx 官方開發的 JavaScript 子集實作,專門設計用於擴展 Nginx 的功能。它允許開發者使用 JavaScript 語法來處理 HTTP 請求、操作變數、執行子請求以及實現複雜的業務邏輯。
njs 與傳統 JavaScript 的差異
njs 並非完整的 JavaScript 引擎(如 V8 或 SpiderMonkey),而是針對 Nginx 環境最佳化的輕量級實作:
- 記憶體效率:每個請求使用獨立的記憶體空間,請求結束後自動釋放
- 非阻塞設計:原生支援 Nginx 的事件驅動架構
- 安全性:沒有檔案系統存取和網路 I/O 的直接 API
- ECMAScript 支援:支援大部分 ES6+ 語法特性
njs 的主要用途
- 請求與回應的動態修改
- 複雜的認證與授權邏輯
- API Gateway 功能實作
- 請求路由與負載均衡決策
- 日誌格式化與資料處理
安裝與設定
前置需求
在 Ubuntu 22.04 上安裝 njs 模組前,請確保系統已更新:
1
| sudo apt update && sudo apt upgrade -y
|
方法一:從官方 Nginx 套件庫安裝
首先,新增 Nginx 官方套件庫:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 安裝必要工具
sudo apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring
# 匯入官方 GPG 金鑰
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
# 驗證金鑰
gpg --dry-run --quiet --no-keyring --import --import-options import-show \
/usr/share/keyrings/nginx-archive-keyring.gpg
# 新增套件庫
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list
# 設定套件庫優先權
echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
| sudo tee /etc/apt/preferences.d/99nginx
|
安裝 Nginx 和 njs 模組:
1
2
| sudo apt update
sudo apt install -y nginx nginx-module-njs
|
方法二:使用 Ubuntu 預設套件庫
Ubuntu 22.04 的預設套件庫也提供 njs 模組:
1
| sudo apt install -y nginx libnginx-mod-http-js
|
啟用 njs 模組
編輯 Nginx 主設定檔 /etc/nginx/nginx.conf,在最上方新增模組載入指令:
1
2
3
4
5
6
| load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;
user nginx;
worker_processes auto;
# ... 其餘設定
|
驗證設定並重新載入 Nginx:
1
2
| sudo nginx -t
sudo systemctl reload nginx
|
驗證安裝
確認 njs 模組已正確載入:
1
| nginx -V 2>&1 | grep -o 'njs'
|
檢查 njs 版本:
基本語法與 API
njs 檔案結構
njs 腳本通常放置於 /etc/nginx/njs/ 目錄下:
1
| sudo mkdir -p /etc/nginx/njs
|
基本的 njs 檔案結構:
1
2
3
4
5
6
7
8
9
10
11
12
| // /etc/nginx/njs/http.js
function hello(r) {
r.return(200, "Hello from njs!\n");
}
function greet(r) {
let name = r.args.name || "World";
r.return(200, `Hello, ${name}!\n`);
}
export default { hello, greet };
|
Nginx 設定整合
在 Nginx 設定中引用 njs 腳本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| http {
js_path "/etc/nginx/njs/";
js_import http.js;
server {
listen 80;
server_name example.com;
location /hello {
js_content http.hello;
}
location /greet {
js_content http.greet;
}
}
}
|
核心物件與屬性
njs 提供的 HTTP 請求物件 r 包含以下重要屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| function requestInfo(r) {
// 請求資訊
let info = {
method: r.method, // HTTP 方法 (GET, POST, etc.)
uri: r.uri, // 請求 URI
args: r.args, // 查詢參數物件
headersIn: r.headersIn, // 請求標頭
headersOut: r.headersOut, // 回應標頭
httpVersion: r.httpVersion, // HTTP 版本
remoteAddress: r.remoteAddress, // 客戶端 IP
requestBody: r.requestBody, // 請求主體
variables: r.variables // Nginx 變數
};
r.return(200, JSON.stringify(info, null, 2));
}
export default { requestInfo };
|
變數存取
透過 r.variables 存取 Nginx 變數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function showVariables(r) {
let vars = {
host: r.variables.host,
request_uri: r.variables.request_uri,
remote_addr: r.variables.remote_addr,
server_name: r.variables.server_name,
server_port: r.variables.server_port,
scheme: r.variables.scheme,
request_method: r.variables.request_method
};
r.return(200, JSON.stringify(vars, null, 2));
}
export default { showVariables };
|
HTTP 請求處理
處理 GET 請求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // /etc/nginx/njs/api.js
function handleGet(r) {
// 取得查詢參數
let id = r.args.id;
let page = parseInt(r.args.page) || 1;
let limit = parseInt(r.args.limit) || 10;
if (!id) {
r.return(400, JSON.stringify({
error: "Missing required parameter: id"
}));
return;
}
let response = {
id: id,
page: page,
limit: limit,
timestamp: new Date().toISOString()
};
r.headersOut['Content-Type'] = 'application/json';
r.return(200, JSON.stringify(response));
}
export default { handleGet };
|
處理 POST 請求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| function handlePost(r) {
// 檢查 Content-Type
let contentType = r.headersIn['Content-Type'] || '';
if (!contentType.includes('application/json')) {
r.return(415, JSON.stringify({
error: "Unsupported Media Type. Expected application/json"
}));
return;
}
// 解析請求主體
let body;
try {
body = JSON.parse(r.requestBody);
} catch (e) {
r.return(400, JSON.stringify({
error: "Invalid JSON in request body"
}));
return;
}
// 驗證必要欄位
if (!body.username || !body.email) {
r.return(400, JSON.stringify({
error: "Missing required fields: username, email"
}));
return;
}
// 處理業務邏輯
let response = {
status: "created",
data: {
username: body.username,
email: body.email,
createdAt: new Date().toISOString()
}
};
r.headersOut['Content-Type'] = 'application/json';
r.return(201, JSON.stringify(response));
}
export default { handlePost };
|
對應的 Nginx 設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| server {
listen 80;
server_name api.example.com;
js_path "/etc/nginx/njs/";
js_import api.js;
location /api/resource {
js_content api.handleGet;
}
location /api/create {
js_content api.handlePost;
}
}
|
請求標頭操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| function modifyHeaders(r) {
// 讀取請求標頭
let userAgent = r.headersIn['User-Agent'];
let authorization = r.headersIn['Authorization'];
// 設定回應標頭
r.headersOut['X-Powered-By'] = 'njs';
r.headersOut['X-Request-ID'] = generateRequestId();
r.headersOut['Cache-Control'] = 'no-cache, no-store, must-revalidate';
r.return(200, "Headers processed");
}
function generateRequestId() {
return 'req-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}
export default { modifyHeaders };
|
使用 js_set 設定變數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 設定動態變數值
function setRequestId(r) {
return 'req-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
}
function setClientInfo(r) {
let info = {
ip: r.remoteAddress,
ua: r.headersIn['User-Agent'] || 'unknown'
};
return Buffer.from(JSON.stringify(info)).toString('base64');
}
export default { setRequestId, setClientInfo };
|
Nginx 設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| http {
js_path "/etc/nginx/njs/";
js_import utils from utils.js;
js_set $request_id utils.setRequestId;
js_set $client_info utils.setClientInfo;
server {
listen 80;
location / {
add_header X-Request-ID $request_id;
add_header X-Client-Info $client_info;
proxy_pass http://backend;
}
}
}
|
子請求與認證
執行子請求
njs 支援使用 r.subrequest() 發送內部子請求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| async function fetchUserData(r) {
try {
// 發送子請求到內部 API
let reply = await r.subrequest('/internal/user', {
method: 'GET',
args: 'id=' + r.args.userId
});
if (reply.status !== 200) {
r.return(reply.status, "User not found");
return;
}
let userData = JSON.parse(reply.responseBody);
r.headersOut['Content-Type'] = 'application/json';
r.return(200, JSON.stringify({
success: true,
user: userData
}));
} catch (e) {
r.return(500, "Internal server error: " + e.message);
}
}
export default { fetchUserData };
|
Nginx 設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| server {
listen 80;
js_path "/etc/nginx/njs/";
js_import auth from auth.js;
location /user {
js_content auth.fetchUserData;
}
# 內部位置,僅供子請求使用
location /internal/user {
internal;
proxy_pass http://user-service:8080/api/user;
}
}
|
JWT 認證實作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| // /etc/nginx/njs/jwt.js
function base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
let padding = str.length % 4;
if (padding) {
str += '='.repeat(4 - padding);
}
return Buffer.from(str, 'base64').toString();
}
function verifyJWT(r) {
let auth = r.headersIn['Authorization'];
if (!auth || !auth.startsWith('Bearer ')) {
return JSON.stringify({ valid: false, error: 'Missing token' });
}
let token = auth.slice(7);
let parts = token.split('.');
if (parts.length !== 3) {
return JSON.stringify({ valid: false, error: 'Invalid token format' });
}
try {
let header = JSON.parse(base64UrlDecode(parts[0]));
let payload = JSON.parse(base64UrlDecode(parts[1]));
// 檢查過期時間
let now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) {
return JSON.stringify({ valid: false, error: 'Token expired' });
}
// 檢查簽發時間
if (payload.iat && payload.iat > now) {
return JSON.stringify({ valid: false, error: 'Token not yet valid' });
}
return JSON.stringify({
valid: true,
payload: payload
});
} catch (e) {
return JSON.stringify({ valid: false, error: 'Token parse error' });
}
}
function authorize(r) {
let result = JSON.parse(verifyJWT(r));
if (!result.valid) {
r.return(401, JSON.stringify({ error: result.error }));
return;
}
// 設定使用者資訊到標頭,供後端使用
r.headersOut['X-User-ID'] = result.payload.sub || '';
r.headersOut['X-User-Role'] = result.payload.role || '';
return 'authorized';
}
export default { verifyJWT, authorize };
|
使用 auth_request 進行認證:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| server {
listen 80;
js_path "/etc/nginx/njs/";
js_import jwt from jwt.js;
js_set $jwt_status jwt.verifyJWT;
location /protected {
auth_request /auth;
auth_request_set $auth_status $upstream_status;
proxy_pass http://backend;
proxy_set_header X-Auth-Status $auth_status;
}
location = /auth {
internal;
js_content jwt.authorize;
}
}
|
API Key 認證
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // /etc/nginx/njs/apikey.js
// API 金鑰對照表(實際環境應從外部來源讀取)
const apiKeys = {
'sk-prod-abc123': { name: 'Production App', tier: 'premium', rateLimit: 10000 },
'sk-dev-xyz789': { name: 'Development App', tier: 'basic', rateLimit: 1000 }
};
function validateApiKey(r) {
let apiKey = r.headersIn['X-API-Key'] || r.args.api_key;
if (!apiKey) {
r.return(401, JSON.stringify({
error: 'API key is required',
hint: 'Provide X-API-Key header or api_key query parameter'
}));
return;
}
let keyInfo = apiKeys[apiKey];
if (!keyInfo) {
r.return(403, JSON.stringify({
error: 'Invalid API key'
}));
return;
}
// 設定回應標頭供日誌記錄
r.headersOut['X-API-Client'] = keyInfo.name;
r.headersOut['X-Rate-Limit'] = keyInfo.rateLimit.toString();
// 回傳空內容表示認證成功
r.return(200);
}
function getApiKeyInfo(r) {
let apiKey = r.headersIn['X-API-Key'] || r.args.api_key;
let keyInfo = apiKeys[apiKey];
if (keyInfo) {
return keyInfo.name;
}
return 'unknown';
}
export default { validateApiKey, getApiKeyInfo };
|
變數與共享資料
使用 Shared Dictionary
njs 支援透過 Nginx 的共享記憶體區域來儲存和共享資料:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| http {
js_path "/etc/nginx/njs/";
js_import cache from cache.js;
# 定義共享記憶體區域
js_shared_dict_zone zone=cache:10m timeout=60s evict;
js_shared_dict_zone zone=counters:1m;
server {
listen 80;
location /cache/set {
js_content cache.set;
}
location /cache/get {
js_content cache.get;
}
location /counter/increment {
js_content cache.increment;
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| // /etc/nginx/njs/cache.js
function set(r) {
let key = r.args.key;
let value = r.args.value;
if (!key || !value) {
r.return(400, "Missing key or value parameter");
return;
}
let cache = ngx.shared.cache;
// 設定快取值,預設 TTL 由 zone 設定決定
cache.set(key, value);
r.return(200, JSON.stringify({
status: "cached",
key: key,
value: value
}));
}
function get(r) {
let key = r.args.key;
if (!key) {
r.return(400, "Missing key parameter");
return;
}
let cache = ngx.shared.cache;
let value = cache.get(key);
if (value === undefined) {
r.return(404, JSON.stringify({
status: "not_found",
key: key
}));
return;
}
r.return(200, JSON.stringify({
status: "found",
key: key,
value: value
}));
}
function increment(r) {
let key = r.args.key || 'default';
let counters = ngx.shared.counters;
// 原子操作遞增計數器
let current = counters.incr(key, 1, 0);
r.return(200, JSON.stringify({
key: key,
count: current
}));
}
export default { set, get, increment };
|
請求間資料傳遞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // /etc/nginx/njs/context.js
function setContext(r) {
// 使用 r.variables 設定自訂變數
r.variables.custom_user_id = r.args.user_id || 'anonymous';
r.variables.custom_request_time = Date.now().toString();
return 'context_set';
}
function getContext(r) {
let context = {
userId: r.variables.custom_user_id,
requestTime: r.variables.custom_request_time,
processingTime: Date.now() - parseInt(r.variables.custom_request_time)
};
r.return(200, JSON.stringify(context));
}
export default { setContext, getContext };
|
Nginx 設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| server {
listen 80;
js_path "/etc/nginx/njs/";
js_import context from context.js;
# 定義自訂變數
set $custom_user_id "";
set $custom_request_time "";
js_set $context_status context.setContext;
location /api {
# 觸發 context 設定
set $dummy $context_status;
js_content context.getContext;
}
}
|
效能考量
記憶體管理
njs 為每個請求分配獨立的記憶體池,需要注意以下事項:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // 避免在全域範圍儲存大量資料
// 錯誤示範:
// let globalCache = {}; // 這不會在請求間共享
// 正確做法:使用共享記憶體
function efficientCaching(r) {
let sharedCache = ngx.shared.cache;
let key = r.args.key;
let cached = sharedCache.get(key);
if (cached) {
r.return(200, cached);
return;
}
// 處理並快取結果
let result = expensiveOperation();
sharedCache.set(key, result);
r.return(200, result);
}
function expensiveOperation() {
// 模擬耗時操作
return JSON.stringify({ data: "processed" });
}
export default { efficientCaching };
|
避免阻塞操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 使用非同步子請求而非同步等待
async function nonBlockingRequest(r) {
try {
// 並行發送多個子請求
let [userReply, configReply] = await Promise.all([
r.subrequest('/internal/user'),
r.subrequest('/internal/config')
]);
let userData = JSON.parse(userReply.responseBody);
let configData = JSON.parse(configReply.responseBody);
r.return(200, JSON.stringify({
user: userData,
config: configData
}));
} catch (e) {
r.return(500, "Error: " + e.message);
}
}
export default { nonBlockingRequest };
|
效能最佳化建議
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| // /etc/nginx/njs/optimized.js
// 1. 預先編譯正則表達式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// 2. 重複使用常數
const CONTENT_TYPE_JSON = 'application/json';
const HTTP_OK = 200;
const HTTP_BAD_REQUEST = 400;
function validateEmail(r) {
let email = r.args.email;
if (!email || !emailRegex.test(email)) {
r.headersOut['Content-Type'] = CONTENT_TYPE_JSON;
r.return(HTTP_BAD_REQUEST, JSON.stringify({
valid: false,
error: 'Invalid email format'
}));
return;
}
r.headersOut['Content-Type'] = CONTENT_TYPE_JSON;
r.return(HTTP_OK, JSON.stringify({
valid: true,
email: email
}));
}
// 3. 減少字串操作
function buildResponse(data) {
// 避免多次字串連接
return JSON.stringify(data);
}
// 4. 早期返回減少巢狀
function processRequest(r) {
let auth = r.headersIn['Authorization'];
if (!auth) {
r.return(401, 'Unauthorized');
return;
}
let contentType = r.headersIn['Content-Type'];
if (!contentType || !contentType.includes('application/json')) {
r.return(415, 'Unsupported Media Type');
return;
}
// 主要處理邏輯
r.return(200, 'Processed');
}
export default { validateEmail, processRequest };
|
Worker 進程設定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # nginx.conf
worker_processes auto;
worker_cpu_affinity auto;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
js_path "/etc/nginx/njs/";
js_import main from main.js;
# 共享記憶體設定
js_shared_dict_zone zone=cache:32m timeout=300s evict;
js_shared_dict_zone zone=rate_limit:16m;
# ... 其他設定
}
|
實際應用範例
範例一:API Gateway 實作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| // /etc/nginx/njs/gateway.js
const routes = {
'/api/users': { backend: 'http://user-service:8080', auth: true },
'/api/products': { backend: 'http://product-service:8080', auth: false },
'/api/orders': { backend: 'http://order-service:8080', auth: true }
};
async function route(r) {
let path = r.uri;
// 尋找匹配的路由
let routeConfig = null;
for (let route in routes) {
if (path.startsWith(route)) {
routeConfig = routes[route];
break;
}
}
if (!routeConfig) {
r.return(404, JSON.stringify({
error: 'Route not found',
path: path
}));
return;
}
// 認證檢查
if (routeConfig.auth) {
let authResult = await checkAuth(r);
if (!authResult.valid) {
r.return(401, JSON.stringify({
error: 'Authentication required',
message: authResult.error
}));
return;
}
// 設定使用者資訊標頭
r.headersOut['X-User-ID'] = authResult.userId;
}
// 設定後端變數供 proxy_pass 使用
r.variables.backend_url = routeConfig.backend;
r.return(200);
}
async function checkAuth(r) {
let token = r.headersIn['Authorization'];
if (!token) {
return { valid: false, error: 'Missing token' };
}
try {
let reply = await r.subrequest('/internal/validate-token', {
method: 'POST',
body: token
});
if (reply.status === 200) {
let data = JSON.parse(reply.responseBody);
return { valid: true, userId: data.userId };
}
return { valid: false, error: 'Invalid token' };
} catch (e) {
return { valid: false, error: 'Auth service error' };
}
}
export default { route };
|
Nginx 設定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| http {
js_path "/etc/nginx/njs/";
js_import gateway from gateway.js;
upstream user-service {
server user-service:8080;
}
upstream product-service {
server product-service:8080;
}
upstream order-service {
server order-service:8080;
}
server {
listen 80;
server_name api.example.com;
set $backend_url "";
location /api/ {
js_content gateway.route;
}
location @proxy {
proxy_pass $backend_url;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /internal/validate-token {
internal;
proxy_pass http://auth-service:8080/validate;
}
}
}
|
範例二:速率限制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| // /etc/nginx/njs/ratelimit.js
function checkRateLimit(r) {
let clientId = r.remoteAddress;
let apiKey = r.headersIn['X-API-Key'];
// 優先使用 API Key 作為識別
if (apiKey) {
clientId = 'key:' + apiKey;
}
let limits = ngx.shared.rate_limit;
let now = Math.floor(Date.now() / 1000);
let windowKey = clientId + ':' + now;
// 取得目前計數
let count = limits.get(windowKey) || 0;
let maxRequests = 100; // 每秒最大請求數
if (count >= maxRequests) {
r.headersOut['X-RateLimit-Limit'] = maxRequests.toString();
r.headersOut['X-RateLimit-Remaining'] = '0';
r.headersOut['X-RateLimit-Reset'] = (now + 1).toString();
r.headersOut['Retry-After'] = '1';
r.return(429, JSON.stringify({
error: 'Too Many Requests',
retryAfter: 1
}));
return;
}
// 遞增計數
limits.incr(windowKey, 1, 0);
// 設定回應標頭
r.headersOut['X-RateLimit-Limit'] = maxRequests.toString();
r.headersOut['X-RateLimit-Remaining'] = (maxRequests - count - 1).toString();
r.headersOut['X-RateLimit-Reset'] = (now + 1).toString();
r.return(200);
}
function getRateLimitStatus(r) {
let clientId = r.remoteAddress;
let limits = ngx.shared.rate_limit;
let now = Math.floor(Date.now() / 1000);
let windowKey = clientId + ':' + now;
let count = limits.get(windowKey) || 0;
return count.toString();
}
export default { checkRateLimit, getRateLimitStatus };
|
範例三:請求日誌與監控
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| // /etc/nginx/njs/logging.js
function logRequest(r) {
let startTime = Date.now();
// 建立日誌物件
let logEntry = {
timestamp: new Date().toISOString(),
method: r.method,
uri: r.uri,
query: r.variables.query_string || '',
clientIp: r.remoteAddress,
userAgent: r.headersIn['User-Agent'] || '',
referer: r.headersIn['Referer'] || '',
requestId: generateRequestId(),
contentLength: r.headersIn['Content-Length'] || 0
};
// 設定請求 ID 供追蹤
r.variables.request_id = logEntry.requestId;
return JSON.stringify(logEntry);
}
function logResponse(r) {
let logEntry = {
requestId: r.variables.request_id,
status: r.variables.status,
bodyBytesSent: r.variables.body_bytes_sent,
upstreamResponseTime: r.variables.upstream_response_time || '0',
requestTime: r.variables.request_time
};
return JSON.stringify(logEntry);
}
function generateRequestId() {
let timestamp = Date.now().toString(36);
let random = Math.random().toString(36).substr(2, 9);
return `${timestamp}-${random}`;
}
function healthCheck(r) {
let health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime ? process.uptime() : 'N/A',
version: '1.0.0'
};
r.headersOut['Content-Type'] = 'application/json';
r.return(200, JSON.stringify(health));
}
export default { logRequest, logResponse, healthCheck };
|
Nginx 設定整合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| http {
js_path "/etc/nginx/njs/";
js_import logging from logging.js;
js_set $request_log logging.logRequest;
js_set $response_log logging.logResponse;
log_format json_combined escape=json
'{'
'"request":$request_log,'
'"response":$response_log'
'}';
access_log /var/log/nginx/access.json json_combined;
server {
listen 80;
set $request_id "";
location /health {
js_content logging.healthCheck;
}
location / {
proxy_pass http://backend;
proxy_set_header X-Request-ID $request_id;
}
}
}
|
範例四:A/B 測試路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| // /etc/nginx/njs/abtest.js
const experiments = {
'homepage-redesign': {
variants: {
'control': 50,
'variant-a': 25,
'variant-b': 25
},
active: true
},
'checkout-flow': {
variants: {
'control': 70,
'variant-a': 30
},
active: true
}
};
function getVariant(r) {
let experimentId = r.args.experiment || 'homepage-redesign';
let experiment = experiments[experimentId];
if (!experiment || !experiment.active) {
return 'control';
}
// 使用客戶端 IP 或 Cookie 確保一致性
let userId = r.headersIn['X-User-ID'] || r.remoteAddress;
let hash = simpleHash(userId + experimentId);
let cumulative = 0;
let roll = hash % 100;
for (let variant in experiment.variants) {
cumulative += experiment.variants[variant];
if (roll < cumulative) {
return variant;
}
}
return 'control';
}
function simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
let char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
function routeByVariant(r) {
let variant = getVariant(r);
// 設定變數供 Nginx 路由使用
r.variables.ab_variant = variant;
// 設定回應標頭供前端追蹤
r.headersOut['X-AB-Variant'] = variant;
return variant;
}
function getExperimentInfo(r) {
let experimentId = r.args.experiment;
if (experimentId && experiments[experimentId]) {
r.return(200, JSON.stringify({
id: experimentId,
config: experiments[experimentId],
yourVariant: getVariant(r)
}));
} else {
r.return(200, JSON.stringify({
experiments: Object.keys(experiments),
hint: 'Use ?experiment=<id> to get specific experiment info'
}));
}
}
export default { getVariant, routeByVariant, getExperimentInfo };
|
除錯與疑難排解
啟用除錯日誌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| error_log /var/log/nginx/error.log debug;
http {
js_path "/etc/nginx/njs/";
js_import debug from debug.js;
server {
listen 80;
location /debug {
js_content debug.showDebugInfo;
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // /etc/nginx/njs/debug.js
function showDebugInfo(r) {
let debug = {
request: {
method: r.method,
uri: r.uri,
httpVersion: r.httpVersion,
remoteAddress: r.remoteAddress,
args: Object.fromEntries(
Object.entries(r.args).map(([k, v]) => [k, v])
)
},
headers: {},
variables: {}
};
// 收集所有請求標頭
for (let h in r.headersIn) {
debug.headers[h] = r.headersIn[h];
}
// 收集常用變數
let varNames = [
'host', 'server_name', 'server_port', 'scheme',
'request_uri', 'document_root', 'realpath_root'
];
varNames.forEach(name => {
try {
debug.variables[name] = r.variables[name];
} catch (e) {
debug.variables[name] = 'N/A';
}
});
r.headersOut['Content-Type'] = 'application/json';
r.return(200, JSON.stringify(debug, null, 2));
}
function logError(r) {
try {
// 可能出錯的操作
let data = JSON.parse(r.requestBody);
ngx.log(ngx.INFO, 'Parsed data: ' + JSON.stringify(data));
r.return(200, 'OK');
} catch (e) {
ngx.log(ngx.ERR, 'Error parsing request: ' + e.message);
r.return(400, 'Parse error: ' + e.message);
}
}
export default { showDebugInfo, logError };
|
常見問題解決
- 模組載入失敗:
1
2
3
4
5
| # 檢查模組檔案是否存在
ls -la /usr/lib/nginx/modules/ngx_http_js_module.so
# 檢查 Nginx 設定語法
sudo nginx -t
|
- 腳本語法錯誤:
1
2
3
4
5
| # 使用 njs 命令列工具測試
echo 'console.log("test")' | njs
# 測試腳本檔案
njs /etc/nginx/njs/test.js
|
- 權限問題:
1
2
3
| # 確保 njs 檔案可被 Nginx 讀取
sudo chown -R nginx:nginx /etc/nginx/njs/
sudo chmod 644 /etc/nginx/njs/*.js
|
結語
Nginx JavaScript(njs)模組提供了一個強大且輕量級的方式來擴展 Nginx 的功能。透過 JavaScript 語法,開發者可以輕鬆實作複雜的請求處理邏輯、認證機制、API Gateway 功能等。
相較於傳統的 Lua 模組(OpenResty),njs 具有以下優勢:
- 使用更普遍的 JavaScript 語法
- 官方原生支援,與 Nginx 整合更緊密
- 記憶體管理更加安全
- 持續更新與維護
在生產環境中使用 njs 時,請注意效能考量,善用共享記憶體和非同步子請求,並確保適當的錯誤處理和日誌記錄。