当前位置: 首页 > 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.jsqmd.com/news/975141/

相关文章:

  • 武汉爱而迷联系电话是多少?正规对接方式与品牌详解 - 中媒介
  • Linux 磁盘操作作业
  • 行情高位变现!2026广州黄金回收TOP1报价超亲民 - 开心测评
  • 2026广州黄金回收深度测评!正规连锁品牌口碑夺冠 - 开心测评
  • 深度解析RTAB-Map:基于外观记忆的实时SLAM系统架构与工程实践
  • 2026深圳新房甲醛检测全流程:CMA检测从预约到出报告实录 - 环保除醛知识库
  • 【H1】深度工业测评:双叠自锁垫圈出厂前要做哪些测试?重型机械紧固件抗震防线的硬核数据解构
  • 终极指南:WorkshopDL如何让非Steam游戏也能畅享创意工坊模组
  • 重庆力冠衡器:自贡电子测量仪器公司 - LYL仔仔
  • 老客带新客!湘潭这家麻辣烫口碑出圈,食客扎堆前来品尝 - 资讯快报
  • 基于LIN总线的分布式五轴机器人控制系统设计与实现
  • Winhance中文版:从Windows新手到系统调优专家的进阶之旅
  • MCreator终极指南:无需编程基础快速制作我的世界模组
  • 2026行业优选-靠谱单头热压机生产厂家|高性价比水口振落机源头厂家合集与推荐:功匠领衔 - 栗子测评
  • StarCore DSP上判决反馈均衡器(DFE)的定点实现与优化
  • Playnite终极指南:如何一键整合20+游戏平台打造专属游戏库
  • 2026年贵阳市泽成学校行业深度测评 - 精选优质企业推荐官
  • i.MX RT内存优化实战:从架构解析到代码重定位提升性能
  • 徐州SEO优化公司|官网收录与排名维护,徐州SEO托管服务商选择指南 - 招财兔数字员工
  • NetTools Pro V1.2.1 更新:WiFi 扫描、连接监控与网络接口
  • 猫抓资源嗅探扩展:全方位指南助你轻松下载网页媒体资源
  • MPC500 TPU FQD正交解码:硬件实现、模式切换与工程实践详解
  • 如何5分钟快速上手Buck-Boost电感计算器:电源工程师的终极指南
  • 干皮用什么眼油淡化细纹?这3款深度润养改善干燥纹路 - 全网最美
  • 基于MR32微控制器的三相感应电机变频调速与PFC系统软件架构解析
  • 如何彻底清理macOS应用残留文件:三步解决磁盘空间问题
  • 别再踩坑了!CentOS7上Oracle 12c保姆级安装避坑指南(附中文方块字解决方案)
  • 2026济南黄金回收避坑榜:8大实体门店坐镇,报价实打实碾压虚价套路 - 奢侈品回收评测
  • 义乌海外仓一件代发服务商选型参考与选择逻辑 - 资讯速览
  • QML与QWidget的流畅度