lolly/docs/nginx/30-nginx-njs-guide.md
xfy 972eab4267 refactor(docs): 重构文档目录结构,nginx 文档移至子目录
将 docs/ 根目录下的 nginx 相关文档统一移动到 docs/nginx/ 子目录,
提高文档组织性和可维护性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 10:48:14 +08:00

29 KiB
Raw Permalink Blame History

Nginx njs JavaScript 模块指南

目录

  1. njs 概述与特性
  2. 安装与启用
  3. 核心指令
  4. njs 语法基础
  5. 常见应用场景与配置示例
  6. njs vs Lua 对比
  7. 性能优化建议

1. njs 概述与特性

什么是 njs

njsnginx JavaScript是 nginx 的一个模块,它使用 JavaScript 语言扩展 nginx 的服务器功能。njs 提供了一个嵌入式的 JavaScript 引擎,以及一个独立的命令行工具用于开发和调试。

主要特性

特性 说明
ECMAScript 5.1+ 兼容 遵循 ES5.1 严格模式,支持部分 ES6+ 特性
双引擎支持 支持原生 njs 引擎和 QuickJS 引擎 (v0.8.6+)
异步支持 完整的 Promise 和 async/await 支持 (v0.7.0+)
Fetch API 内置 HTTP 客户端功能 (v0.7.0+)
Crypto API WebCrypto 和 Node.js 风格的加密支持
共享字典 跨 worker 进程的内存键值存储 (v0.8.0+)
HTTP 和 Stream 同时支持 HTTP 和 TCP/UDP Stream 模块

适用场景

  • 访问控制: 复杂的安全检查逻辑
  • 请求/响应处理: 动态修改头部、响应体
  • 内容生成: 灵活的内容处理程序
  • API 网关: JWT 验证、动态路由
  • 数据处理: 过滤和转换响应体

2. 安装与启用

从源码编译安装

编译参数

# 下载 nginx 和 njs 源码
cd /usr/local/src
wget http://nginx.org/download/nginx-1.25.3.tar.gz
tar -xzf nginx-1.25.3.tar.gz

git clone https://github.com/nginx/njs.git
cd nginx-1.25.3

# 编译安装(动态模块方式推荐)
./configure \
    --prefix=/etc/nginx \
    --sbin-path=/usr/sbin/nginx \
    --modules-path=/usr/lib/nginx/modules \
    --with-compat \
    --add-dynamic-module=../njs/nginx

make && make install

启用模块

在 nginx.conf 顶部添加:

# 动态加载 njs 模块
load_module modules/ngx_http_js_module.so;

# 如果使用 Stream 模块
load_module modules/ngx_stream_js_module.so;

user nginx;
worker_processes auto;
...

使用包管理器安装

CentOS/RHEL

# 添加 nginx 官方仓库
sudo tee /etc/yum.repos.d/nginx.repo << 'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
EOF

# 安装 nginx 和 njs 模块
sudo yum install nginx nginx-module-njs

Ubuntu/Debian

# 添加 nginx 官方仓库
sudo apt-get update
sudo apt-get install curl gnupg2 ca-certificates lsb-release

curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo apt-key add -
sudo tee /etc/apt/sources.list.d/nginx.list << 'EOF'
deb http://nginx.org/packages/ubuntu `lsb_release -cs` nginx
deb-src http://nginx.org/packages/ubuntu `lsb_release -cs` nginx
EOF

# 安装 nginx 和 njs 模块
sudo apt-get update
sudo apt-get install nginx nginx-module-njs

Alpine Linux

apk add nginx nginx-mod-http-njs

验证安装

# 检查模块是否加载
nginx -V 2>&1 | grep njs

# 使用 njs CLI 工具验证
njs -v

# 测试 JavaScript 语法
njs -c "console.log('Hello from njs');"

3. 核心指令

指令汇总表

