新特性:设计系统(Design System)构建 —— 建立团队的 Token 变量库
文章目录
- 前言
- 一、为什么需要 Token 变量库?
- 1.1 硬编码的四大问题
- 1.2 Token 解决什么?
- 1.3 与现有 DSTokens.ets 的关系
- 二、HarmonyOS 资源体系速览
- 2.1 目录结构
- 2.2 引用语法
- 2.3 单位约定
- 三、命名规范:语义化,而非描述色值
- 3.1 推荐格式
- 3.2 好的命名 vs 坏的命名
- 3.3 完整 Token 清单(Demo)
- 四、定义颜色 Token
- 4.1 base/element/color.json
- 4.2 dark/element/color.json
- 4.3 对比一览
- 五、定义间距与圆角 Token
- 5.1 float.json 完整定义
- 5.2 4vp 网格原则
- 5.3 引用示例
- 六、在组件中消费 Token
- 6.1 迁移前 vs 迁移后
- 6.2 Demo 组件画廊
- 6.3 色板数据驱动
- 七、暗色模式:零代码适配
- 7.1 原理
- 7.2 开发注意事项
- 7.3 容器色的暗色处理
- 八、团队落地工作流
- 8.1 设计 → 开发协作
- 8.2 渐进式迁移策略
- 8.3 与 @Styles / @Extend 结合
- 8.4 Code Review 检查项
- 九、常见误区
- 9.1 「Token 就是常量类」
- 9.2 「暗色模式在组件里判断」
- 9.3 「Token 名用色值描述」
- 9.4 「间距随意定」
- 9.5 「只定义亮色」
- 十、Token 规模参考
- 十一、总结
前言
打开一个三个月前写的页面,你可能会看到:
.fontColor('#6B7280').padding(12).borderRadius(10).backgroundColor('#2563EB')当时觉得「就几个魔法数字,记得住」。三个月后品牌色从#2563EB换成#0A59F7,设计师要求暗色模式适配,间距从 12 统一调到 16——你需要在几十个文件里全文搜索替换,漏一处就是视觉不一致。
这就是没有设计系统的代价。
Design System(设计系统)的核心产物之一,是Token 变量库——把颜色、间距、圆角、字号等视觉属性抽象成语义化变量,集中定义在resources目录,组件通过$r('app.color.ds_text_primary')引用,而非硬编码。
HarmonyOS 的资源体系(base+dark双目录)天然支持主题切换。本文结合项目 Demo,从 Token 理念、资源定义、引用方式、暗色适配到组件落地,做一次完整实战解析。读完后,你应该能回答:
- 什么是语义化 Token,和直接写色值有什么区别?
- 颜色、间距、圆角分别放在哪个 JSON 文件?
$r()如何在 ArkUI 组件中使用?- 如何实现「改一处 resources,全 App 换肤」?
一、为什么需要 Token 变量库?
1.1 硬编码的四大问题
| 问题 | 后果 |
|---|---|
| 散落各处 | 同一个#2563EB出现 80 次,改品牌色要改 80 处 |
| 语义缺失 | #6B7280是标题色还是提示色?新人无法判断 |
| 无法换肤 | 暗色模式需在每个组件写if (isDark) |
| 协作断裂 | 设计师给 Figma Token,开发手写 hex,两边对不上 |
1.2 Token 解决什么?
Token 是设计决策的命名快照:
设计师说:「主文本用 text-primary」 开发写: .fontColor($r('app.color.ds_text_primary')) 资源文件:ds_text_primary = #111827(亮色)/ #F9FAFB(暗色)改色?只改color.json。加暗色?只加dark/color.json同名条目。组件代码一行不动。
1.3 与现有 DSTokens.ets 的关系
项目中designsystem/tokens/DSTokens.ets用 TypeScript 常量定义 Token:
exportclassDSColor{staticreadonlybrandPrimary='#0A59F7'}这适合纯逻辑层或跨 module 编译期引用,但不支持系统级主题切换——暗色模式来了要手动判断。本文推荐的resources Token则由框架在运行时按主题自动选取,是 HarmonyOS 官方推荐的视觉变量管理方式。两者可并存:resources 管颜色/尺寸,ETS 类管枚举和组件变体。
二、HarmonyOS 资源体系速览
2.1 目录结构
entry/src/main/resources/ ├── base/ ← 默认(亮色)资源 │ └── element/ │ ├── color.json ← 颜色 Token │ ├── float.json ← 间距/圆角/字号 Token │ └── string.json └── dark/ ← 暗色覆写 └── element/ └── color.json ← 同名 Token,暗色值系统根据当前主题自动选择base或dark目录下的同名资源。组件始终引用$r('app.color.ds_text_primary'),无需关心当前是亮是暗。
2.2 引用语法
// 颜色.fontColor($r('app.color.ds_text_primary')).backgroundColor($r('app.color.ds_bg_surface'))// 间距 / 圆角 / 字号.padding($r('app.float.ds_space_lg')).borderRadius($r('app.float.ds_radius_md')).fontSize($r('app.float.ds_font_body1'))格式:$r('app.<type>.<name>'),其中type对应 JSON 文件名(color、float、string)。
2.3 单位约定
| 文件 | 常用单位 | 用途 |
|---|---|---|
color.json | #RRGGBB | 颜色值 |
float.json | vp | 间距、圆角(随屏幕密度缩放) |
float.json | fp | 字号(随系统字体缩放) |
Demo 间距用vp,字号用fp,符合 HarmonyOS 适配规范。
三、命名规范:语义化,而非描述色值
3.1 推荐格式
ds_<category>_<role>| 部分 | 含义 | 示例 |
|---|---|---|
ds_ | 设计系统前缀,避免与业务资源冲突 | — |
category | 类别 | text、bg、space、radius |
role | 语义角色 | primary、secondary、md、lg |
3.2 好的命名 vs 坏的命名
| ✅ 语义化 | ❌ 描述性 |
|---|---|
ds_text_primary | ds_gray_900 |
ds_brand_primary | ds_blue_500 |
ds_bg_surface | ds_white |
ds_space_md | ds_twelve_vp |
语义化命名的好处:品牌色从蓝变绿,Token 名ds_brand_primary不用改,只改值。ds_blue_500则名实不符,制造混乱。
3.3 完整 Token 清单(Demo)
颜色(17 个):
| 类别 | Token |
|---|---|
| 品牌/语义 | ds_brand_primary,ds_brand_primary_container,ds_success,ds_success_container,ds_warning,ds_warning_container,ds_danger,ds_danger_container |
| 文本 | ds_text_primary,ds_text_secondary,ds_text_tertiary,ds_text_inverse |
| 表面 | ds_bg_page,ds_bg_surface,ds_bg_elevated,ds_border_default,ds_border_strong,ds_divider |
间距(6 级,4vp 网格):
| Token | 值 |
|---|---|
ds_space_xs | 4vp |
ds_space_sm | 8vp |
ds_space_md | 12vp |
ds_space_lg | 16vp |
ds_space_xl | 24vp |
ds_space_xxl | 32vp |
圆角(5 级):
| Token | 值 | 典型场景 |
|---|---|---|
ds_radius_sm | 6vp | 小标签、徽标 |
ds_radius_md | 10vp | 按钮、输入框 |
ds_radius_lg | 16vp | 卡片、面板 |
ds_radius_xl | 24vp | 底部弹窗 |
ds_radius_full | 999vp | 胶囊、圆形 |
字阶(6 级):
| Token | 值 |
|---|---|
ds_font_caption | 10fp |
ds_font_body2 | 12fp |
ds_font_body1 | 14fp |
ds_font_subtitle | 16fp |
ds_font_title | 20fp |
ds_font_headline | 24fp |
四、定义颜色 Token
4.1 base/element/color.json
{"color":[{"name":"ds_brand_primary","value":"#2563EB"},{"name":"ds_text_primary","value":"#111827"},{"name":"ds_bg_page","value":"#F9FAFB"},{"name":"ds_bg_surface","value":"#FFFFFF"}]}容器色(*_container)是 Material Design 3 风格:主色的低饱和背景,用于标签、选中态、提示区块。Demo 中ds_brand_primary_container = #EFF6FF,与ds_brand_primary = #2563EB配对使用。
4.2 dark/element/color.json
暗色目录中定义同名 Token,赋予不同值:
{"color":[{"name":"ds_text_primary","value":"#F9FAFB"},{"name":"ds_bg_page","value":"#111827"},{"name":"ds_bg_surface","value":"#1F2937"},{"name":"ds_brand_primary","value":"#60A5FA"}]}注意暗色模式下品牌色通常提高亮度(#2563EB→#60A5FA),而非简单反转,以保证对比度和可读性。
4.3 对比一览
| Token | 亮色 (base) | 暗色 (dark) |
|---|---|---|
ds_text_primary | #111827 | #F9FAFB |
ds_text_secondary | #6B7280 | #9CA3AF |
ds_bg_page | #F9FAFB | #111827 |
ds_bg_surface | #FFFFFF | #1F2937 |
ds_brand_primary | #2563EB | #60A5FA |
Demo 在「色彩 Token」场景底部有暗色模式提示:切换系统暗色后,所有引用$r('app.color.ds_*')的组件自动更新,无需任何代码修改。
五、定义间距与圆角 Token
5.1 float.json 完整定义
{"float":[{"name":"ds_space_xs","value":"4vp"},{"name":"ds_space_sm","value":"8vp"},{"name":"ds_space_md","value":"12vp"},{"name":"ds_space_lg","value":"16vp"},{"name":"ds_space_xl","value":"24vp"},{"name":"ds_space_xxl","value":"32vp"},{"name":"ds_radius_sm","value":"6vp"},{"name":"ds_radius_md","value":"10vp"},{"name":"ds_radius_lg","value":"16vp"},{"name":"ds_radius_xl","value":"24vp"},{"name":"ds_radius_full","value":"999vp"}]}5.2 4vp 网格原则
间距刻度以4vp 为基数:4、8、12、16、24、32。这保证:
- 所有间距是 4 的倍数,视觉节奏统一
- 设计师在 Figma 中用 8px 网格,开发用 8vp,心算对齐
- 避免
padding(13)这种「拍脑袋」数值
Demo「间距 Token」场景用色条直观展示各级长度,并附有 padding 实战卡片。
5.3 引用示例
// 统一内边距.padding($r('app.float.ds_space_lg'))// 方向性内边距.padding({left:$r('app.float.ds_space_md'),right:$r('app.float.ds_space_md'),top:$r('app.float.ds_space_sm'),bottom:$r('app.float.ds_space_sm')})// 圆角.borderRadius($r('app.float.ds_radius_lg'))// Column 间距Column({space:12})// 注意:space 参数目前只接受 number,暂不能用 Resource最后一个注意点:Column({ space })等布局参数的space在某些 API 版本只接受number,不支持$r()。这类场景可暂时保留数字,或在@Builder封装层统一处理。颜色、padding、borderRadius、fontSize 等属性已广泛支持 Resource 类型。
六、在组件中消费 Token
6.1 迁移前 vs 迁移后
// ❌ 硬编码Text('标题').fontSize(22).fontColor('#111827')Column().padding(16).backgroundColor('#FFFFFF').borderRadius(12)// ✅ Token 驱动Text('标题').fontSize($r('app.float.ds_font_headline')).fontColor($r('app.color.ds_text_primary'))Column().padding($r('app.float.ds_space_lg')).backgroundColor($r('app.color.ds_bg_surface')).borderRadius($r('app.float.ds_radius_lg'))6.2 Demo 组件画廊
「组件应用」场景展示四类组件,全部零硬编码:
按钮:
Text('主按钮').fontColor($r('app.color.ds_text_inverse')).backgroundColor($r('app.color.ds_brand_primary')).borderRadius($r('app.float.ds_radius_md')).padding({left:$r('app.float.ds_space_md'),right:$r('app.float.ds_space_md'),top:$r('app.float.ds_space_sm'),bottom:$r('app.float.ds_space_sm')})卡片:
Column({space:8}).padding($r('app.float.ds_space_lg')).backgroundColor($r('app.color.ds_bg_surface')).borderRadius($r('app.float.ds_radius_lg')).border({width:1,color:$r('app.color.ds_border_default')})标签(品牌/成功/警告/危险四色):
Text('成功').fontColor($r('app.color.ds_success')).backgroundColor($r('app.color.ds_success_container')).borderRadius($r('app.float.ds_radius_full'))语义色 + 容器色配对,是设计系统的标准做法:文字用高饱和色,背景用低饱和同色系。
6.3 色板数据驱动
Demo 将 Token 组织为可遍历数组,便于文档化展示:
interfaceColorSwatch{id:stringtoken:stringlabel:stringcolor:ResourceColor}constBRAND_SWATCHES:ColorSwatch[]=[{id:'c1',token:'ds_brand_primary',label:'主色',color:$r('app.color.ds_brand_primary')}]$r()返回值可直接存入常量数组,ForEach 渲染色板——这也是团队内部 Token 文档页的常见实现。
七、暗色模式:零代码适配
7.1 原理
组件引用 $r('app.color.ds_bg_page') │ ▼ 系统判断当前主题 ╱ ╲ base/color.json dark/color.json #F9FAFB #111827组件层不出现任何isDarkMode判断。资源框架在运行时解析正确的值。
7.2 开发注意事项
- 所有颜色必须走 Token— 一个
#FFFFFF漏网就会暗色下刺眼 - 暗色值不是简单反转— 品牌色提亮、边框色减淡,需设计稿校准
- 对比度—
ds_text_primary与ds_bg_surface在两端都需满足 WCAG AA(对比度 ≥ 4.5:1) - 测试方法— 系统设置切换暗色,或在 DevEco 预览器选择 dark 主题
7.3 容器色的暗色处理
亮色:ds_success_container = #ECFDF5(浅绿底)
暗色:ds_success_container = #064E3B(深绿底)
容器色在暗色模式下反而加深,避免大面积浅色刺眼,同时保持语义色相。
八、团队落地工作流
8.1 设计 → 开发协作
Figma Design Tokens │ ▼ 导出/对照 resources/color.json + float.json │ ▼ 引用 $r('app.color.ds_*') 全 App 消费 │ ▼ 变更 只改 JSON → 全 App 生效建议在设计稿中直接使用与代码一致的 Token 名(ds_text_primary),减少翻译损耗。
8.2 渐进式迁移策略
不必一次性重构全 App。推荐顺序:
| 阶段 | 范围 | 做法 |
|---|---|---|
| Phase 1 | 新页面 | 强制使用 Token,禁止硬编码 |
| Phase 2 | 核心组件 | Button、Card、Input 封装 Token |
| Phase 3 | 存量页面 | 按模块逐步替换#hex→$r() |
| Phase 4 | 审查 | CI 脚本扫描硬编码色值 |
8.3 与 @Styles / @Extend 结合
对于重复样式组合,可封装:
@Extend(Text)functiondsBody1(){.fontSize($r('app.float.ds_font_body1')).fontColor($r('app.color.ds_text_primary'))}@Extend(Column)functiondsCard(){.padding($r('app.float.ds_space_lg')).backgroundColor($r('app.color.ds_bg_surface')).borderRadius($r('app.float.ds_radius_lg')).border({width:1,color:$r('app.color.ds_border_default')})}// 使用Text('正文').dsBody1()Column().dsCard()Token 定义「值」,@Extend 定义「组合」—— 两层分工,避免每个组件重复写五六个属性。
8.4 Code Review 检查项
- 新增颜色是否定义为 Token?
- 组件是否使用
$r()而非 hex? - 暗色
color.json是否有对应覆写? - 间距是否为 4vp 网格倍数?
- 命名是否符合
ds_<category>_<role>规范?
九、常见误区
9.1 「Token 就是常量类」
DSColor.brandPrimary = '#2563EB'在 TypeScript 里编译期固定,无法响应系统主题切换。resources Token 才是运行时主题感知的正解。常量类可作为补充,不应替代 resources。
9.2 「暗色模式在组件里判断」
// 不推荐.fontColor(this.isDark?'#F9FAFB':'#111827')// 推荐.fontColor($r('app.color.ds_text_primary'))前者每处都要写判断,后者集中管理。
9.3 「Token 名用色值描述」
ds_blue_500在品牌换色后名不副实。用语义名ds_brand_primary,值可以随便换。
9.4 「间距随意定」
padding(15)、margin(13)破坏网格节奏。Stick to the scale:xs/sm/md/lg/xl/xxl。
9.5 「只定义亮色」
暗色适配不是可选项。从第一天就在dark/color.json维护同名 Token,成本远低于事后补。
十、Token 规模参考
Demo 当前规模:
| 类型 | 数量 | 文件 |
|---|---|---|
| 颜色 | 17 | color.json × 2 |
| 间距 | 6 | float.json |
| 圆角 | 5 | float.json |
| 字阶 | 6 | float.json |
| 合计 | 34 | — |
一个中等复杂度 App,颜色 Token 通常在 20–40 个,间距/圆角/字号各 5–8 个。不宜贪多——每个 Token 都应有明确使用场景,否则团队记不住。
扩展时优先加语义色(如ds_info、ds_link),而非加第 17 个灰色变体。
十一、总结
设计系统 Token 变量库的本质,是把视觉决策从组件代码中抽离到 resources:
定义一次 → 处处引用 → 改一处全局生效HarmonyOS 落地三件套:
| 文件 | 放什么 | 引用方式 |
|---|---|---|
base/element/color.json | 语义化颜色 | $r('app.color.ds_*') |
dark/element/color.json | 暗色覆写 | 同名,自动切换 |
base/element/float.json | 间距/圆角/字号 | $r('app.float.ds_*') |
关键原则:
- 语义命名—
ds_text_primary,不是ds_gray_900 - 4vp 网格— 间距刻度可预期
- 零硬编码— 组件不出现
#hex魔法数字 - 双目录主题— base + dark 同名 Token
- 渐进迁移— 新代码强制 Token,存量逐步替换
本文 Demo 在四个场景中完整展示了色板、间距刻度、圆角阶梯和组件画廊。建议你在真机上运行后,到系统设置切换暗色模式——观察ds_bg_page从浅灰变为深蓝灰,而组件代码一行未改。这就是 Token 变量库的价值。
设计系统不是设计师的文档摆设,而是开发团队的视觉基础设施。从第一个ds_brand_primary开始,你的 App 就拥有了统一的语言。
相关文件:
- Demo 页面:
entry/src/main/ets/pages/Index.ets - 亮色 Token:
entry/src/main/resources/base/element/color.json、float.json - 暗色 Token:
entry/src/main/resources/dark/element/color.json
编译命令:
node/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js assembleHap-pproduct=default-pbuildMode=debug --no-daemon