将 docs/ 根目录下的 nginx 相关文档统一移动到 docs/nginx/ 子目录, 提高文档组织性和可维护性。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1233 lines
29 KiB
Markdown
1233 lines
29 KiB
Markdown
# Nginx njs JavaScript 模块指南
|
||
|
||
## 目录
|
||
|
||
1. [njs 概述与特性](#1-njs-概述与特性)
|
||
2. [安装与启用](#2-安装与启用)
|
||
3. [核心指令](#3-核心指令)
|
||
4. [njs 语法基础](#4-njs-语法基础)
|
||
5. [常见应用场景与配置示例](#5-常见应用场景与配置示例)
|
||
6. [njs vs Lua 对比](#6-njs-vs-lua-对比)
|
||
7. [性能优化建议](#7-性能优化建议)
|
||
|
||
---
|
||
|
||
## 1. njs 概述与特性
|
||
|
||
### 什么是 njs
|
||
|
||
njs(nginx 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. 安装与启用
|
||
|
||
### 从源码编译安装
|
||
|
||
#### 编译参数
|
||
|
||
```bash
|
||
# 下载 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 顶部添加:
|
||
|
||
```nginx
|
||
# 动态加载 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
|
||
|
||
```bash
|
||
# 添加 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
|
||
|
||
```bash
|
||
# 添加 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
|
||
|
||
```bash
|
||
apk add nginx nginx-mod-http-njs
|
||
```
|
||
|
||
### 验证安装
|
||
|
||
```bash
|
||
# 检查模块是否加载
|
||
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 模块文件。
|
||
|
||
```nginx
|
||
# 基本用法
|
||
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 变量。
|
||
|
||
```nginx
|
||
# 基本用法
|
||
js_set $foo http.foo;
|
||
|
||
# 不缓存模式(每次引用都执行)
|
||
js_set $dynamic http.dynamic_handler nocache;
|
||
```
|
||
|
||
#### js_content
|
||
|
||
将 JavaScript 函数设置为 location 的内容处理器。
|
||
|
||
```nginx
|
||
location /api {
|
||
js_content api.handleRequest;
|
||
}
|
||
```
|
||
|
||
#### js_body_filter
|
||
|
||
设置响应体过滤器函数,用于修改响应内容。
|
||
|
||
```nginx
|
||
location / {
|
||
proxy_pass http://backend;
|
||
js_body_filter http.modifyBody;
|
||
}
|
||
```
|
||
|
||
#### js_engine
|
||
|
||
选择 JavaScript 引擎(njs 或 QuickJS)。
|
||
|
||
```nginx
|
||
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 ||= b`, `a &&= b` |
|
||
|
||
### 不支持的特性
|
||
|
||
| 特性 | 说明 |
|
||
|------|------|
|
||
| 类(Classes) | 不支持 `class` 关键字 |
|
||
| 解构赋值 | `const {a, b} = obj` 不支持 |
|
||
| 展开运算符 | `...` 展开语法不支持 |
|
||
| 默认参数 | `function(a=1)` 不支持 |
|
||
| 生成器函数 | `function*`, `yield` 不支持 |
|
||
| Proxy/Reflect | 不支持 |
|
||
| Map/Set | 不支持原生 Map/Set |
|
||
|
||
### 请求对象(r)API
|
||
|
||
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>` | 共享字典访问 |
|
||
|
||
### 内置模块
|
||
|
||
```javascript
|
||
// 文件系统模块 (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`):**
|
||
|
||
```javascript
|
||
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 配置:**
|
||
|
||
```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`):**
|
||
|
||
```javascript
|
||
// 添加自定义响应头
|
||
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 配置:**
|
||
|
||
```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`):**
|
||
|
||
```javascript
|
||
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 配置:**
|
||
|
||
```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`):**
|
||
|
||
```javascript
|
||
// 基于地理位置的路由
|
||
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 配置:**
|
||
|
||
```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`):**
|
||
|
||
```javascript
|
||
// 简单的文本替换过滤器
|
||
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 配置:**
|
||
|
||
```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`):**
|
||
|
||
```javascript
|
||
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 配置:**
|
||
|
||
```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 |
|
||
| 需要复杂异步逻辑 | njs(async/await 更清晰) |
|
||
| 已有 Lua 代码库 | 继续使用 Lua |
|
||
| 极致性能要求 | LuaJIT(可能更快) |
|
||
| 前后端代码共享 | njs |
|
||
| OpenResty 生态依赖 | Lua |
|
||
|
||
---
|
||
|
||
## 7. 性能优化建议
|
||
|
||
### 1. 使用 QuickJS 引擎
|
||
|
||
QuickJS 引擎在某些场景下性能更好,支持 ES2023。
|
||
|
||
```nginx
|
||
http {
|
||
js_engine qjs;
|
||
}
|
||
```
|
||
|
||
### 2. 合理配置 Context 重用
|
||
|
||
```nginx
|
||
http {
|
||
# 调整 QuickJS context 池大小(默认 128)
|
||
js_context_reuse 256;
|
||
}
|
||
```
|
||
|
||
### 3. 使用缓存
|
||
|
||
```javascript
|
||
// 在 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. 使用共享字典
|
||
|
||
```nginx
|
||
# 配置足够大的共享内存
|
||
js_shared_dict_zone zone=my_cache:100M type=string timeout=300s evict;
|
||
```
|
||
|
||
### 5. 避免同步阻塞
|
||
|
||
```javascript
|
||
// 使用异步操作
|
||
async function good(r) {
|
||
const result = await ngx.fetch('http://backend');
|
||
r.return(200, await result.text());
|
||
}
|
||
|
||
// 避免同步文件操作
|
||
import fs from 'fs';
|
||
// 仅使用同步方法,因为 njs 在 nginx 中不支持异步文件操作
|
||
```
|
||
|
||
### 6. 过滤器性能注意
|
||
|
||
```javascript
|
||
// js_body_filter 和 js_header_filter 只支持同步操作
|
||
function filter(r, data, flags) {
|
||
// 只能使用同步操作
|
||
r.sendBuffer(data.toLowerCase(), flags);
|
||
|
||
// 以下操作不支持:
|
||
// await r.subrequest(...)
|
||
// setTimeout(...)
|
||
}
|
||
```
|
||
|
||
### 7. 预加载配置对象
|
||
|
||
```nginx
|
||
# 预加载配置到内存
|
||
js_preload_object config.json;
|
||
js_preload_object api_keys from /etc/nginx/secrets/keys.json;
|
||
```
|
||
|
||
```javascript
|
||
// 在 JavaScript 中访问
|
||
function handler(r) {
|
||
const config = ngx.conf.config;
|
||
const keys = ngx.conf.api_keys;
|
||
}
|
||
```
|
||
|
||
### 8. 调整 Fetch API 缓冲区
|
||
|
||
```nginx
|
||
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. 监控和调试
|
||
|
||
```javascript
|
||
// 使用定时器监控性能
|
||
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. 代码组织最佳实践
|
||
|
||
```javascript
|
||
// 模块化组织代码
|
||
// 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 };
|
||
```
|
||
|
||
---
|
||
|
||
## 附录:快速参考
|
||
|
||
### 常用代码片段
|
||
|
||
```javascript
|
||
// 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 |
|
||
|
||
### 官方资源
|
||
|
||
- [njs 官方文档](https://nginx.org/en/docs/njs/)
|
||
- [ngx_http_js_module](https://nginx.org/en/docs/http/ngx_http_js_module.html)
|
||
- [GitHub 仓库](https://github.com/nginx/njs)
|
||
- [示例代码](https://github.com/nginx/njs-examples/)
|