指令 语法 上下文 说明
js_import js_import module.js | export_name from module.js; http, server, location 导入 njs 模块
js_set js_set $variable module.function [nocache]; http, server, location 设置变量处理器
js_content js_content module.function; location, if, limit_except 设置内容处理器
js_body_filter js_body_filter module.function [buffer_type=string | buffer]; location, if, limit_except 响应体过滤器
js_header_filter js_header_filter module.function; location, if, limit_except 响应头过滤器
js_var js_var $variable [value]; http, server, location 声明可写变量
js_engine js_engine njs | qjs; http, server, location 设置 JavaScript 引擎
js_path js_path path; http, server, location 设置模块搜索路径
js_shared_dict_zone js_shared_dict_zone zone=name:size [timeout=time] [type=string|number] [evict]; http 共享内存字典
js_periodic js_periodic module.function [interval=time] [jitter=number] [worker_affinity=mask]; location 周期性任务
js_preload_object js_preload_object name.json | name from file.json; http, server, location 预加载配置对象

指令详细说明

js_import

导入 JavaScript 模块文件。

# 基本用法
js_import /etc/nginx/njs/http.js;

# 使用别名
js_import main from /etc/nginx/njs/http.js;

# 导入特定导出
js_import {hello, api} from /etc/nginx/njs/utils.js;

js_set

使用 JavaScript 函数设置 nginx 变量。

# 基本用法
js_set $foo http.foo;

# 不缓存模式(每次引用都执行)
js_set $dynamic http.dynamic_handler nocache;

js_content

将 JavaScript 函数设置为 location 的内容处理器。

location /api {
    js_content api.handleRequest;
}

js_body_filter

设置响应体过滤器函数,用于修改响应内容。

location / {
    proxy_pass http://backend;
    js_body_filter http.modifyBody;
}

js_engine

选择 JavaScript 引擎njs 或 QuickJS

http {
    # 使用 QuickJS 引擎ES2023 支持)
    js_engine qjs;
    
    # 或使用原生 njs 引擎
    js_engine njs;
}

4. njs 语法基础

ECMAScript 兼容性

njs 遵循 ECMAScript 5.1 (严格模式),并支持部分 ES6+ 扩展。

支持的 ES6+ 特性

