一个注入式扩展总得有个入口:点一下,打开面板、看状态、拨开关。
这个入口前后换了三种形态。
为什么不是工具栏图标
最自然的答案是浏览器工具栏上那个扩展图标。它确实有,点开是个 popup。
但它不能当主入口,两个原因:
① 扩展没法把自己固定到工具栏上。 新装的扩展默认藏在拼图图标的下拉里,要用户自己去点「固定」。Chrome 没有让扩展代码自己固定的 API——这件事我们试过不止一次,答案一直是没有。对不熟悉浏览器的用户来说,一个藏起来的入口约等于不存在。
② 它离工作区太远。 用户的注意力在页面上,工具栏在屏幕最顶端的角落。
所以工具栏图标最后只做一件事:账户与凭据录入(工号、自己的 API key)。不做状态展示——那是别处的事。
第一版:一颗浮动小球
入口改成了页面上一颗 40×40 的浮动圆球。它被做得很精致:
- 贴屏幕右沿,三档垂直位置可选;
- 旁边有个换位手柄,拖一下换档;
- 可以折叠成半隐藏的把手,也可以停靠;
- 有呼吸、眨眼、摇头的微动效;
- 记住像素坐标。
对应的配置项有五六个。设计稿还单独做了一版动效稿。
它有一个无法靠打磨解决的问题:
它看起来就不属于这个页面。
一块带阴影、带渐变、会动的圆形色块,贴在一个平面风格的业务系统上——像一块外挂的膏药。用户每天看它八个小时。
还有一个技术上的附带问题:对 position: fixed 的元素做 transform: scale() 悬停动画,在重页面上会触发合成层闪现。那个动效后来改成了只换底色和阴影。
第二版:宿主顶栏里的一盏「状态灯」
决定把入口放进宿主页面自己的顶栏里——那一排原生图标旁边(通知铃铛、帮助问号)。
参照的是顶栏里一个原生元素的写法:一个表示网络状态的指示灯,彩色圆点 + 文字。照着做出来是一枚会变色、会脉冲的绿灯。
被打回了。用户的本意是一个图标,和旁边那排原生图标同款。
扩展是否在运行,不需要一盏常亮的灯来宣告。
真正需要传达给用户的信息只有一个:有没有未读的提醒。而这件事,宿主自己的通知铃铛早就有惯例了——右上角一颗小红点。
第三版:一枚安静的图标
最后的形态:
- 宿主顶栏里的一枚线性 SVG 图标,尺寸、颜色、hover 底色逐项对齐旁边的原生图标(实测量出来的值,不是估的);
- 有未读提醒时,右上角一颗红点;
- 点击打开一个模态面板:身份与授权状态、各功能的开关速拨、到设置页的入口。
那颗小球连同它的五六个配置项整个删掉了——不是藏起来,是代码移除。
对应的旧配置字段在类型定义里还留着(标了 @deprecated、改成可选),只为了兼容用户存储里的旧数据能正常反序列化;默认配置里已经删掉,迁移时会清理残留的键。
⚠ 别因为「字段还在」就把开关加回设置页。 拨了不会有任何效果——一个不起作用的开关,比没有这个开关更糟。
放进别人的顶栏,要付的代价
入口放进宿主的 DOM 里,就要承担宿主 DOM 的所有不稳定性。
代价一:它会被整段重渲染掉
宿主是个 SPA,顶栏会被整段重建,挂进去的节点连带被移除。
所以用 MutationObserver 盯着,发现自己掉了就补挂。回调用 requestAnimationFrame 合并。
⚠ 这里有个细节:必须盯 body,不能只盯顶栏容器。 内容脚本在 document_end 运行,那时宿主往往还没渲染出顶栏——顶栏容器是 null,observer 挂不上去,就永远等不到「顶栏出现」这个事件。结果是入口只有在刷新时顶栏恰好已经存在的情况下才能挂上,看起来像时好时坏。
盯 body 的代价是回调会被各种表单、弹窗的 DOM 变动触发,所以回调本身必须极廉价:
function onMutation() { if (document.getElementById(HOST_ID)?.isConnected) return; // 还在,什么都不做 tryMount(); // 掉了才补}一次 getElementById 加一次 isConnected,命中才做事。
代价二:某些页面上没有顶栏
在宿主的某些非工作台页面,顶栏根本不存在——入口不可见。
兜底是一个不依赖入口是否挂载的快捷键。它注册在全局的键位管理器里,和入口组件没有任何耦合:
找不到顶栏时,只是少了一个可以点的入口,功能本身不受影响。
入口不应该是唯一的路径。 这条在所有注入式 UI 里都成立:你依附的那块宿主 DOM 随时可能不在。
代价三:挂载顺序决定显示顺序
顶栏里不只一个扩展组件(入口图标、一条常驻消息条、一个监控胶囊、一个计数徽标)。它们都用 insertBefore(firstChild) 挂进去,后挂的排在最左边。
所以想要的显示顺序是 [徽标] | [监控] | [消息条] | [入口],挂载顺序就必须反过来。这件事在代码里写了注释——因为下一个改挂载顺序的人不会想到它决定的是视觉顺序。
一条关于设计稿的纪律
那颗小球的动效稿还留在仓库里,当作历史存档。我在它所在目录的 README 里写了一句:
这些 HTML 是历史设计稿存档,不是待办。别主动「重新设计 / 优化」它们。
这句话是有原因的:某次会话在上下文被压缩之后,把很早之前一个「重新设计这个动效稿」的任务参数误当成了当前待办,跑去重做一个早已废弃的设计。
留在仓库里的东西会被当成「还在进行中」。 如果它不是,就明确地写出来。
三版入口的演进,方向始终是变得更不显眼:从一个会动的色块,到一盏会变色的灯,到一枚和周围一模一样的图标。
每一步删掉的都是「让人注意到扩展存在」的东西。而这恰恰是对的——一个每天用八小时的工具,最好的状态是用户忘了它是外挂的。