设计令牌的最小实现:一个颜色变量驱动全站(附阅读进度条)

设计令牌的最小实现:一个颜色变量驱动全站(附阅读进度条)-知集
设计令牌的最小实现:一个颜色变量驱动全站(附阅读进度条)
此内容为付费阅读,请付费后查看
¥1
立即购买
您当前未登录!建议登陆后购买,可保存购买订单
付费阅读
已售 1
阅读约 42 分钟12,303 字

摘要:「设计令牌」听起来是大厂设计系统里的重装备,但它的最小可用形态只有三行:一个色值源头、一组派生纯函数、一堆 CSS 变量引用。本文用「品牌主色 + 阅读进度条」这对最小组合,讲清令牌体系为什么值钱、hex 解析有哪些必须兜住的边界(写错会让全站 var() 一起静默失效)、以及一个 124 行的进度条里藏着的 4 个工程决定。
适合阅读:做主题 / 组件库的前端与全栈开发,以及想给项目引入设计令牌但不想上重装备的人。文末附零依赖交付代码(纯函数 29 项 + 浏览器 27 项断言)。

一、颜色抄 19 遍的代价

一个真实的起点:站点主色 #2e7cf6 在主题代码里出现了 19 次 —— 按钮、链接、进度条、徽章、通知高亮、邮件模板各一份。

于是「换个品牌色」这件事变成了:

  • 打开 19 个文件,Ctrl+H 替换 —— 换一次要半小时,还容易漏;
  • 更麻烦的是那些派生出来的颜色:hover 加深色、阴影用的半透明色、浅底强调色 —— 它们是算出来的,也被人肉抄成了字面量;
  • n
  • 邮件模板是特例:邮件客户端不支持 CSS 变量,只能在 PHP 侧注入实际色值 —— 于是它天然是「另一份真值」。

这就是令牌体系要解决的问题:让颜色变成「一个源头 + 一堆派生」,而不是一堆字面量。

二、最小可用形态:三层

层做什么放在哪为什么
① 源头后台一个 color 字段(品牌主色)配置模块需要一个「可改」的入口,这是唯一应该存值的地方
② 派生hex → rgba / darken 纯函数core 层派生是计算,不是配置;放模块里会让「关掉模块」连带失去派生能力
③ 消费组件引用 var(--brand)各组件组件只认变量名,不认色值 —— 这才是「改一处全站生效」的关键

第二层的位置是这套体系里最容易做错的地方,值得展开讲。

2.1 为什么令牌与派生必须放在 core,不能放业务模块

我在这上面踩过一次真实的坑:早期把「令牌输出」放进了一个可关闭的业务模块里。结果模块一被关掉(或被删掉),「后台改品牌色 → 全站生效」的能力就静默消失了 —— 全站 CSS 都写了 fallback,页面不破版,只是改色不再生效,没有任何报错。

更严重的一次是「删模块没查引用」:那个模块里定义了 zhiji_token_color(),而它在别处被调用了 19 次(含邮件渲染路径)。模块一删,任何一次发信都会 500。静态扫描查不出来(函数名是动态拼的),只有运行时探针才暴露。

所以现在的位置约定是:

  • 纯函数与令牌输出 → core(随主题常驻,不受任何开关影响);
  • 配置入口 → 模块(只负责渲染那个 color 字段 + 「关开关回落默认色」);
  • 模块与 core 之间用 apply_filters 解耦:模块在就注入,不在就回落默认值 —— core 永远不会因为某个模块被关掉而崩。

三、hex 解析:必须先说清「非法值怎么办」

这是整套代码里最不起眼、却最容易静默破版的一段。看派生函数的输入容忍度:

输入期望结果说明
#2e7cf6#2e7cf6标准 6 位
#abc#aabbcc3 位缩写要展开
abc#aabbcc没写 # 也要认
#AABBCC #aabbcc空格 + 大写要归一
xyz / #abcd / #12g456空串非法一律返回空串 —— 这一条最关键

最后一行为什么关键?如果你对非法值「硬转」(比如强行当 6 位截断),就会往 CSS 里写出一个残缺色值;如果你返回空串却仍然拼接,会得到这样的输出:

:root{ --brand:; --brand-deep:; }

空值变量的后果不是「这个颜色不对」,而是所有引用它的地方一起失效 —— 按钮变透明、进度条消失、边框不见。所以正确做法是两层防御:解析层对非法值返回空串;汇总层发现空令牌表就整块不输出,让组件里的 fallback 生效。

顺带两个格式化细节:alpha 要夹取到 0~1(越界会写出非法 rgba);数字格式化后要去掉尾随零(0.300 → 0.3,1.000 → 1),否则 CSS 里会出现一堆 rgba(46,124,246,1.000) 这种能跑但难看的值。

四、消费方示例:一条 3px 的阅读进度条

令牌有没有价值,要看消费方是不是真的「只认变量」。拿阅读进度条做例子:

4.1 颜色只写变量 —— 令牌的收益在这里兑现