特性 版本 示例
let / const 0.6.0+ const x = 10; let y = 20;
箭头函数 0.3.1+ (a, b) => a + b
模板字符串 0.3.2+ `Hello ${name}`
async/await 0.7.0+ async function() { await ... }
Promise 0.3.8+ Promise.all(), .then()
ES6 模块 0.3.0+ export default {...}
可选链操作符 0.9.6+ obj?.property
逻辑赋值 0.9.6+ `a

不支持的特性

特性 说明
Classes 不支持 class 关键字
解构赋值 const {a, b} = obj 不支持
展开运算符 ... 展开语法不支持
默认参数 function(a=1) 不支持
生成器函数 function*, yield 不支持
Proxy/Reflect 不支持
Map/Set 不支持原生 Map/Set

请求对象rAPI

HTTP 处理函数接收请求对象 r,包含以下属性和方法:

属性

属性 类型 说明
r.method string HTTP 方法 (GET, POST 等)
r.uri string 请求 URI
r.httpVersion string HTTP 版本
r.remoteAddress string 客户端 IP 地址
r.headersIn object 请求头(只读)
r.headersOut object 响应头(可写)
r.args object URL 查询参数
r.variables object nginx 变量
r.requestText string 请求体文本
r.requestBuffer Buffer 请求体 Buffer
r.status number 响应状态码

方法

方法 说明
r.return(status[, body]) 返回响应
r.send(data) 发送响应体片段
r.sendHeader() 发送响应头
r.finish() 完成响应
r.log(msg) 记录信息日志
r.error(msg) 记录错误日志
r.subrequest(uri[, opts[, cb]]) 发起子请求
r.internalRedirect(uri) 内部重定向

ngx 全局对象

属性/方法 说明
ngx.fetch(url[, opts]) Fetch API 请求
ngx.log(level, msg) 写入错误日志
ngx.version nginx 版本
ngx.worker_id Worker 进程 ID
ngx.shared.<zone> 共享字典访问

内置模块

// 文件系统模块 (v0.8.9+)
import fs from 'fs';
const data = fs.readFileSync('/path/to/file');

// 加密模块 (v0.7.0+)
import crypto from 'crypto';
const hash = crypto.createHash('sha256');

// Buffer 模块
import { Buffer } from 'buffer';
const buf = Buffer.from('hello');

// 查询字符串
import qs from 'querystring';
const obj = qs.parse('a=1&b=2');

5. 常见应用场景与配置示例

示例 1: 动态响应生成

创建一个简单的 "Hello World" 端点。

JavaScript 文件 (/etc/nginx/njs/hello.js):

function hello(r) {
    r.return(200, "Hello world!\n");
}

function personalizedHello(r) {
    const name = r.args.name || "Guest";
    r.return(200, `Hello, ${name}!\n`);
}

function jsonResponse(r) {
    const data = {
        method: r.method,
        uri: r.uri,
        headers: r.headersIn,
        remoteAddress: r.remoteAddress
    };
    r.headersOut['Content-Type'] = 'application/json';
    r.return(200, JSON.stringify(data, null, 2));
}

export default { hello, personalizedHello, jsonResponse };

nginx 配置:

load_module modules/ngx_http_js_module.so;

events {}

http {
    js_import /etc/nginx/njs/hello.js;
    
    server {
        listen 80;
        server_name example.com;
        
        # 简单的 Hello World
        location /hello {
            js_content hello.hello;
        }
        
        # 个性化问候
        location /greet {
            js_content hello.personalizedHello;
        }
        
        # JSON 响应
        location /info {
            js_content hello.jsonResponse;
        }
    }
}

示例 2: 请求头处理

修改请求头和响应头。

JavaScript 文件 (/etc/nginx/njs/headers.js):

// 添加自定义响应头
function addCustomHeaders(r) {
    r.headersOut['X-Powered-By'] = 'njs';
    r.headersOut['X-Request-ID'] = r.variables.request_id;
    r.headersOut['X-Processed-At'] = new Date().toISOString();
    
    // 继续处理到上游
    r.internalRedirect('@proxy');
}

// 响应头过滤器
function modifyResponseHeaders(r) {
    // 移除敏感头
    delete r.headersOut['Server'];
    delete r.headersOut['X-Powered-By'];
    
    // 添加安全头
    r.headersOut['X-Frame-Options'] = 'SAMEORIGIN';
    r.headersOut['X-Content-Type-Options'] = 'nosniff';
    r.headersOut['Referrer-Policy'] = 'strict-origin-when-cross-origin';
}

// 基于请求头的路由
function routeByHeader(r) {
    const apiVersion = r.headersIn['X-API-Version'];
    
    if (apiVersion === 'v2') {
        r.internalRedirect('@api_v2');
    } else if (apiVersion === 'v1') {
        r.internalRedirect('@api_v1');
    } else {
        r.return(400, JSON.stringify({ error: 'Invalid API version' }));
    }
}

// 设置变量用于日志
function getClientInfo(r) {
    const userAgent = r.headersIn['User-Agent'] || 'unknown';
    const device = userAgent.match(/Mobile|Android|iPhone/i) ? 'mobile' : 'desktop';
    return device;
}

export default { 
    addCustomHeaders, 
    modifyResponseHeaders, 
    routeByHeader,
    getClientInfo 
};

nginx 配置:

http {
    js_import /etc/nginx/njs/headers.js;
    
    # 使用 js_set 设置变量
    js_set $device_type headers.getClientInfo;
    
    log_format custom '$remote_addr - $device_type - "$request" ' 
                      '$status $body_bytes_sent';
    
    server {
        listen 80;
        
        # 添加自定义头并代理
        location /api {
            js_content headers.addCustomHeaders;
        }
        
        location @proxy {
            proxy_pass http://backend;
            js_header_filter headers.modifyResponseHeaders;
        }
        
        # 基于 API 版本路由
        location / {
            js_content headers.routeByHeader;
        }
        
        location @api_v1 {
            proxy_pass http://api-v1-backend;
        }
        
        location @api_v2 {
            proxy_pass http://api-v2-backend;
        }
    }
}

示例 3: JWT 验证(简化版)

实现简单的 JWT token 验证。

JavaScript 文件 (/etc/nginx/njs/jwt.js):

import crypto from 'crypto';

// Base64URL 解码
function base64UrlDecode(str) {
    // 添加标准 Base64 填充
    const padding = '='.repeat((4 - str.length % 4) % 4);
    const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
    return Buffer.from(base64, 'base64').toString('utf8');
}

// 简单的 JWT 验证(仅验证签名格式,生产环境请使用完整实现)
function verifyJwt(r) {
    const authHeader = r.headersIn['Authorization'];
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        r.return(401, JSON.stringify({ error: 'Missing or invalid Authorization header' }));
        return;
    }
    
    const token = authHeader.substring(7);
    const parts = token.split('.');
    
    if (parts.length !== 3) {
        r.return(401, JSON.stringify({ error: 'Invalid JWT format' }));
        return;
    }
    
    try {
        const payload = JSON.parse(base64UrlDecode(parts[1]));
        
        // 检查过期时间
        if (payload.exp && payload.exp < Date.now() / 1000) {
            r.return(401, JSON.stringify({ error: 'Token expired' }));
            return;
        }
        
        // 在变量中存储用户信息
        r.variables.jwt_sub = payload.sub || '';
        r.variables.jwt_role = payload.role || 'user';
        
        // 继续处理
        r.internalRedirect('@protected');
        
    } catch (e) {
        r.return(401, JSON.stringify({ error: 'Invalid token payload' }));
    }
}

// 检查权限
function checkRole(r, requiredRole) {
    const userRole = r.variables.jwt_role || '';
    return userRole === requiredRole;
}

export default { verifyJwt, checkRole };

nginx 配置:

http {
    js_import /etc/nginx/njs/jwt.js;
    
    # 声明变量
    js_var $jwt_sub;
    js_var $jwt_role;
    
    server {
        listen 80;
        
        # 公开端点
        location /public {
            proxy_pass http://backend;
        }
        
        # 需要 JWT 验证的端点
        location /protected {
            js_content jwt.verifyJwt;
        }
        
        location @protected {
            proxy_pass http://backend;
            proxy_set_header X-User-ID $jwt_sub;
            proxy_set_header X-User-Role $jwt_role;
        }
        
        # 管理员端点
        location /admin {
            js_content jwt.verifyJwt;
        }
        
        location @admin {
            # 检查角色
            if ($jwt_role != 'admin') {
                return 403 "Forbidden";
            }
            proxy_pass http://admin-backend;
        }
    }
}

示例 4: 动态路由

根据请求参数动态选择上游。

JavaScript 文件 (/etc/nginx/njs/router.js):

// 基于地理位置的路由
function routeByGeo(r) {
    const country = r.variables.geoip_country_code || 'US';
    
    const regionMap = {
        'CN': '@asia_backend',
        'JP': '@asia_backend',
        'KR': '@asia_backend',
        'DE': '@eu_backend',
        'FR': '@eu_backend',
        'UK': '@eu_backend',
        'US': '@us_backend',
        'CA': '@us_backend'
    };
    
    const target = regionMap[country] || '@default_backend';
    r.internalRedirect(target);
}

// 基于请求体的路由(用于 webhooks
function routeByPayload(r) {
    try {
        const body = JSON.parse(r.requestText || '{}');
        const eventType = body.event || 'unknown';
        
        const routeMap = {
            'payment.success': '@payment_success',
            'payment.failed': '@payment_failed',
            'user.created': '@user_created',
            'user.deleted': '@user_deleted'
        };
        
        const target = routeMap[eventType] || '@default_webhook';
        r.internalRedirect(target);
        
    } catch (e) {
        r.return(400, JSON.stringify({ error: 'Invalid JSON' }));
    }
}

// 基于负载的路由(选择最空闲的上游)
function routeByLoad(r) {
    const upstreams = ['backend1', 'backend2', 'backend3'];
    let selected = upstreams[0];
    let minConnections = Number.MAX_SAFE_INTEGER;
    
    for (const upstream of upstreams) {
        // 使用共享字典存储连接数
        const connections = ngx.shared.load_stats.get(upstream) || 0;
        if (connections < minConnections) {
            minConnections = connections;
            selected = upstream;
        }
    }
    
    // 增加计数
    ngx.shared.load_stats.incr(selected, 1, 0, 60);
    
    r.variables.target_upstream = selected;
    r.internalRedirect('@dynamic_proxy');
}

// A/B 测试路由
function routeABTest(r) {
    const cookie = r.headersIn['Cookie'] || '';
    const variantMatch = cookie.match(/ab_variant=(\w+)/);
    let variant = variantMatch ? variantMatch[1] : null;
    
    if (!variant) {
        // 50/50 分流
        variant = Math.random() < 0.5 ? 'a' : 'b';
        // 设置 cookie
        r.headersOut['Set-Cookie'] = `ab_variant=${variant}; Path=/; Max-Age=86400`;
    }
    
    if (variant === 'a') {
        r.internalRedirect('@variant_a');
    } else {
        r.internalRedirect('@variant_b');
    }
}

export default { 
    routeByGeo, 
    routeByPayload, 
    routeByLoad, 
    routeABTest 
};

nginx 配置:

http {
    js_import /etc/nginx/njs/router.js;
    
    # 配置共享字典
    js_shared_dict_zone zone=load_stats:1M type=number;
    
    # GeoIP 模块(可选)
    geoip_country /usr/share/GeoIP/GeoIP.dat;
    
    upstream backend1 {
        server 10.0.1.10:8080;
    }
    
    upstream backend2 {
        server 10.0.1.11:8080;
    }
    
    upstream backend3 {
        server 10.0.1.12:8080;
    }
    
    server {
        listen 80;
        
        # 地理位置路由
        location / {
            js_content router.routeByGeo;
        }
        
        # Webhook 路由
        location /webhooks {
            js_content router.routeByPayload;
        }
        
        # A/B 测试
        location /experiment {
            js_content router.routeABTest;
        }
        
        # 后端定义
        location @asia_backend {
            proxy_pass http://asia-cluster;
        }
        
        location @eu_backend {
            proxy_pass http://eu-cluster;
        }
        
        location @us_backend {
            proxy_pass http://us-cluster;
        }
        
        location @default_backend {
            proxy_pass http://default-cluster;
        }
    }
}

示例 5: 响应体修改

使用 body filter 修改响应内容。

JavaScript 文件 (/etc/nginx/njs/body_filter.js):

// 简单的文本替换过滤器
function replaceText(r, data, flags) {
    if (data.length > 0) {
        // 替换敏感信息
        let modified = data.toString().replace(
            /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
            '****-****-****-****'
        );
        
        // 替换邮箱
        modified = modified.replace(
            /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
            '***@***.***'
        );
        
        r.sendBuffer(modified, flags);
    } else {
        r.sendBuffer(data, flags);
    }
}

// 添加内容到响应体
function appendContent(r, data, flags) {
    if (flags.last) {
        // 在最后添加内容
        const append = '\n<!-- Processed by njs -->';
        r.sendBuffer(data + append, flags);
    } else {
        r.sendBuffer(data, flags);
    }
}

// JSON 数据修改
function modifyJson(r, data, flags) {
    if (data.length > 0) {
        try {
            const obj = JSON.parse(data.toString());
            
            // 添加服务器信息
            obj._meta = {
                processed_by: 'nginx-njs',
                timestamp: Date.now()
            };
            
            // 移除敏感字段
            delete obj.password;
            delete obj.secret_key;
            delete obj.internal_notes;
            
            r.sendBuffer(JSON.stringify(obj), flags);
        } catch (e) {
            // JSON 解析失败,原样返回
            r.sendBuffer(data, flags);
        }
    } else {
        r.sendBuffer(data, flags);
    }
}

// HTML 注入(添加分析脚本)
function injectAnalytics(r, data, flags) {
    if (data.length > 0) {
        let html = data.toString();
        
        if (flags.last && html.includes('</body>')) {
            const analytics = `
