🚀 nacos-web-config:运营半夜改条配置,网页秒更新 —— 不用发版、不用轮询,我把它开源了

0 阅读10分钟

前言

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,它只跟你的后端打交道;你的后端,也只放行你点名的那几条。

整套链路长这样:

链路架构图(Nacos → Starter → SSE → Browser SDK → Page) 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 + 网页配置下发:

  • 如果觉得有帮助,顺手帮我点个 ⭐,让更多人看到它。