NestJS 别用 Express 了!Fastify + Nacos 打造配置实时推送

张开发
2026/6/8 14:37:45 15 分钟阅读

分享文章

NestJS 别用 Express 了!Fastify + Nacos 打造配置实时推送
本文基于 NestJS 11 Fastify 5 Nacos v3 API。项目代码见文末仓库链接。一、为什么微前端需要一个 BFF前端产出的 HTML、JS、CSS 都是静态文件。部署到服务器后没有运行时读取环境变量或配置文件的能力不像 Node.js 后端。但微前端的子应用列表本身就是动态的——哪些子应用可用、它们的入口地址是什么这些信息可能随环境变化开发/测试/预发/生产甚至需要在不停服的情况下热更新。常见的三种配置注入方式方案原理一次构建到处部署缺点构建时注入Vite .envimport.meta.env.VITE_*编译期替换否每个环境需重新构建部署时注入entrypoint.sh容器启动时写入 config.json是更新需重启容器运行时加载/api/config启动时 fetch 后端接口是需要额外的后端服务我们选择了第三种——在微前端应用和配置中心之间加一层 BFFBackend For Frontend同时解决两个问题读取配置实时推送变更。┌──────────────┐ │ Nacos │ 配置中心 │ (配置源) │ └──────┬───────┘ │ HTTP v3 API (polling) ┌──────▼───────┐ │ BFF (NestJS) │ 中间层 │ port 3000 │ └──┬────────┬──┘ │ │ GET /api/config GET /api/config/stream (SSE) │ │ ┌──▼────────▼──┐ │ Nginx │ 反向代理 │ port 80 │ └──────┬───────┘ │ ┌──────▼───────┐ │ 浏览器 │ │ (qiankun / │ │ wujie 主应用)│ └──────────────┘二、为什么选 NestJS Fastify 而不是 Express2.1 Fastify 适配器NestJS 默认使用 Express但它支持切换 HTTP 适配器。一行改动// packages/bff/src/main.tsimport{FastifyAdapter,NestFastifyApplication}fromnestjs/platform-fastifyconstappawaitNestFactory.createNestFastifyApplication(AppModule,newFastifyAdapter(),// 替换掉默认的 Express)awaitapp.listen(3000,0.0.0.0)2.2 为什么不用 Express维度ExpressFastify吞吐量基准线2-3x ExpressTypeScript 支持需要types/express原生 TypeScript类型完整插件系统中间件插件封装更好可组合序列化手动 JSON.stringify内置 fast-json-stringifyschema-based更快响应流操作需要通过底层 Node res 对象res.raw直接暴露底层流在这个项目里最关键的理由是写 SSE 时需要直接操作底层响应流。Fastify 的res.raw就是底层的 Node.jsServerResponse读写响应头、写数据都很直接// 写 SSE 响应头res.raw.writeHead(200,{Content-Type:text/event-stream,Cache-Control:no-cache,Connection:keep-alive,})res.raw.write(\n)res.raw.flushHeaders()Express 也能做到但 Fastify 的封装更薄踩坑更少。另外NestJS 的 Controller 可以直接注入 Fastify 的类型importtype{FastifyReply,FastifyRequest}fromfastifyGet(config/stream)stream(Query(dataId)dataId:string,Req()req:FastifyRequest,Res()res:FastifyReply){// ...}依赖注入 完整类型推断 操作底层流的便利性这三者结合在一起是选 Fastify 的核心理由。三、自研 Nacos 接入Nacos 官方提供了 Java SDK 和 Go SDK但没有维护良好的 Node.js SDK社区有但不稳定。所以我们直接调 REST API。3.1 认证与 Token 管理// packages/bff/src/nacos/nacos.service.tsasynclogin():Promisestring{constresawaitfetch(http://${this.nacosAddr}/nacos/v3/auth/user/login,{method:POST,headers:{Content-Type:application/x-www-form-urlencoded},body:username${encodeURIComponent(this.nacosUsername)}password${encodeURIComponent(this.nacosPassword)},})if(!res.ok)thrownewError(Nacos login failed:${res.status})constbodyawaitres.json()this.accessTokenbody.accessToken// token 过期前 5 分钟自动刷新this.tokenExpiresAtDate.now()(body.tokenTtl||18000)*1000-300_000returnthis.accessToken}asyncensureToken():Promisestring{if(!this.accessToken||Date.now()this.tokenExpiresAt){awaitthis.login()}returnthis.accessToken!}关键设计tokenExpiresAt比 Nacos 返回的 TTL 提前 5 分钟留足余量ensureToken()在每次请求前检查过期自动刷新如果因为网络抖动导致登录失败请求会直接抛错——让调用方重试3.2 获取配置与缓存asyncgetConfig(dataId:string):Promise{config:unknown;rawContent:string}{consttokenawaitthis.ensureToken()consturlhttp://${this.nacosAddr}/nacos/v3/client/cs/config?dataId${encodeURIComponent(dataId)}groupNameDEFAULT_GROUPconstresawaitfetch(url,{headers:{accessToken:token}})if(!res.ok)thrownewError(Nacos config fetch failed:${res.status})constbodyawaitres.json()if(body.code!0)thrownewError(Nacos error:${body.message})constrawbody.data.contentconstconfigJSON.parse(raw)this.cache.set(dataId,{data:config,ts:Date.now()})return{config,rawContent:raw}}缓存策略按dataId分组的Mapstring, { data, timestamp }默认 TTL 60 秒可通过CACHE_TTL_MS环境变量调整on-demand 接口GET /api/config优先走缓存轮询检测变更时 bypass 缓存直接拉最新数据3.3 变更检测privatestartPolling(){setInterval(async(){for(constdataIdofthis.trackedDataIds){try{const{config,rawContent}awaitthis.getConfig(dataId)constnewHashString(rawContent.length):rawContent.slice(0,50)constprevHashthis.hashes.get(dataId)if(prevHash!newHash){this.hashes.set(dataId,newHash)this.onChange?.(dataId,config)}}catch{// 单次轮询失败不影响后续}}},Number(process.env.POLL_INTERVAL_MS)||10_000)}变更检测用的是一个轻量 hash内容长度 前 50 个字符。对于 JSON 配置文件通常几百字节这个粒度足以区分任何实质变更。轮询间隔默认 10 秒。当检测到变更时通过onChange回调通知订阅方——这个订阅方就是 SSE 服务。四、SSE 实时推送4.1 客户端管理// packages/bff/src/sse/sse.service.tsInjectable()exportclassSseService{privateclientsByDataIdnewMapstring,SetFastifyReply()add(dataId:string,client:FastifyReply){letgroupthis.clientsByDataId.get(dataId)if(!group){groupnewSet()this.clientsByDataId.set(dataId,group)}group.add(client)}remove(dataId:string,client:FastifyReply){constgroupthis.clientsByDataId.get(dataId)if(!group)returngroup.delete(client)if(group.size0)this.clientsByDataId.delete(dataId)}broadcast(dataId:string,data:unknown){constgroupthis.clientsByDataId.get(dataId)if(!group||group.size0)returnconstpayloadJSON.stringify(data)constmessageevent: config-update\ndata:${payload}\n\nfor(constclientofgroup){try{client.raw.write(message)}catch{group.delete(client)// 写入失败 → 清理死连接}}}}设计要点按dataId分组——qiankun 主应用和 wujie 主应用订阅的是不同的配置广播时只推给关心该配置的客户端广播时用try/catch——如果客户端已断开但还没触发 close 事件write()会抛错直接清理空组自动删除——所有客户端断开后释放内存4.2 SSE Endpoint// packages/bff/src/config-stream/config-stream.controller.tsController(api)exportclassConfigStreamControllerimplementsOnModuleInit{constructor(privatereadonlysse:SseService,privatereadonlynacos:NacosService,){}onModuleInit(){// 关键将 Nacos 的变更检测和 SSE 广播连接起来this.nacos.onChange(dataId,data){this.sse.broadcast(dataId,data)}}Get(config/stream)stream(Query(dataId)dataId:string,Req()req:FastifyRequest,Res()res:FastifyReply){constiddataId||qiankun-main-configres.raw.writeHead(200,{Content-Type:text/event-stream,Cache-Control:no-cache,Connection:keep-alive,})res.raw.write(\n)res.raw.flushHeaders()this.sse.add(id,res)req.raw.on(close,(){this.sse.remove(id,res)})}}onModuleInit里的那一行是整个系统的关键连线NacosService.onChange ──→ SseService.broadcast ──→ 所有订阅的浏览器当 Nacos 上的配置被修改10 秒内轮询间隔所有打开了 SSE 连接的浏览器都会收到推送不用手动刷新页面。4.3 On-Demand 接口顺带实现一个普通的 REST 接口用于浏览器首次加载// packages/bff/src/config-api/config-api.controller.tsController(api)exportclassConfigApiController{Get(config)asyncgetConfig(Headers(host)host:string){constdataIdthis.dataIdFromHost(host)constcachedthis.nacos.getFromCache(dataId)if(cached)returncachedconst{config}awaitthis.nacos.getConfig(dataId)returnconfig}privatedataIdFromHost(host:string):string{if(host.startsWith(qiankun))returnqiankun-main-configif(host.startsWith(wujie))returnwujie-main-configreturnqiankun-main-config}}这里通过 Host 头区分请求来源——qiankun 主应用和 wujie 主应用请求的配置是不同的 Nacos dataId。一个 BFF 同时服务两套微前端容器。五、Nginx 层的角色两套微前端容器qiankun-main / wujie-main的 Nginx 配置一模一样# packages/qiankun-main/nginx.conf server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri /index.html; # SPA fallback } location /api/ { proxy_pass http://bff:3000; # API 反向代理到 BFF proxy_http_version 1.1; } }为什么要通过 Nginx 反向代理而不是前端直接请求 BFF同域前端页面和/api在同一个域名下避免跨域问题解耦前端不关心 BFF 的实际地址bff:3000是 Docker 内网地址统一入口Nginx 可以统一做日志、限流、缓存等六、完整数据流把前面的所有组件串起来一条配置变更的完整旅程1. 运维在 Nacos 控制台修改配置 (e.g. 新增一个子应用) │ 2. NacosService 轮询检测到变更 (10s 内) ├─ hash 对比上次 ≠ 这次 ├─ 更新缓存 └─ 触发 onChange(dataId, newConfig) │ 3. SseService.broadcast(dataId, newConfig) ├─ 找到所有订阅该 dataId 的 SSE 客户端 ├─ 遍历写入 event: config-update\ndata: {json}\n\n └─ 通过 Fastify res.raw.write() 推送到浏览器 │ 4. 浏览器 EventSource 收到 config-update 事件 ├─ 解析 JSON → window.__APP_CONFIG__ 更新 ├─ 触发 CustomEvent(config-changed) │ ├─ Navbar 组件重渲染 → 新子应用出现在导航栏 ├─ App.tsx 重渲染 → 新路由注册 └─ qiankun-main 额外registerMicroApps(新增子应用) │ 5. 用户看到新子应用入口 —— 全程无刷新从配置变更到用户可见端到端延迟 轮询间隔10s 网络传输100ms。七、前端如何消费前端主应用启动时的流程以 wujie-main 为例// packages/wujie-main/src/main.tsxasyncfunctionbootstrap(){// 1. 首次加载fetch /api/configawaitloadConfig()// 2. 渲染应用此时 window.__APP_CONFIG__ 已就绪createRoot(document.getElementById(root)!).render(App/)// 3. 订阅 SSE接收后续变更subscribeConfig()}// packages/wujie-main/src/config/loader.tsexportasyncfunctionloadConfig(){constresawaitfetch(/api/config)constconfigawaitres.json()window.__APP_CONFIG__config}exportfunctionsubscribeConfig(){constesnewEventSource(/api/config/stream?dataIdwujie-main-config)es.addEventListener(config-update,(event){constconfigJSON.parse(event.data)window.__APP_CONFIG__config window.dispatchEvent(newCustomEvent(config-changed))})}首次加载走 REST 接口简单直接后续变更走 SSE实时推送。前端代码不关心配置是从 Nacos 来的还是从文件来的——它只关心window.__APP_CONFIG__里有正确的 JSON。项目源码完整代码见https://gitee.com/bytesifter/front-example├── packages/ │ ├── qiankun-main/ ← qiankun 主应用 │ ├── wujie-main/ ← wujie 主应用 │ ├── exp1-react/ ← React 子应用 │ ├── exp2-vue/ ← Vue 子应用 │ └── bff/ ← NestJS Fastify BFF 服务 └── articles/ ← 本文及相关文章更多关于 qiankun 和 wujie 如何消费配置、如何做框架选型请阅读前一篇文章《qiankun Vite 8 踩坑》。

更多文章