文章目录前言什么是 Design Token在 HarmonyOS 中实现 Token 系统颜色 Token间距 Token字体 Token在组件中使用 Token资源文件配合 Token一个踩坑经验前言你有没有经历过这种场面设计师说这个蓝色不对应该是 #0A59F7 不是 #1060FF然后你全局搜了一下代码发现项目里用了 17 种不同的蓝色。我当时看到这个搜索结果差点没绷住。后来我搞了一套 Design Token 系统这类问题基本上就消失了。今天聊聊怎么在 HarmonyOS 里落地。什么是 Design TokenDesign Token 说白了就是把设计决策变成变量。颜色不直接写#0A59F7而是写colorPrimary间距不直接写16px而是写spacingMd。这么做的好处显而易见——设计师想换主色调改一个 Token 的值就行不用翻几百个文件。而且 Token 天然带有语义colorError比#FF3B30好理解多了。Token 一般分三层全局 Token原始值、语义 Token带用途的值、组件 Token组件专属的值。我们这次主要做语义层够用且不会过度设计。在 HarmonyOS 中实现 Token 系统实现思路有三层资源文件定义原始值、常量类做语义映射、组件里直接引用常量类。这套方案在亮色暗色切换时特别好用——只需要切换常量类指向的资源就行。颜色 Token先定义资源文件resources/base/element/color.json{color:[{name:blue_primary,value:#0A59F7},{name:blue_secondary,value:#3B82F6},{name:red_error,value:#FF3B30},{name:green_success,value:#34C759},{name:orange_warning,value:#FF9500},{name:gray_50,value:#FAFAFA},{name:gray_100,value:#F5F5F5},{name:gray_200,value:#EEEEEE},{name:gray_400,value:#BDBDBD},{name:gray_600,value:#757575},{name:gray_800,value:#424242},{name:gray_900,value:#212121},{name:white,value:#FFFFFF},{name:black,value:#000000}]}然后建一个ColorTokens.ets做语义化映射// tokens/ColorTokens.etsexportclassColorTokens{// 品牌色staticreadonlyprimary:string#0A59F7staticreadonlyprimaryLight:string#3B82F6staticreadonlyprimaryDark:string#0040C0// 语义色staticreadonlysuccess:string#34C759staticreadonlywarning:string#FF9500staticreadonlyerror:string#FF3B30staticreadonlyinfo:string#0A59F7// 表面色staticreadonlysurface:string#FFFFFFstaticreadonlysurfaceVariant:string#F5F5F5staticreadonlybackground:string#FAFAFA// 文本色staticreadonlytextPrimary:string#212121staticreadonlytextSecondary:string#757575staticreadonlytextDisabled:string#BDBDBDstaticreadonlytextOnPrimary:string#FFFFFF// 边框色staticreadonlyborder:string#EEEEEEstaticreadonlydivider:string#EEEEEE}间距 Token间距用阶梯式的设计4px 为基础单位// tokens/SpacingTokens.etsexportclassSpacingTokens{// 基础间距4px 阶梯staticreadonlyxs:number4// 极小间距staticreadonlysm:number8// 小间距staticreadonlymd:number12// 中等间距staticreadonlylg:number16// 大间距staticreadonlyxl:number24// 超大间距staticreadonlyxxl:number32// 特大间距staticreadonlyxxxl:number48// 巨大间距// 组件内边距语义化staticreadonlycomponentPaddingH:number16// 水平内边距staticreadonlycomponentPaddingV:number12// 垂直内边距staticreadonlycardPadding:number16// 卡片内边距staticreadonlylistItemPadding:number12// 列表项内边距// 页面布局staticreadonlypageMargin:number16// 页面边距staticreadonlysectionGap:number24// 区块间距staticreadonlyitemGap:number12// 元素间距}字体 Token字体需要管理字号、字重和行高// tokens/FontTokens.etsexportclassFontTokens{// 字号staticreadonlysizeXs:number11staticreadonlysizeSm:number13staticreadonlysizeMd:number15staticreadonlysizeLg:number17staticreadonlysizeXl:number20staticreadonlysizeXxl:number24staticreadonlysizeTitle:number28// 字重staticreadonlyweightRegular:FontWeightFontWeight.NormalstaticreadonlyweightMedium:FontWeightFontWeight.MediumstaticreadonlyweightBold:FontWeightFontWeight.Bold// 行高倍数基于字号staticreadonlylineHeightTight:number1.2staticreadonlylineHeightNormal:number1.5staticreadonlylineHeightLoose:number1.8}// 字体预设组合字号 字重 行高exportinterfaceFontPreset{size:numberweight:FontWeight lineHeight:number}exportclassFontPresets{staticreadonlyheadline:FontPreset{size:FontTokens.sizeXxl,weight:FontTokens.weightBold,lineHeight:FontTokens.sizeXxl*FontTokens.lineHeightTight}staticreadonlytitle:FontPreset{size:FontTokens.sizeLg,weight:FontTokens.weightBold,lineHeight:FontTokens.sizeLg*FontTokens.lineHeightTight}staticreadonlybody:FontPreset{size:FontTokens.sizeMd,weight:FontTokens.weightRegular,lineHeight:FontTokens.sizeMd*FontTokens.lineHeightNormal}staticreadonlycaption:FontPreset{size:FontTokens.sizeSm,weight:FontTokens.weightRegular,lineHeight:FontTokens.sizeSm*FontTokens.lineHeightNormal}staticreadonlylabel:FontPreset{size:FontTokens.sizeSm,weight:FontTokens.weightMedium,lineHeight:FontTokens.sizeSm*FontTokens.lineHeightTight}}在组件中使用 TokenToken 定义好了关键是怎么在组件里用起来。来看一个实际的卡片组件import{ColorTokens}from../tokens/ColorTokensimport{SpacingTokens}from../tokens/SpacingTokensimport{FontPresets}from../tokens/FontTokensComponentstruct ProductCard{Proptitle:stringPropprice:stringPropdesc:stringbuild(){Column(){// 标题Text(this.title).fontSize(FontPresets.title.size).fontWeight(FontPresets.title.weight).fontColor(ColorTokens.textPrimary).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})// 价格Text(this.price).fontSize(FontTokens.sizeXl).fontWeight(FontTokens.weightBold).fontColor(ColorTokens.error).margin({top:SpacingTokens.sm})// 描述Text(this.desc).fontSize(FontPresets.caption.size).fontColor(ColorTokens.textSecondary).margin({top:SpacingTokens.xs}).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})}.width(100%).padding(SpacingTokens.cardPadding).borderRadius(12).backgroundColor(ColorTokens.surface).shadow({radius:8,color:#1A000000,offsetX:0,offsetY:2})}}看到没有整个组件里没有一个硬编码的颜色值shadow 的半透明除外全是 Token 引用。设计师说把主色调从蓝色改成紫色只需要改ColorTokens.primary的值全项目一键生效。资源文件配合 Token有些场景下用资源文件引用比硬编码更好——比如支持暗色模式时资源文件可以根据系统配置自动切换。这时候可以这样写// 资源文件方式引用颜色Text(标题).fontColor($r(app.color.text_primary))// 自动适配亮暗色// 常量方式引用颜色Text(标题).fontColor(ColorTokens.textPrimary)// 固定颜色值两种方式各有适用场景。如果组件不需要支持主题切换用常量方式更直接如果需要跟随系统主题变化用资源文件更省心。实际项目中我一般混着用——核心品牌色走常量背景色和文本色走资源文件。一个踩坑经验间距 Token 千万别搞得太细。我最开始定义了 2px/4px/6px/8px/10px/12px/14px/16px 八档结果团队成员根本分不清 sm 和 md 到底哪个大后来直接砍到 4/8/12/16/24 五档世界清静了。Token 系统这事儿核心原则是少即是多。颜色不超过 15 个语义色间距不超过 6 档字号不超过 7 级。约束越明确团队用起来的决策成本就越低。这也是为什么我在 Token 类里全用了readonly——就是不让人随便加新值想加先跟设计师商量。