<script>
(function() {
    console.log('Page loaded: ' + location.pathname);
    // 发送分析数据...
})();
</script>
`;
            html = html.replace('</body>', analytics + '</body>');
        }
        
        r.sendBuffer(html, flags);
    } else {
        r.sendBuffer(data, flags);
    }
}

export default { 
    replaceText, 
    appendContent, 
    modifyJson, 
    injectAnalytics 
};

nginx 配置:

http {
    js_import /etc/nginx/njs/body_filter.js;
    
    server {
        listen 80;
        
        # 敏感信息脱敏
        location /api/users {
            proxy_pass http://backend;
            js_body_filter body_filter.replaceText;
        }
        
        # JSON API 修改
        location /api/data {
            proxy_pass http://backend;
            js_body_filter body_filter.modifyJson;
        }
        
        # HTML 页面注入
        location / {
            proxy_pass http://backend;
            js_body_filter body_filter.injectAnalytics;
        }
    }
}

示例 6: 使用 Fetch API 的请求聚合

将多个后端请求合并为一个响应。

JavaScript 文件 (/etc/nginx/njs/aggregate.js):

async function aggregateUserData(r) {
    const userId = r.args.userId;
    
    if (!userId) {
        r.return(400, JSON.stringify({ error: 'Missing userId' }));
        return;
    }
    
    try {
        // 并行发起多个请求
        const [profileRes, ordersRes, preferencesRes] = await Promise.all([
            ngx.fetch(`http://user-service/users/${userId}`),
            ngx.fetch(`http://order-service/orders?userId=${userId}`),
            ngx.fetch(`http://preference-service/preferences/${userId}`)
        ]);
        
        // 解析所有响应
        const [profile, orders, preferences] = await Promise.all([
            profileRes.json(),
            ordersRes.json(),
            preferencesRes.json()
        ]);
        
        // 合并数据
        const result = {
            user: profile,
            orders: orders,
            preferences: preferences,
            aggregatedAt: new Date().toISOString()
        };
        
        r.headersOut['Content-Type'] = 'application/json';
        r.return(200, JSON.stringify(result, null, 2));
        
    } catch (e) {
        r.return(500, JSON.stringify({ error: 'Failed to aggregate data', message: e.message }));
    }
}

