Nginx 是绝大部分前端工程师经常需要打交道的 web 服务器,凭借 事件驱动架构 与 异步非阻塞 I/O 模型,在高并发场景下展现出卓越的性能表现。其核心功能覆盖静态资源托管、反向代理、负载均衡及请求过滤等,成为现代 Web 架构中的关键基础设施。 然而,原生 Nginx 依赖静态配置文件驱动,在动态业务逻辑处理层面存在显著局限,难以满足动态路由分发、实时请求改写、精细化鉴权等复杂场景需求。 为突破这一限制,OpenResty 作为集成 Nginx 与 LuaJIT 的增强套件,通过内置的 Lua 脚本执行环境实现了动态能力的系统性扩展:允许嵌入自定义 Lua 逻辑实现上下文感知的动态决策 ------ 例如通过解析 JWT 令牌进行权限校验,或依据请求参数动态调整后端服务集群的路由策略。OpenResty 通过预集成 lua-nginx-module 等核心组件,在保持 Nginx 高性能特性的同时,构建了完整的动态处理生态,使其能够作为生产级 API 网关支撑复杂业务场景。 但从前端工程师的技术栈视角来看,Lua 方案存在明显的适配门槛。首先是语言生态的壁垒:Lua 的语法体系(如变量作用域规则、函数定义方式)与前端工程师熟悉的 JavaScript 存在本质差异,异步处理模式也与 Promise/async/await 范式不兼容,导致学习曲线陡峭。其次是工具链的割裂:前端工程化常用的构建工具、转译工具均无法直接适配 Lua 脚本,需额外搭建独立的开发链路,增加了环境配置成本。此外,团队协作中,前端工程师与运维工程师的技术栈差异会导致逻辑交接效率低下,简单的路由规则调整都可能因语言隔阂产生沟通成本。
早期实践
为了解决沟通和交付效率,早期我们会采用 nodejs 或者 bun 去做一些中间层去做一些中间层进行转发或处理。举个例子:
客户端染色后,我们希望通过IP来区分一些用户权限,有权限的返回真实的响应,没权限的重定向到
baidu.com
如果只用 nginx 也可以做到,但就是配置不够灵活,如果 ip 有变化还得依靠运维同事帮忙,因此我们加了一层 nodejs
如图所示。 这么做有一个问题,就是得手动写一堆面对 海量流量 的处理策略,什么 熔断 ,限流 甚至得写一些 负载均衡 的算法~~(后续写出来😪)~~
基于 NJS 的实践
今年7月,nginx 官方宣布新增了
quick.js
的引擎,支持了现代 JavaScript 的语法,门槛更低,但需要手动开启,后面有空会把镜像制作过程贴出来。
采用 NJS 的方案后,整个架构模式变成如下图所示,具体实践过程见下文。
前期准备
- 具备 qjs 的一个 nginx 镜像
- 一个
typescript
的开发环境
预期流程
一般来说,访问 ip 需要在数据库中动态维护,本文简单一些,用一个额外的路由(js模块)去mock。整个流程如下:
nginx.conf
nginx
server {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type";
add_header Access-Control-Allow-Credentials "true";
add_header Access-Control-Expose-Headers "Location";
listen 80;
js_engine qjs;
js_import main from conf.d/ngc/njs/index.js;
default_type application/json;
location /test {
js_content main.redirect.test;
}
location /auth {
js_content main.redirect.getVersion;
}
}
auth 路由
其作用是判断请求中的 ['X-Forwarded-For'] 是否属于 黑名单(banIps) 中 ;此外,如果它的 remoteAddress 不是来自 127.0.0.1
说明是外部访问,需要挂掉。
ts
const banIps = ["2.2.2.2", "127.0.0.1"];
function getVersion(r: NginxHTTPRequest) {
if(r.remoteAddress !== "127.0.0.1"){
return r.return(500, "ip error")
}
r.headersOut['Content-Type'] = "text/plain; charset=UTF-8";
const ip = r.headersIn['X-Forwarded-For'] || '127.0.0.1';
if (banIps.includes(ip)) {
return r.return(403, "没有权限");
}
return r.return(200, `有权限, ${r.headersIn['X-Forwarded-For']}`)
}
test 路由
其作用便是返回用户是否可见
ts
async function test(r: NginxHTTPRequest) {
const resp = await ngx.fetch("http://127.0.0.1:80/auth", {
headers: r.headersIn,
})
const canSee = resp.status === 200;
if (!canSee) {
//* 如果没有权限重定向到百度
return r.return(302, "https://baidu.com");
}
//* 如果有权限展示 self
r.headersOut["Content-Type"] = "text/plain; charset=UTF-8";
r.return(200, "hi,siroi 👋👋")
}
typescript 配置
通常在使用 tsc --init
会生成一个 commonjs 的方案,这个需要留意,需要改成 esm 的格式,推荐使用 esnext
json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"rootDir": ".",
"moduleResolution": "node",
"baseUrl": ".",
"outDir": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules"
]
}
项目结构

实践结果
- 设置 ip 为 2.2.2.2(没有权限的❌)
- 设置 ip 为 2.2.2.3 (有权限的✔️)