.zdw-progress-bar{
  background: var(--zdw-brand, #2e7cf6);
  box-shadow: 0 0 8px var(--zdw-glow, transparent);
}

这样「改品牌色」这一个动作,自动改掉了进度条、它的发光、以及所有后续接进来的组件。而且注意每个 var() 都带了 fallback —— 令牌没输出时组件依然可用,只是回到默认蓝。

4.2 用 rAF 节流,不是直接改样式

滚动时 scroll 事件在移动端每秒可以触发上百次。如果每次都直接读 scrollHeight 再写 style.width,就会在同一个帧里交替读写布局属性,触发强制同步布局,滚动会明显发涩。

正确姿势是把「读」和「写」都放进 requestAnimationFrame 里,并且用一个标志位做节流:

var ticking = false;
function onScroll() {
    if (!ticking) { ticking = true; window.requestAnimationFrame(render); }
}

4.3 页面不足一屏:一个必然出现的 NaN

百分比是这样算的:(scrollTop / (scrollHeight - innerHeight)) * 100。当页面内容比视口还短时,分母是 0 或负数 —— 0/0 得到 NaN,写进样式就是 width: NaN%,整条进度条行为未定义。

这个 case 在「文章很短」或「移动端横屏」时会真实出现。必须显式挡掉:

var h = document.documentElement.scrollHeight - window.innerHeight;
if (!(h > 0)) return 0;      // 🧨 不足一屏 → 直接 0,别让除零产生 NaN

4.4 内联还是外链?

进度条样式 + 脚本合计不到 1KB,且只在页面上出现一次 —— 这种情况内联更划算(省一次请求,还能避免样式到达前的短暂无样式)。(内联 vs 外链的判定阈值与实测,我在另一篇里专门做过测量。)

五、三层实测

层测什么规模结果
① 纯函数单测(PHP CLI)hex 归一化 9 种输入(含 4 类非法)/ rgba 的 alpha 夹取与格式化 / darken 的边界(0、1、越界、非法)/ 令牌派生与空令牌表29 项29/29 通过
② 浏览器测试(无头 + 真实输出)令牌取色链路(组件背景色 == 品牌色)/ 初始 0% / 中段 50% / 到底 100% / 不足一屏恒 0% 且无 NaN / scroll→rAF→写入链路 / 文字开关3 场景 × 9 断言27/27 通过
③ 线上注入令牌变量在文章页/首页/归档都输出;进度条前台有、后台无;卸载后无残留5 次抓取全绿

第 ② 层有两处「测试环境的坑」,也会在别的地方遇到,我记在这:

现象根因正确做法
派发 scroll 事件后宽度不变headless 里 window.scrollTo 不真的改变 pageYOffset(没有合成器帧)stub pageYOffset + 手动 dispatchEvent(new Event('scroll')),测接线而不是测浏览器
stub 设成 0 却不生效代码里是 pageYOffset || scrollTop || 0,0 是 falsy 会落到第二分支stub 用非零值(10% / 50%)绕开兜底;真实浏览器里两者同步为 0 只在顶部,所以无害
断言结果时有时无rAF 回调是异步的,--dump-dom 可能在它之前就抓取把 requestAnimationFrame stub 成同步执行,链路即可确定性断言

六、令牌体系的三条纪律

  1. 组件里不出现色值字面量(#2e7cf6 / rgb(...))—— 只允许 var(--token);出现字面量就说明还差一个令牌。
  2. 派生色一律算,不抄:hover 加深、半透明底、发光都由纯函数派生;抄出来的派生色是「改色时必然漏掉的那一份」。
  3. 例外要显式标注:邮件模板不支持 CSS 变量,必须在 PHP 侧取色值 —— 这种例外要在代码注释里写明原因,否则下一个人会以为你忘了用令牌。

附:测试方法

这套代码的测试分两层做,思路可以直接搬走:

① 纯函数层:定义几个桩函数(add_action / apply_filters / esc_attr / is_admin),
   用 php CLI 直接 require 交付文件 → 断言归一化、派生、边界值。
   ⚠️ 非法输入必须写成断言(空串 / 4 位 / 含非 hex 字符),这类边界才是 bug 高发区。

② 浏览器层:先用本地 PHP 生成**真实输出片段**,再注入无头浏览器 —— 不手抄 CSS/JS。
   断言令牌取色链路(getComputedStyle 拿到的是否真是品牌色)、
   以及把 requestAnimationFrame stub 成同步后再派发事件,让异步链路也可确定性断言。

最后一个提醒:不要用「页面能打开、颜色看起来对」来验收令牌体系。令牌出问题的表现是「某个颜色不对」,很容易被当成设计问题而放过;用 getComputedStyle 把实际取到的值断言出来,才算测过。


📚 zibll 子主题开发实录(共 22 篇) · 系列总目录与阅读路径 →

① 前端性能与加载

② 排版与样式系统

③ 功能实现与边界

④ 系统与运维

⑤ 成就与活动系统

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容