// 带缓存的请求
async function cachedFetch(r) {
    const cacheKey = 'api:' + r.uri;
    const cached = ngx.shared.api_cache.get(cacheKey);
    
    if (cached) {
        r.headersOut['X-Cache'] = 'HIT';
        r.return(200, cached);
        return;
    }
    
    try {
        const response = await ngx.fetch('http://backend' + r.uri);
        const body = await response.text();
        
        // 缓存 60 秒
        ngx.shared.api_cache.set(cacheKey, body, 60000);
        
        r.headersOut['X-Cache'] = 'MISS';
        r.return(response.status, body);
        
    } catch (e) {
        r.return(502, JSON.stringify({ error: 'Backend unavailable' }));
    }
}

export default { aggregateUserData, cachedFetch };

nginx 配置:

http {
    js_import /etc/nginx/njs/aggregate.js;
    
    # 配置共享字典用于缓存
    js_shared_dict_zone zone=api_cache:10M type=string evict;
    
    server {
        listen 80;
        
        # 数据聚合端点
        location /api/aggregate {
            js_content aggregate.aggregateUserData;
        }
        
        # 带缓存的代理
        location /api/ {
            js_content aggregate.cachedFetch;
        }
    }
}

6. njs vs Lua 对比

特性 njs (JavaScript) Lua (ngx_lua)
语言流行度 广泛,前后端通用 游戏/嵌入式领域为主
学习曲线 低(开发者熟悉) 中等(需学习新语言)
异步支持 原生 Promise/async-await 协程 (coroutine)
模块生态 Node.js 部分兼容 LuaRocks 生态
JSON 处理 原生支持 需 cjson 库
正则表达式 原生支持 模式匹配(不同语法)
调试工具 njs CLI 工具 有限
内存管理 自动垃圾回收 自动垃圾回收
执行引擎 njs/QuickJS LuaJIT/PUC-Rio
HTTP 客户端 内置 Fetch API 需 cosocket
共享内存 js_shared_dict_zone ngx.shared.DICT
子请求 r.subrequest() ngx.location.capture

