内部工具迟早会需要一个「不发版就能告诉所有人一件事」的通道:
- 新版本发布了,这次改了什么;
- 今晚十点宿主系统维护;
- 下个月起某个流程变了,附说明文档链接。
扩展内部本来就有一条通知总线,但它只能由扩展自己的代码触发,没有云端入口。
实现本身不难:后端已经有一张按 key 拆记录的远程配置表(按域拆 key),加一条 key='announcements' 的记录就够了,不用改表结构、不用新开轮询——同一轮同步里多拉一个 key。
真正要想清楚的是四个决策。
数据结构
{ "items": [ { "id": "release-1-18", // 必填,唯一。已读状态按它记 —— 发布后不要改 "level": "info", // info | success | warn | error "title": "v1.18 已发布", "detail": "顶栏新增常驻消息条,提示不再遮挡表单", "link": { "url": "https://example.com/changelog", "label": "查看更新日志" }, "sticky": true, // 可选:常驻,不自动转静默 "startAt": "2026-08-30T00:00:00Z", // 可选:未到不显示 "endAt": "2026-09-15T00:00:00Z" // 可选:过了不显示 } ]}为什么是数组而不是单条:同时可能有多条有效公告(一条版本更新 + 一条维护通知)。数组也让「下线某条」等于删掉那个元素,不需要状态字段。
先只做全员广播,没有 audience 字段。读取端本来就忽略未知字段,将来要定向再加,是向后兼容的——不必为还没有的需求提前预留。
下发模型选的是「只读层」:公告是纯云端资产,本地不产生也不该编辑,所以写进独立的只读键,不进配置结构、设置页不可编辑、不随个人偏好导出。
决策一:业务提示优先
公告占的是顶栏那条消息条——而那条消息条平时在显示业务提示(「客户已填入」「字段未填全」)。
规则是:公告只在消息条空闲时占位;有业务提示时让位。
理由:业务提示是用户当下正在做的事,公告不是。打断正在进行的操作是净损失。
而公告不会丢——它常驻到被读过为止,用户手停下来自然会看到。
实现上的一个细节:业务提示顶掉公告时,公告不计入「错过的消息」计数。它不是被错过了,只是暂时让位,稍后还会回来。
决策二:已读存本地,不回写云端
已读记录是 chrome.storage.local 里的一个 id 数组。
不回写的理由是安全,不是省事:回写要给客户端开写权限,而读码是内置在扩展里、发给所有人的——等于任何拿到扩展的人都能改公告。
代价是换浏览器或清数据会重看一次。对公告来说完全可以接受。
⚠ 已读列表要按现存公告的 id 做清理,否则它会无限增长——下线的公告 id 永远留在里面。
决策三:必须有一个明确动作才算读过
点击公告,或者点「×」关闭,才算已读。
不用「显示满 N 秒就算读过」——用户可能压根没往那看,那等于静默吞掉一条公告。
所以公告态的消息条右侧要多一个「×」。业务提示态不显示它(业务提示本来就会自己转静默)。
决策四:一次只显示一条
按生效时间最新的未读那条显示,读完或关掉后自动换下一条,右侧用 +N 显示还剩几条。
不做轮播。 顶栏是余光区,动来动去很烦人。
为什么 id 发出去就绝对不能改
这条是整个功能里最容易踩、也最反直觉的:
已读状态是按 id 记的。
所以:
- 改 id = 变成一条新公告,所有已经点掉的人会重新看到;
- 改同一个 id 的内容,读过的人不会再看到新内容。
第二条尤其阴险:你修正了一条公告里的错误信息,已经读过错误版本的人永远看不到更正。
规矩只能是:
- 要发新公告,换新 id;
- 要改一条已发的,先删再加——代价是全员重看,所以要想清楚值不值。
发公告的脚本在 add 时会拦重复 id 并解释这一点。把「为什么不能这么做」写进拒绝信息里,比写在文档里有用得多——人在做这件事的那一刻才会读它。
发版公告会自动顶掉上一条
发版流程最后一步会自动发一条「vX.Y 已发布」。它要顺带移除上一条发版公告,否则用户会同时看到「v1.17 已发布」和「v1.18 已发布」两条。
脚本认两种「发版公告」:新的统一前缀 release-*,以及一份历史遗留 id 名单(那些是早期手工发的,没按前缀命名)。
那份名单看起来很多余,但删了它就会复现「两条发版公告同时挂着」——在 dry-run 里实测复现过。
而非发版公告(故障通知、流程变更)脚本不碰,那些由人管理。
「以为发了其实没发」的四种情况
客户端对云端数据会二次校验,不合格的条目直接丢弃,不报错:
| 情况 | 结果 |
|---|---|
缺 id 或 title | 整条被丢弃,就是不显示 |
level 写了个不认识的值 | 静默退化成 info |
link.url 不是 http(s):// 开头 | 整个 link 被丢掉 |
| 字段超长 | 截断 |
这些「静默丢弃」在客户端是对的(坏数据不该让界面崩),但在发布端就是陷阱——你在管理后台手工改完,看着没问题,用户那边什么都没有。
所以发布脚本在本地先按同一套规则拦一道,不合格的直接报错。客户端的静默是为了容错,发布端的报错是为了让人知道——同一条规则,两端的失败方式应该相反。
安全边界
公告来自云端,对客户端来说是外部输入:
link.url只放行 http(s)。javascript:或data:链接能在用户已登录的宿主会话里执行脚本;- 打开链接带
noopener,noreferrer,新页面拿不到window.opener; - 标题和正文全部转义后渲染,不用
innerHTML直插云端文本。
但有一条技术上防不住:
公告文案本身是不可信内容。 它可以写「请把密码发给 IT」。
这条靠的不是代码,是「只有超管能写这张表」这个组织约束。所以——不要把写权限下放,哪怕是为了方便。
这个功能从设计到上线,代码量大概是一次「加一个远程配置域」的量。花时间的全在那四个决策上,而它们的共同点是:都在回答「公告和用户正在做的事,谁优先」。
答案始终是后者。公告可以等,用户手上的活不能被打断。