ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

TinyVue设计系统实战:从设计令牌到主题切换的完整落地指南

TinyVue设计系统实战:从设计令牌到主题切换的完整落地指南 1. 组件库并非设计系统先厘清边界很多团队把“装一个组件库、页面风格统一”当成设计系统落地了结果做着做着就发现不对劲按钮是统一了但弹窗的间距和表单的间距不是一个体系主色是改了但成功、警告、错误色在各组件里深浅不一小屏适配更是靠开发“凭感觉改”。组件库和设计系统之间隔着的恰恰是一整套可度量的决策规则。TinyVue 作为华为开源的前端组件库在国内企业级项目里使用率一直不低但我观察到一个很普遍的现象大家把它当作“现成 UI 资源包”来用很少有人去研究它背后那套设计系统是怎么实现的。这才是我写这个系列的原因——不聊“这个组件长什么样”而是聊“这套设计语言为什么能长成这样、你怎么在业务项目里完整复用”。先说结论组件库是资产设计系统是规则。资产给规则提供输出物规则保证资产在规模扩张时不塌方。TinyVue 的设计系统实现核心有三层设计令牌层颜色、字体、间距、圆角、阴影、动效时长等最小单元的命名与取值。组件映射层令牌如何映射到每个组件的具体样式属性比如一个按钮的 hover 背景色对应哪个语义令牌。工程注入层令牌通过什么机制进入运行时支持亮暗主题切换、品牌换肤、局部覆盖而不会互相污染。这篇文章我只做一件事把这套三层实现拆开手把手讲清楚并且把我落地过程中的坑一并写出来。适合三类读者准备导 TinyVue 进项目的负责人、已经在用但觉得“定制不舒服”的开发者、以及所有想把任何组件库改造得更有设计系统味的同学。1.1 组件库解决了什么设计系统补上什么组件库解决的问题很具体提供可复用的 UI 模块减少重复造轮子。设计系统要解决的问题更抽象让团队在速度、一致性和体验质量之间找到可持续的平衡点。我用一个实际场景说明。假设你有一个表格组件TinyVue 默认把行高定成了 40px你的业务弹窗需要更紧凑的表单间距是 28px。没有设计系统约束时开发会直接写:deep()覆盖一次两次没问题等覆盖超过五十处新来的同学根本不知道哪些地方该用 28、哪些该用 40。设计系统要做的就是定义“紧凑密度”的令牌组让表格、表单、弹窗在同一个密度档位下自动统一。TinyVue 的好处是它把常见企业场景的组件都覆盖了但组件再多也只是“零件”真正让零件协同工作的是设计令牌和组件之间的映射关系。所以这篇文章的重心不在 API 使用而在令牌体系。1.2 TinyVue 在设计系统上的独特切入点TinyVue 有它自己的特殊性这也是为什么拿它来讲设计系统实现比较有代表性。第一它要同时支持 Vue 2 和 Vue 3。这意味着它的样式系统不能依赖某个版本特有的运行时机制必须足够抽象。第二它提供跨端能力不只是 Web还涉及小程序等场景所以设计令牌必须是纯数据化的而不是写在某个.vue文件里的魔法数字。第三它是企业级组件库对接的是大量后台管理系统、中台项目这类项目最需要的不是好看而是可维护、可换肤、可无障碍适配。这三点决定了 TinyVue 的设计系统实现路径令牌先行、运行时注入、样式与逻辑解耦。理解了这条路径你去读它的配置源码会有一种“果然如此”的感觉。2. 设计令牌决定设计系统的地基TinyVue 的令牌化思路设计系统里最容易被低估的就是设计令牌。很多人以为它只是把颜色变量抽出来实际远远不止。设计令牌的本质是把设计决策转译成一份可以被代码、设计工具、文档同时消费的单一事实源。2.1 令牌不是简单的变量变量解决的是“重复”问题令牌解决的是“一致性 语义化”问题。区别在于变量只在技术层面去重令牌在设计层面定义含义。举个例子你把主色定义为$brand-color: #1476FF这只是一个变量。但如果你定义// 基础令牌primitive tokens --color-blue-60: #1476FF; // 语义令牌semantic tokens --brand-primary: var(--color-blue-60); --brand-primary-hover: var(--color-blue-70); --brand-primary-active: var(--color-blue-80);那它就是一套令牌体系。基础令牌表达“我有哪些颜色可用”语义令牌表达“某个状态下应该用哪个颜色”。组件样式里只允许引用语义令牌不允许直接写#1476FF这样当产品换品牌色时你只需要改基础令牌所有关联组件同步变化。我在 TinyVue 的实际使用里至少遇到两类收益立竿见影的场景按钮的 hover 一直在变一会儿深一会儿浅。有了--brand-primary-hover这类语义令牌设计侧只要给出明确的色阶即可。错误状态的颜色要配合主题调整。错误正红在暗色模式下偏刺眼你可以定义两套基础令牌但语义令牌永远指向当前激活的那套。2.2 一套命名体系如何保证 100 组件不失控TinyVue 组件数量庞大表格、表单、树、分页、对话框……每个组件都有自身的状态色、边框色、背景色、间距值。如果每个组件各自为政根本没法维护。所以它的令牌命名遵循了严格的阶层规则。我的习惯是看三层全局层级影响所有组件的比如--tv-color-primary、--tv-color-success、--tv-font-size-base。组件层级只影响某个组件的比如--tv-button-border-radius、--tv-table-row-height。状态层级针对交互状态比如--tv-button-primary-hover-bg。这样拆的好处是定位一个样式问题非常快。你说“按钮 hover 颜色不对”我直接找--tv-button-*-hover-*这个命名空间下的令牌而不是翻开组件 CSS 到处找。在项目里我强烈建议团队内部也沿用这种命名规则做业务层覆盖。业务项目可以用--app-button-hover-bg但不要跳出层级随意命名否则一年之后你根本不知道哪些令牌是被实际消费的。2.3 令牌层级和注入机制在工程上的选择TinyVue 的令牌消费机制抽象来看就是 CSS 变量注入。组件内部样式中大量使用var()具体的 CSS 变量值在运行时根据当前主题挂在:root上。:root { --tv-color-primary: #1476FF; --tv-font-size-base: 14px; --tv-button-border-radius: 4px; } html[data-themedark] { --tv-color-primary: #3A8BFF; --tv-font-size-base: 14px; --tv-button-border-radius: 4px; }选择 CSS 变量而不是 SCSS 变量是因为 CSS 变量是运行时的SCSS 变量是编译期的。设计系统必须支持运行时切换主题比如用户点了“暗色模式”页面不能重新编译一遍。CSS 变量天然支持即时换肤而且可以作用在局部 DOM 节点上这让“某个容器内部使用不同主题”成为可能。这里有个工程上的取舍值得展开。如果完全依赖 CSS 变量老浏览器兼容性会是个问题比如 IE11 不支持。TinyVue 在工程上做了降级处理构建时生成一套不带var()的静态值作为回退支持var()的环境则优先用变量。这个模式对任何要做设计系统的团队都有参考价值不是追求绝对先进而是保留能力边界。3. 用一个真实需求把设计系统跑起来品牌换肤与暗色模式理论讲太多容易飘我直接带一个真实需求走完整条链路把 TinyVue 默认的蓝色主色换成企业品牌绿同时支持暗色模式切换。这个需求在后台系统里几乎必然出现而且能验证设计系统是否真的“立得住”。3.1 从设计稿到令牌的转换第一步不是写代码而是把设计稿里的颜色“翻译”成令牌。假设设计稿给出品牌绿主色是#00B578hover 是#009C68active 是#008257。我一般先建一个基础令牌文件集中管理原始色阶:root { --brand-green-60: #00B578; --brand-green-70: #009C68; --brand-green-80: #008257; --brand-green-10: #E5FBF4; --brand-green-20: #CCF7E9; }然后把语义令牌映射过去:root { --tv-color-primary: var(--brand-green-60); --tv-color-primary-hover: var(--brand-green-70); --tv-color-primary-active: var(--brand-green-80); --tv-color-primary-light: var(--brand-green-10); --tv-color-primary-lighter: var(--brand-green-20); }注意语义令牌名沿用了 TinyVue 的命名这样组件内部引用var(--tv-color-primary)时会自动吃到新的品牌色不需要去改组件源码。这块的实操心得是不要直接改 TinyVue 的默认令牌文件而是在项目里建立覆盖层。原因是升级组件库时如果你改动过库内部源码升级成本会指数级上升而覆盖层只依赖公共契约版本升级基本无感。3.2 亮色暗色切换的完整链路暗色模式的坑在于组件库默认只定义了一套亮色令牌暗色模式下所有组件要“整体换一套底”。实现链路我给一个可复制的版本。第一步维护两套颜色基础令牌:root { /* 亮色基础色阶 */ --color-bg-page: #F5F6FA; --color-bg-container: #FFFFFF; --color-text-primary: #191919; --color-border-base: #D9DCE3; } html[data-themedark] { /* 暗色基础色阶 */ --color-bg-page: #16181E; --color-bg-container: #1E2026; --color-text-primary: #E8EAEF; --color-border-base: #3B3E45; }第二步让组件语义令牌跟随主题切换:root, html[data-themedark] { --tv-color-primary: var(--brand-green-60); } html[data-themedark] { --tv-color-primary: #4EE0B0; }暗色模式下主色要有更高亮度否则在深色底上对比度不够。这一步是很多团队容易忽略的不是把所有颜色变暗就完事主色、文字色、边框色要单独校准对比度。第三步切换主题的 JS 逻辑// theme.js const THEME_KEY app-theme export function getCurrentTheme() { return localStorage.getItem(THEME_KEY) || light } export function applyTheme(theme) { document.documentElement.setAttribute(data-theme, theme) localStorage.setItem(THEME_KEY, theme) } export function initTheme() { const theme getCurrentTheme() applyTheme(theme) }在应用入口调用initTheme()暗色模式就生效了。因为组件样式全部消费语义令牌切换只发生在根节点性能开销极小。注意如果你有 logo、背景图这类非 CSS 变量素材暗色切换时也要做对应切换。CSS 变量只能管样式管不了图片资源。这块我在项目里是用>.dashboard-dark-area { --tv-color-primary: #4EE0B0; --tv-border-color-base: #2A2D34; --tv-bg-color-container: #111419; }只要这个容器在 DOM 树中位于组件外层内部所有 TinyVue 组件都会自动采用局部令牌不需要给每个组件单独传 prop。这个能力在“全局主题 局部定制”的混搭场景里极其好用。我踩过的一个坑是局部覆盖只写在某个页面的样式文件里没有做统一管理。后来这个区域被复用到别的页面样式却带不过去。建议把这种局部主题封装成一个组合式函数或高阶组件令牌定义和业务逻辑绑定避免散落。4. 组件间的一致性是通过规范化细节锻造出来的间距、排版、圆角与动效主色定了、暗色也切了组件库看起来“像模像样”了。但如果间距、字号、圆角没有体系页面还是会有一种说不上来的乱。这一个小节讲设计系统里最容易忽视、却最影响观感的部分。4.1 尺寸体系是页面的骨架TinyVue 内部有一套尺寸递进关系常见的是 4px 基准。这个基准意味着所有间距、控件高度、内边距都应该是 4 的整数倍。:root { --tv-space-1: 4px; --tv-space-2: 8px; --tv-space-3: 12px; --tv-space-4: 16px; --tv-space-6: 24px; --tv-space-8: 32px; --tv-space-12: 48px; }用 4px 基准的好处是不同组件放在一起时视觉留白天然协调。后台管理系统常见的表单、表格、筛选区组合间距是否统一直接决定页面是“专业”还是“临时拼凑”。开发时我要求团队遵循一个原则能用令牌表达的间距都不要写魔法数值。遇到 20px 这种间距先问自己为什么不是 16 或 24如果两个都不合适那可能不是间距问题而是布局设计需要重新考虑。4.2 排版体系要容纳中文场景TinyVue 的字号定义通常包括font-size-xs / sm / md / lg / xl这些档位对应 12 / 14 / 16 / 18 / 20 这类常用字号。企业级后台和 C 端产品不太一样信息密度高正文一般用 14px标题用 16-20px辅助说明用 12px。中文场景有个容易被忽略的点行高。英文 1.5 倍行高放在中文里版面会显得稀松且参差。我的做法是把行高也令牌化:root { --tv-font-size-base: 14px; --tv-line-height-base: 1.6; --tv-font-size-title: 18px; --tv-line-height-title: 1.4; }TinyVue 的组件内部已经做了基础排版但业务自定义页面时要主动使用同一套字阶否则会出现组件里是 14px旁边业务模块写死 15px 的细微不协调。这种问题不仔细看很难发现但用户能感受到。4.3 圆角与阴影的克制设计系统里的圆角不是“喜欢圆还是方”的审美问题而是层级语言。TinyVue 的圆角令牌我建议这样理解小圆角用在输入框、按钮这类高频交互控件克制、专业。中圆角用在卡片、弹窗这类容器组件亲和但不失严谨。大圆角用在标签、图片预览等展示类元素。阴影同理。它表达层级的深浅弹窗的阴影应该比下拉面板深悬浮卡片应该有明显的提升感。如果所有组件阴影都一样页面层次感就没了。这部分是设计系统里最需要“定规则守住规则”的环节。我会在项目验收标准里加入一条新写的业务样式圆角、阴影、间距必须从令牌取禁止新造数值。不这样卡半年之后设计系统就退化成“只有主色令牌”的孤岛。4.4 动效是最后一块拼图很多人做设计系统把动效漏掉结果弹窗出现方式五花八门有的一闪而出有的慢吞吞位移感觉像两个团队做的。动效规范不需要复杂但必须有。我推荐把动效拆成三件事时长、缓动函数、动画范围。:root { --tv-transition-duration-fast: 100ms; --tv-transition-duration-base: 240ms; --tv-transition-duration-slow: 400ms; --tv-transition-easing-base: cubic-bezier(0.25, 0.1, 0.25, 1); --tv-transition-easing-enter: cubic-bezier(0, 0, 0.2, 1); --tv-transition-easing-leave: cubic-bezier(0.4, 0, 1, 1); }进入动画用更轻快的缓存函数退出动画用稍重的曲线视觉上会更有质感。TinyVue 组件自身动效已经做了但业务自定义容器、路由切换、数字滚动这些场景团队内部也要按同一套令牌走。动效一旦规范产品的精致度会有肉眼可见的提升。5. 工程化接入是设计系统落地的最后一公里设计系统不是一套 CSS 变量就完了它要运转起来必须有工程化配套。TinyVue 的设计系统实现里工程接入是决定体验的重头戏。5.1 从“引库色”到“可配置色”的升级大多数项目的接入方式是npm i opentiny/vue全局注册然后用默认样式。这种方式的本质是把组件库当成不可变零件。要落地设计系统就要升级为“可配置”接入。我实践下来的推荐结构project/ ├── src/ │ ├── designs/ │ │ ├── tokens/ │ │ │ ├── base.css # 基础令牌 │ │ │ ├── semantic.css # 语义令牌 │ │ │ └── themes/ │ │ │ ├── light.css │ │ │ └── dark.css │ │ ├── mixins.scss │ │ └── components/ │ │ ├── button.css │ │ └── table.css │ ├── utils/ │ │ └── theme.js │ └── main.tsdesigns目录成为设计系统的单一事实源。业务代码只从里面取令牌组件库只是令牌的消费者。这个结构的好处是设计系统可以独立评审、独立测试、甚至独立发布成 npm 包给多项目共享。5.2 兼容 Vue 2 / Vue 3 的接入细节TinyVue 的一个卖点是支持 Vue 2 和 Vue 3。对设计系统的接入来说两者差异主要体现在组件注册方式上令牌体系可以完全复用。这正是因为 CSS 变量与框架无关。无论你是 Vue 2 的 Options API 项目还是 Vue 3 的 Composition API 项目theme.js里那段document.documentElement.setAttribute的逻辑都能直接运行。设计系统一旦沉淀为纯 CSS 变量 配置文件它就天然获得了跨框架能力。甚至你以后要把部分页面迁到 React、跨端小程序样式资产也能复用。这里有个实际建议如果你的组织里同时存在 Vue 2 和 Vue 3 的老项目尽量让设计系统包独立于组件库发布。这样两套框架的项目依赖同一个设计系统包而不是各自 copy 一份避免改主题时出现两处不一致。5.3 主题在 CI/CD 中的持续交付设计系统的维护不能只靠“手动改 CSS”要纳入持续交付。我这里分享一套低成本但有效的方案。设计令牌文件在构建时参与编译构建产物会生成多套主题 CSS。前端工程里通过一个简单的环境变量控制默认主题# .env.production VITE_DEFAULT_THEMElightCI 流程里增加一个主题校验任务检查新提交的样式是否使用了未定义的数值。这个校验可以用 Stylelint 实现规则就是“不允许出现不在令牌库里的颜色、间距、字号”。团队只要把这条校验介入pre-commit和 CI设计系统就不会随着迭代腐烂。说实话很多团队的设计系统是“搭好了但没人维护”最终活不过半年。根源不是设计能力而是工程上缺少“守卫者”。在 CI 里卡一道检查比任何文化宣导都有效。6. 落地过程中的踩坑记录与修复经验最后这部分我把这几年在不同项目里实践 TinyVue 设计系统时踩过的坑整理出来。这些坑都是真实发生过的写出来帮大家省点时间。6.1 全局样式污染是最早出现的坑刚接入设计系统时我习惯把所有令牌直接挂在:root下。结果在多个项目复用时发现某个项目的全局样式覆盖导致 TinyVue 组件的盒子模型异常排查了很久才发现是业务侧针对div写了个全局box-sizing覆盖。解法有两层第一给设计令牌的容器加命名空间不全部挂在:root上而是挂在应用根节点或指定容器上。第二在样式文件入口做一个“隔离区”确保基础样式只作用于自己的组件不向外部泄漏。#app { box-sizing: border-box; --tv-color-primary: var(--brand-green-60); }注意box-sizing要配合继承属性处理好否则组件内的元素还是会被全局的*选择器干扰。这一点在团队协作项目里尤其要提前约定。6.2 主题切换闪现一个“错误颜色帧”暗色模式上线后用户反馈说每次刷新页面先是亮色闪一下再变暗色。原因是我把主题初始化放在了组件挂载后执行CSS 加载和 JS 执行之间有一段空白期。修复方式是“入口前置”。在 HTML 里在应用脚本加载之前就先执行主题设置// index.html 内联脚本 ;(function () { var theme localStorage.getItem(app-theme) || light document.documentElement.setAttribute(data-theme, theme) })()这样首屏渲染时 DOM 节点已经带上了正确的主题属性不会闪错帧。如果你用了 SSR也要在服务端渲染阶段就输出正确的>
返回列表