选择建议

场景 推荐方案
团队熟悉 JavaScript njs
需要复杂异步逻辑 njsasync/await 更清晰)
已有 Lua 代码库 继续使用 Lua
极致性能要求 LuaJIT可能更快
前后端代码共享 njs
OpenResty 生态依赖 Lua

7. 性能优化建议

1. 使用 QuickJS 引擎

QuickJS 引擎在某些场景下性能更好,支持 ES2023。

http {
    js_engine qjs;
}

2. 合理配置 Context 重用

http {
    # 调整 QuickJS context 池大小(默认 128
    js_context_reuse 256;
}

3. 使用缓存

// 在 njs 中缓存计算结果
const cache = {};

function cachedOperation(r) {
    const key = r.args.key;
    
    if (cache[key]) {
        return cache[key];
    }
    
    const result = expensiveComputation(key);
    cache[key] = result;
    return result;
}

4. 使用共享字典

# 配置足够大的共享内存
js_shared_dict_zone zone=my_cache:100M type=string timeout=300s evict;

5. 避免同步阻塞

// 使用异步操作
async function good(r) {
    const result = await ngx.fetch('http://backend');
    r.return(200, await result.text());
}

// 避免同步文件操作
import fs from 'fs';
// 仅使用同步方法,因为 njs 在 nginx 中不支持异步文件操作

