当前位置: 首页 > news >正文

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

本文基于 NestJS 11 + Fastify 5 + Nacos v3 API。项目代码见文末仓库链接。

一、为什么微前端需要一个 BFF

前端产出的 HTML、JS、CSS 都是静态文件。部署到服务器后,没有运行时读取环境变量或配置文件的能力(不像 Node.js 后端)。

但微前端的子应用列表本身就是动态的——哪些子应用可用、它们的入口地址是什么,这些信息可能随环境变化(开发/测试/预发/生产),甚至需要在不停服的情况下热更新。

常见的三种配置注入方式:

方案原理一次构建到处部署缺点
构建时注入(Vite .env)import.meta.env.VITE_*编译期替换每个环境需重新构建
部署时注入(entrypoint.sh)容器启动时写入 config.json更新需重启容器
运行时加载(/api/config)启动时 fetch 后端接口需要额外的后端服务

我们选择了第三种——在微前端应用和配置中心之间加一层 BFF(Backend 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 而不是 Express

2.1 Fastify 适配器

NestJS 默认使用 Express,但它支持切换 HTTP 适配器。一行改动:

// packages/bff/src/main.tsimport{FastifyAdapter,NestFastifyApplication}from'@nestjs/platform-fastify'constapp=awaitNestFactory.create<NestFastifyApplication>(AppModule,newFastifyAdapter(),// 替换掉默认的 Express)awaitapp.listen(3000,'0.0.0.0')

2.2 为什么不用 Express

维度ExpressFastify
吞吐量基准线2-3x Express
TypeScript 支持需要@types/express原生 TypeScript,类型完整
插件系统中间件插件(封装更好,可组合)
序列化手动 JSON.stringify内置 fast-json-stringify(schema-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}from'fastify'@Get('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():Promise<string>{constres=awaitfetch(`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}`)constbody=awaitres.json()this.accessToken=body.accessToken// token 过期前 5 分钟自动刷新this.tokenExpiresAt=Date.now()+(body.tokenTtl||18000)*1000-300_000returnthis.accessToken}asyncensureToken():Promise<string>{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}>{consttoken=awaitthis.ensureToken()consturl=`http://${this.nacosAddr}/nacos/v3/client/cs/config?dataId=${encodeURIComponent(dataId)}&groupName=DEFAULT_GROUP`constres=awaitfetch(url,{headers:{accessToken:token}})if(!res.ok)thrownewError(`Nacos config fetch failed:${res.status}`)constbody=awaitres.json()if(body.code!==0)thrownewError(`Nacos error:${body.message}`)constraw=body.data.contentconstconfig=JSON.parse(raw)this.cache.set(dataId,{data:config,ts:Date.now()})return{config,rawContent:raw}}

缓存策略:

  • dataId分组的Map<string, { 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)constnewHash=String(rawContent.length)+':'+rawContent.slice(0,50)constprevHash=this.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.ts@Injectable()exportclassSseService{privateclientsByDataId=newMap<string,Set<FastifyReply>>()add(dataId:string,client:FastifyReply){letgroup=this.clientsByDataId.get(dataId)if(!group){group=newSet()this.clientsByDataId.set(dataId,group)}group.add(client)}remove(dataId:string,client:FastifyReply){constgroup=this.clientsByDataId.get(dataId)if(!group)returngroup.delete(client)if(group.size===0)this.clientsByDataId.delete(dataId)}broadcast(dataId:string,data:unknown){constgroup=this.clientsByDataId.get(dataId)if(!group||group.size===0)returnconstpayload=JSON.stringify(data)constmessage=`event: config-update\ndata:${payload}\n\n`for(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.ts@Controller('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){constid=dataId||'qiankun-main-config'res.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.ts@Controller('api')exportclassConfigApiController{@Get('config')asyncgetConfig(@Headers('host')host:string){constdataId=this.dataIdFromHost(host)constcached=this.nacos.getFromCache(dataId)if(cached)returncachedconst{config}=awaitthis.nacos.getConfig(dataId)returnconfig}privatedataIdFromHost(host:string):string{if(host.startsWith('qiankun'))return'qiankun-main-config'if(host.startsWith('wujie'))return'wujie-main-config'return'qiankun-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:

  1. 同域:前端页面和/api在同一个域名下,避免跨域问题
  2. 解耦:前端不关心 BFF 的实际地址(bff:3000是 Docker 内网地址)
  3. 统一入口: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(){constres=awaitfetch('/api/config')constconfig=awaitres.json()window.__APP_CONFIG__=config}exportfunctionsubscribeConfig(){constes=newEventSource('/api/config/stream?dataId=wujie-main-config')es.addEventListener('config-update',(event)=>{constconfig=JSON.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 踩坑》。

http://www.rkmt.cn/news/1486867.html

相关文章:

  • 2026深圳新房甲醛检测全流程:CMA检测从预约到出报告实录 - 环保除醛知识库
  • 终极指南:WorkshopDL如何让非Steam游戏也能畅享创意工坊模组
  • 2026行业优选-靠谱单头热压机生产厂家|高性价比水口振落机源头厂家合集与推荐:功匠领衔 - 栗子测评
  • StarCore DSP上判决反馈均衡器(DFE)的定点实现与优化
  • NetTools Pro V1.2.1 更新:WiFi 扫描、连接监控与网络接口
  • MPC500 TPU FQD正交解码:硬件实现、模式切换与工程实践详解
  • 如何5分钟快速上手Buck-Boost电感计算器:电源工程师的终极指南
  • 如何彻底清理macOS应用残留文件:三步解决磁盘空间问题
  • 义乌海外仓一件代发服务商选型参考与选择逻辑 - 资讯速览
  • 2026济南黄金回收避坑榜:8大实体门店坐镇,报价实打实碾压虚价套路 - 奢侈品回收评测
  • 基于Hadoop+Spark的中文手写数字实时识别教学实践包(含代码、报告、演示视频)
  • Workflow Agent 是什么:为什么很多生产级系统不再把流程完全交给模型
  • 基于强化学习的UI动效参数优化:从手动调参到智能搜索
  • 2026年6月劳力士国内官方热线与售后收费标准全解析 - 资讯速览
  • 2026最新大学生证书含金量排行榜:避开考证坑,拿下高薪发牌权!
  • 2026 石家庄黄金回收本地测评,实力商家大盘点 - 奢侈品回收测评
  • 微博图片批量下载终极指南:如何高效获取高清素材库
  • 2026长沙留学机构红黑榜:行业头部梯队十家优选 - 资讯快报
  • 026 年 Q2 网红螺蛳粉加盟 推荐权威排名:TOP5 推荐榜、网红螺蛳粉加盟”、“2026年热门螺蛳粉加盟品牌及费用 - 安互工业信息
  • 企业财税服务系统哪个好?亿企赢视角下的中小企业选型判断标准 - 新闻快传
  • DSP56300 ECP并口DMA高速数据传输实战:原理、配置与优化
  • 三步实现专业级AI换脸:roop-unleashed完整操作指南
  • DevOps 入门系列:从 Pod 到 Ingress(K8s 核心概念)
  • Day 8:手撸一个豆包!流式输出 + 工具调用 + Web聊天应用
  • ncmppGui极速解密教程:3分钟掌握NCM音乐文件转换技巧
  • Sunshine游戏串流终极指南:构建你的个人云游戏服务器
  • 2026职场高阶能力含金量排行榜20名:进阶避坑与职业发展指南
  • MFC与Windows钩子实战:构建来电显示程序的技术解析
  • 如何用RTAB-Map视觉SLAM让机器人看懂复杂世界:5步构建精准3D地图
  • GetQzonehistory终极指南:如何永久保存你的QQ空间记忆