【nginx】NJS 的简单实践

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 的方案后,整个架构模式变成如下图所示,具体实践过程见下文。

前期准备

  1. 具备 qjs 的一个 nginx 镜像
  2. 一个 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"
  ]
}

项目结构

实践结果

  1. 设置 ip 为 2.2.2.2(没有权限的❌)
  2. 设置 ip 为 2.2.2.3 (有权限的✔️)
相关推荐
roamingcode1 小时前
Claude Code NPM 包发布命令
前端·npm·node.js·claude·自定义指令·claude code
码哥DFS1 小时前
NPM模块化总结
前端·javascript
灵感__idea2 小时前
JavaScript高级程序设计(第5版):代码整洁之道
前端·javascript·程序员
唐璜Taro2 小时前
electron进程间通信-IPC通信注册机制
前端·javascript·electron
陪我一起学编程3 小时前
创建Vue项目的不同方式及项目规范化配置
前端·javascript·vue.js·git·elementui·axios·企业规范
LinXunFeng4 小时前
Flutter - 详情页初始锚点与优化
前端·flutter·开源
GISer_Jing4 小时前
Vue Teleport 原理解析与React Portal、 Fragment 组件
前端·vue.js·react.js
Summer不秃4 小时前
uniapp 手写签名组件开发全攻略
前端·javascript·vue.js·微信小程序·小程序·html
coderklaus4 小时前
Base64编码详解
前端·javascript
NobodyDJ5 小时前
Vue3 响应式大对比:ref vs reactive,到底该怎么选?
前端·vue.js·面试