6. 过滤器性能注意

// js_body_filter 和 js_header_filter 只支持同步操作
function filter(r, data, flags) {
    // 只能使用同步操作
    r.sendBuffer(data.toLowerCase(), flags);
    
    // 以下操作不支持:
    // await r.subrequest(...)
    // setTimeout(...)
}

7. 预加载配置对象

# 预加载配置到内存
js_preload_object config.json;
js_preload_object api_keys from /etc/nginx/secrets/keys.json;
// 在 JavaScript 中访问
function handler(r) {
    const config = ngx.conf.config;
    const keys = ngx.conf.api_keys;
}

8. 调整 Fetch API 缓冲区

http {
    # 增加 Fetch API 缓冲区
    js_fetch_buffer_size 32k;
    js_fetch_max_response_buffer_size 10m;
    js_fetch_timeout 30s;
    
    # 启用连接池
    js_fetch_keepalive 32;
    js_fetch_keepalive_timeout 60s;
}

9. 监控和调试

// 使用定时器监控性能
function monitoredHandler(r) {
    const start = Date.now();
    
    // 处理逻辑...
    
    const duration = Date.now() - start;
    r.headersOut['X-Response-Time'] = duration + 'ms';
    ngx.log(ngx.INFO, `Request processed in ${duration}ms`);
}

// 内存统计(仅限 CLI
console.log(njs.memoryStats);

10. 代码组织最佳实践

// 模块化组织代码
// utils.js
function logRequest(r) {
    ngx.log(ngx.INFO, `${r.method} ${r.uri}`);
}

function sanitizeInput(input) {
    return input.replace(/[<>]/g, '');
}

export default { logRequest, sanitizeInput };

// handlers.js
import utils from 'utils.js';

function handleApiRequest(r) {
    utils.logRequest(r);
    // ...
}

export default { handleApiRequest };

附录:快速参考

常用代码片段

// 1. 读取请求体
const body = r.requestText;
const json = JSON.parse(body);

// 2. 设置响应头
r.headersOut['X-Custom'] = 'value';
r.headersOut['Content-Type'] = 'application/json';

// 3. 获取查询参数
const param = r.args.name;
const allParams = r.args; // 对象

// 4. 获取请求头
const auth = r.headersIn['Authorization'];

// 5. 子请求
const reply = await r.subrequest('/internal/api');
r.return(200, reply.responseText);

// 6. 外部 HTTP 请求
const response = await ngx.fetch('https://api.example.com/data');
const data = await response.json();

// 7. 日志记录
r.log('Info message');
r.warn('Warning message');
r.error('Error message');
ngx.log(ngx.INFO, 'Nginx log');

// 8. 共享字典操作
const dict = ngx.shared.myDict;
dict.set('key', 'value', 60000); // 60秒 TTL
const val = dict.get('key');
dict.incr('counter', 1, 0);

// 9. Base64 编码/解码
const encoded = btoa('hello');
const decoded = atob(encoded);

// 10. 哈希计算
import crypto from 'crypto';
const hash = crypto.createHash('sha256').update('data').digest('hex');

版本要求

功能 最低版本
基本 HTTP 模块 0.4.0
Stream 模块 0.4.4
Fetch API 0.7.0
WebCrypto 0.7.0
async/await 0.7.0
共享字典 0.8.0
QuickJS 引擎 0.8.6
fs 模块 0.8.9
Fetch keepalive 0.9.2

官方资源