前言
Hello~大家好,我是秋天的一阵风
nacos-web-config
简介:嵌进你现有 Spring Boot 应用的一个 starter + 一个零依赖前端 SDK,把 Nacos 白名单里的配置,实时、只读、安全地推到浏览器。
版本:0.1.0(已发布到 Maven Central 和 npm)
协议:Apache-2.0
一、前言
很多人第一次接触配置中心时,都会冒出一个很自然的想法: Nacos 里存放业务配置,那前端页面直接读取 Nacos,是不是就能做到改配置、页面立刻生效,不用发版?
这个想法看上去很美好,但在生产环境中基本不可行。
Nacos 本身是面向后端服务设计的配置中心,后端程序可以启动拉取配置,还能监听配置变更。但浏览器不能直接消费 Nacos 配置。
落到业务上,当页面需要动态配置,比如首页活动横幅、功能开关时,团队一般只有两条路可选:
路线 1:文案 / 开关硬编码打包进前端产物。 优点是实现简单,无需额外服务;缺点是任何改动都要重新构建、发布前端版本。运营临时更换活动横幅,完全做不到。
路线 2:后端开发代理接口,作为 Nacos 的中转层。后端订阅 Nacos 变更、缓存配置,对外暴露只包含前端需要字段的接口,前端通过轮询或者 SSE 获取配置。 缺点是每次新增一类前端配置,都需要后端配合开发接口、处理鉴权;轮询自带延迟,SSE 又要处理连接管理。只为一条活动公告单独开发这套逻辑,成本偏高。
两种方案都不够理想。明明配置中心已经支持动态变更,前端却很难低成本享用这个能力。
看到这里,很多人会产生疑问:浏览器到底为什么不能直接读取 Nacos?
二、为什么网页连不到 Nacos
1. 账号没法写进网页代码
Nacos 的账号密码,只要写进前端 JS,任何人按一下 F12,就能在源码里翻出来。
而且这个账号不是只读的 —— 它能发布配置、能删配置。也就是说,你等于把配置中心的修改权限,直接发给了每一个访问网页的人。
2. 授权的最小单位是 namespace
Nacos 的权限是按 namespace 来分的,最小就到这一层。
你其实只想把 web-demo.public.json 这一条给网页看。但权限一开,整个 namespace 里的配置,它都能读。
3. Nacos 不是给浏览器用的
Nacos 一般跑在内网,地址长这样:127.0.0.1:8848。它的接口没有配 CORS,浏览器的跨域请求直接被拒。
它是给后端程序之间互相调用用的,压根没打算接浏览器的 HTTP 请求。
4. 能连上,就等于能被打
如果 Nacos 直接暴露在公网、又没配鉴权,谁都能连。连上就能枚举、就能刷请求,把配置中心打到内存爆掉,所有依赖它的后端一起跟着崩。
你只是想让一条公告上线,结果把整条配置链的安全押了上去。
所以我的做法就一条:
Nacos 的账号和读取,全部留在后端。
后端在内网连 Nacos,只把你事先写进白名单的那几个键读出来、校验一遍,再通过同源 SSE 推给浏览器。
浏览器从头到尾摸不到 Nacos,它只跟你的后端打交道;你的后端,也只放行你点名的那几条。
整套链路长这样:
三、先泼盆冷水:有些场景用不上它
如果只是想给一条公告上线,而且能容忍它晚几十秒才变 —— 那后端开个普通接口,前端 setInterval 定时拉一下,完全够用。这种场景,nacos-web-config 这个库是多余的。
它真正有用,是你慢慢发现 "事情没这么简单" 的时候:
-
你要保证别人不能顺着接口,摸出你不想公开的配置;
-
运营手滑存了一条坏数据,你是原样推给前端让它当场报错,还是拦下来、继续给上一条好的?
-
想秒级生效,就不能定时拉取了,得做实时推送。实时推送一上,坑全跟着来:消息会不会发漏、顺序会不会乱、网络断了怎么接回来、一个网很慢的用户会不会把别人也拖住、同时在线的人多了会不会崩 —— 每一条都得自己想明白;
-
还要分清两件事:是 Nacos 连不上了,还是运营真的把那条配置删了?前者,页面该继续用旧值、等它恢复;后者,页面才该回落到默认。要是把这两种情况当一回事,配置中心半夜抖一下,页面就可能跟着白屏;
-
然后,每一个用到配置的页面,都得自己再写一遍重试和兜底。
这些要是都让你从零写一遍,得花不少时间。
nacos-web-config 这个库就是把这些一次做完、测好、钉死,然后交给你直接能用。
四、它是什么 / 它不是什么
一句话:嵌在你现有后端里的 Spring Boot starter + 一个零运行时依赖的前端 SDK,一条只读、只走白名单的 JSON 通道。
再用大白话翻译一遍:
- 不用新起一个服务,装进你现在的应用就能用;
- 前端 SDK 贼小,TypeScript ESM,零运行时依赖;
- 网页只能拿到你允许的那几条 JSON,别的连影子都看不到。
接入它,只需要三个步骤
① 后端加依赖
<dependency>
<groupId>io.github.fatmii.nacoswebconfig</groupId>
<artifactId>nacos-web-config-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
② 声明白名单
一个逻辑 key 对应一条固定的 Nacos 配置:
nacos-web-config:
enabled: true # 默认 false,不显式开就整个模块不工作
access: public # 必填,public 或 authenticated,没有默认值
nacos:
server-addr: 127.0.0.1:8848
exposures:
ui:
group: WEB_DEMO
data-id: web-demo.public.json
max-bytes: 65536
启动之后端点就有了:/_web-config/v1/stream,前缀可以用 path 改。
③ 前端装包、订阅
npm install @fatmii/nacos-web-config
import { createWebConfig } from '@fatmii/nacos-web-config'
const config = createWebConfig({
definitions: {
ui: { fallback: { announcement: '' } },
},
})
const unsubscribe = config.subscribe('ui', state => {
renderAnnouncement(state.value?.announcement ?? '')
})
endpoint 可以省略,默认就是上面那个路径;
decode 也可以省略,服务端校验过的 JSON 对象会冻结后原样给你,类型由 fallback 推出来。
有两个省心的小设计:
-
subscribe()会立刻用当前状态回调一次,之后每次变化再回调; -
连接在第一次
subscribe()时才建立,config.get('ui')只读当前状态、永远不发请求。
这样页面代码就不用操心 "什么时候开始连"。
想更严一点,给键加个 decode:
ui: {
fallback: { announcement: '' },
decode(value) {
if (typeof value !== 'object' || value === null) throw new Error('ui must be an object')
return { announcement: String((value as { announcement?: unknown }).announcement ?? '') }
},
}
decode 抛异常时,页面上已经显示的值不会被换掉,键状态标成 invalid,错误走 onError。没传 onError,每个键也会 console.warn 一次,不会一声不吭。
五、运营存了条坏内容,页面会怎样
你怕的事,我都替你防了。
每次更新必须是 max-bytes 以内的 JSON 对象。超限的、不是对象的、被 decode 拒掉的,都走同一台状态机,结果都是:不覆盖最近有效值。 内容哈希(sha256: 前缀)相同就不重复推。
后端每次拒收记一条 WARN,里面是逻辑键、稳定错误码、被拒内容的哈希—— 原文不进日志。
记这条的原因很实际:页面显示不对的时候,你要能在服务端日志里一眼看到 “这条内容被拒了、因为什么”,而不是靠猜。。
六、消息不丢、顺序不乱,协议是怎么做到的
实时推送最怕两件事:消息发漏、顺序乱。
协议上,为了达到不丢、不乱的效果,我做了以下处理:
一条流上最多同时订 32 个 key。连接一建立,服务端先发一版完整现状(快照,编号 seq 0 ) ;之后每条改动都带一个单调递增的编号,必须严格挨着走 ——
- 编号跳了(3 蹦到 7)→ 有消息漏了;
- 编号倒着来(5 排在 4 前)→ 顺序乱了。
只要撞上,前端直接抛 PROTOCOL_ERROR 、停手重来,绝不拿错数据往下渲染 —— 就像追剧突然跳集,宁可重新看,也不把乱掉的剧情演给用户。(每个事件还带协议版本 v1 和流 ID,相当于 “统一说第几版的话” 和 “这条连接的身份牌”。)
服务端同样有防线:每 15 秒心跳一次防假死,no-store 不许读旧缓存;
七、断线、故障、删配置,各走各的分支
搞实时推送,最考验功底的就是 “出事了怎么办”。我把三种最容易翻车的情况,做了三种完全不同的处理:
- Nacos 连不上: 配置标 stale,值保留,恢复后自动重读。页面不会因为配置中心抖一下就白屏。
- 运营真删了那条配置: 状态变 deleted,前端回落到 fallback。
- 浏览器断线: 随机化指数退避重连,上限 30 秒,服务端给了 Retry-After 就听它的;45 秒收不到任何字节,判定这条流死了、重开。标签页从后台切回前台,立刻换新流,不干等退避。
八、access 必须自己选:没有默认值
public 还是 authenticated,不配,应用直接启动失败 —— 不让你糊里糊涂把配置放出去。
-
public:能访问到端点的人都能读白名单里的内容,只读、读不到整库。公告、活动文案这种陌生人看到也无所谓的,用这个。 -
authenticated:要求请求上已经挂着登录身份。starter 只检查request.getUserPrincipal()是不是非空,不管身份是谁挂上的 ——Spring Security、你写的 JWT 过滤器、容器托管认证都算。它自己从不执行登录。
这里有个坑值得单独说:如果应用里根本没有组件会去设置 principal,authenticated 会把所有连接都拒成 401。这个行为看着像配坏了,其实是故意的 —— 它不会因为你没接登录,就假装你登录了。
级别是整条流一个,不能逐键设。要分级,就拆两个部署。
九、键怎么划:能聚合,就别拆
一个逻辑键固定对应一条 Nacos 配置;加键要改 application.yml 并重启。这是故意留的门槛:对外可见范围每扩大一次,都该在部署流程里留个痕迹,跟改防火墙规则一个性质。
但高频改的内容,不需要新键。 暴露一个粗粒度键(比如 homepage),把容易变的字段全放进它那条 JSON,运营改一条就即时到页面,白名单永远不用动。
真正该拆键的地方,是边界不同的地方:访问级别不一样、负责人不一样、大小预算不一样。
十、30 秒,自己跑一遍
# 固定版本的本地 Nacos(Docker)
cd dev-environment ; .\start.ps1
# 参考宿主,不全局装任何东西
cd ..\java ; mvn -q -pl examples/spring-mvc -am install -DskipTests
mvn -pl examples/spring-mvc spring-boot:run
打开 http://localhost:8080,在 Nacos 控制台发布 WEB_DEMO / web-demo.public.json,内容随便写 {"announcement":"hello"},页面数秒内变,不用刷新。
总结
装个 starter、配一段 YAML、前端订阅一下,Nacos 配置就实时、只读、安全地到了浏览器—— 不用发版,不用轮询,更不用把 Nacos 暴露给浏览器。
如果你正在做 Spring Boot + Nacos + 网页配置下发:
- 用得上,可以去仓库看文档:github.com/FatMii/naco…;
- 如果觉得有帮助,顺手帮我点个 ⭐,让更多人看到它。