在之前的文章中,我介绍了Design Token 相关的基本概念。本文将介绍如何使用现有的工具来管理 Design Token。
什么是Style Dictionary
社区上处理和解析Design Token的工具很多,这有一个可参考的目录。比较受欢迎的有 Style Dictionary、Theo 等。我选择 Style Dictionary,因为社区受关注度更高,更新也比较及时,最近正在计划做4.0版本。
Style Dictionary 是一个构建系统。使用它可以让你一次性定义风格,供任何平台或语言使用。在一个地方可以创建和编辑你的样式,通过一个命令就可以把这些规则导出到你需要的所有地方,iOS、Android、CSS、JS、HTML、Sketch 文件、样式文档,或者任何你能想得到的地方。它可以通过npm作为CLI使用,也可以像普通的Node.js模块一样使用。
简单案例,快速上手
为了帮助开发者快速上手,官方很贴心地提供了一些实例。让我们来看一下简单的实例,从中了解Style Dictionary 到底做了什么事情。随便找个目录,执行以下命令。
$ mkdir MyStyleD
$ cd MyStyleD
$ style-dictionary init basic
这个命令先将仓库中的实例文件复制到本地目录,然后执行 style-dictionary build
,生成构建的产出物。不出意外的话,你会在你的控制台中看到这些输出:
Copying starter files...
Source style dictionary starter files created!
Running `style-dictionary build` for the first time to generate build artifacts.
scss
✔︎ build/scss/_variables.scss
android
✔︎ build/android/font_dimens.xml
✔︎ build/android/colors.xml
compose
✔︎ build/compose/StyleDictionaryColor.kt
✔︎ build/compose/StyleDictionarySize.kt
ios
✔︎ build/ios/StyleDictionaryColor.h
✔︎ build/ios/StyleDictionaryColor.m
✔︎ build/ios/StyleDictionarySize.h
✔︎ build/ios/StyleDictionarySize.m
ios-swift
✔︎ build/ios-swift/StyleDictionary+Class.swift
✔︎ build/ios-swift/StyleDictionary+Enum.swift
✔︎ build/ios-swift/StyleDictionary+Struct.swift
这意味着你已经成功运行了这个实际的例子。回头看看当前你操作的文件目录,它应该长这样:
├── README.md
├── config.json
├── tokens/
│ ├── color/
│ ├── base.json
│ ├── font.json
│ ├── size/
│ ├── font.json
├── build/
│ ├── android/
│ ├── font_dimens.xml
│ ├── colors.xml
│ ├── compose/
│ ├── StyleDictionaryColor.kt
│ ├── StyleDictionarySize.kt
│ ├── scss/
│ ├── _variables.scss
│ ├── ios/
│ ├── StyleDictionaryColor.h
│ ├── StyleDictionaryColor.m
│ ├── StyleDictionarySize.h
│ ├── StyleDictionarySize.m
│ ├── ios-swift/
│ ├── StyleDictionary.swift
│ ├── StyleDictionaryColor.swift
│ ├── StyleDictionarySize.swift
Style Dictionary 由配置驱动,必须包含一份config.json和配置中引用的design token对应的文件。
-
config.json:style dictionary 依赖的配置。告诉 style dictionary去哪里找design tokens,以及如何转换和格式化输出文件。
{ "source": ["tokens/**/*.json"], "platforms": { "scss": { "transformGroup": "scss", "prefix": "sd", "buildPath": "build/scss/", "files": [{ "destination": "_variables.scss", "format": "scss/variables" }], "actions": ["copy_assets"] }, "android": { "transforms": [ "attribute/cti", "name/cti/snake", "color/hex", "size/remToSp", "size/remToDp" ], "buildPath": "build/android/src/main/res/values/", "files": [{ "destination": "style_dictionary_colors.xml", "format": "android/colors" }] } } }
-
design tokens: 定义了 design token的一系列JSON或者JavaScript Module文件。会在config.json 中的source属性中使用。
{ "size": { "font": { "small": { "value": "10px" }, "medium": { "value": "16px" }, "large": { "value": "24px" }, "xl": { "value": "34px" }, "xxl": { "value": "46px" }, "base": { "value": "{size.font.medium.value}", "attributes": { "comment": "All about that base" } }, "heading": { "1": { "value": "{size.font.xxl.value}" }, "2": { "value": "{size.font.xl.value}" } } } } }
Style Dictionary 的架构
为了更好地理解style dictionary的能力和工作原理,让我们来看看它的架构设计。下面是官方给到的架构图。
一图胜千言,这张图已经很直观地展示其工作原理。
-
第一步:解析配置文件。
-
第二步:找到所有 token 文件。config中的
include
和source
组合决定了搜索查找的范围。 -
第三步:将所有的token文件进行一个深度合并。相同的结构将会覆盖。
-
第四步:遍历config中定义所有platform,执行一下操作:
-
执行 token转换,提取token中的value。这会深度遍历每一个对象,直到找到
value
这个key。 -
解析别名和引用。找到
value
之后,如果其值是别名或者引用,比如"{size.font.base.value}”
,就用转换后的值替换之。 -
根据每一个platform配置中的format格式,输出不同的文件。
-
在输出文件之后,还可以执行自定义的Actions。
-
当上述四个步骤都成功执行并结束后,你就能得到你想要的输出产物了。
与设计师协作
在浅谈Design Token中我曾经提到过,设计师可以通过 Figma 等设计工具,以文件的形式将 design token 提供给开发人员。现在,身为开发人员的我们,可以使用 Style Dictionary 对token进行处理,输出我们想要的文件。
我们将所有的tokens文件托管到内部的gitlab仓库中。设计师定义好token之后,将文件上传到仓库,然后告知到对应的工程师。工程师使用基于Node.js开发的脚本,实现token的下载和转换输出。
使用 Node API 增强定制能力
style dictionary 提供了比较强大的 Node API,你可以使用这些API实现一些更复杂的能力,满足更多的需求场景,这里是API的文档,感兴趣的朋友可以仔细阅读。接下来我会结合实际案例,向大家演示如何使用Node API。
先从一个简单的例子开始,新建一个JavaScript文件。引入依赖之后,调用extend
传入配置。然后调用返回实例的buildAllPlatforms
方法。
// build.js
const StyleDictionary = require('style-dictionary').extend('config.json');
StyleDictionary.buildAllPlatforms();
extend方法的参数可以是一个文件路径,也可以是个对象。
// build.js
const StyleDictionary = require('style-dictionary').extend({
source: ['properties/**/*.json'],
platforms: {
scss: {
transformGroup: 'scss',
buildPath: 'build/',
files: [{
destination: 'variables.scss',
format: 'scss/variables'
}]
}
// ...
}
});
StyleDictionary.buildAllPlatforms();
你可以多次调用extend和buildAllPlatforms方法,可以用在输出嵌套主题或者多品牌主题等类似的场景。
// build.js
const StyleDictionary = require('style-dictionary');
const brands = [`brand-1`, `brand-2`, `brand-3`];
brands.forEach(brand => {
StyleDictionary.extend({
include: [`tokens/default/**/*.json`],
source: [`tokens/${brand}/**/*.json`],
// ...
}).buildAllPlatforms();
});
假设下面是我们的 token.json。
// token.json
{
"sizing": {
"10": {
"value": "10",
"type": "sizing"
},
"12": {
"value": "12",
"type": "sizing"
},
"14": {
"value": "14",
"type": "sizing"
},
"16": {
"value": "16",
"type": "sizing"
},
"18": {
"value": "18",
"type": "sizing"
},
"20": {
"value": "20",
"type": "sizing"
},
"22": {
"value": "22",
"type": "sizing"
},
"24": {
"value": "24",
"type": "sizing"
},
"28": {
"value": "28",
"type": "sizing"
},
"32": {
"value": "32",
"type": "sizing"
},
"36": {
"value": "36",
"type": "sizing"
},
"40": {
"value": "40",
"type": "sizing"
},
"48": {
"value": "48",
"type": "sizing"
}
}
}
先创建Style Dictionary 需要的 config.json。
// config.json
{
"source": ["./token.json"],
"platforms": {
"scss": {
"transformGroup": "scss",
"buildPath": "build/scss/",
"files": [
{
"destination": "sizing.scss",
"format": "scss/variables"
}
]
}
}
}
然后试着执行 node build.js
。不出意外的话,执行结束之后,会在当前目录中创建 build/sizing.scss
。
// Do not edit directly
// Generated on Thu, 04 Aug 2022 07:08:59 GMT
$sizing-10: 10;
$sizing-12: 12;
$sizing-14: 14;
$sizing-16: 16;
$sizing-18: 18;
$sizing-20: 20;
$sizing-22: 22;
$sizing-24: 24;
$sizing-28: 28;
$sizing-32: 32;
$sizing-36: 36;
$sizing-40: 40;
$sizing-48: 48;
虽然输出了我们想要的文件,但是文件的内容好像和预期不符,每一个 Scss 变量应该带上单位。要想定制这个输出结果,我们需要使用自定义的Transform。
Transform 和 TransformGroup
Style Dictionary 中的Transform是用来修改token的函数。使用 Transform可以对token的name、value 或者 attribute 进行转换,从而实现适配输出不同平台的能力。比如,将pixel转换成point。可以使用内置的Transforms,也可以使用registerTransform
方法注册自定义Transform。
StyleDictionary.registerTransform({
name: 'time/seconds',
type: 'value',
matcher: function(token) {
return token.attributes.category === 'time';
},
transformer: function(token) {
// Note the use of prop.original.value,
// before any transforms are performed, the build system
// clones the original token to the 'original' attribute.
return (parseInt(token.original.value) / 1000).toString() + 's';
}
});
TransformGroup就是一组Transform。有开箱即用的内置的TransformGroup,也可以通过registerTransformGroup
方法注册自定义TransformGroup。
StyleDictionary.registerTransformGroup({
name: 'Swift',
transforms: [
'attribute/cti',
'size/pt',
'name/cti'
]
});
为了给sizing加上单位pixel,我们先注册一个自定义 Transform 和 TransformGroup。
StyleDictionary.registerTransform({
name: 'size/px', //定义transform的名称,作为引用标记
type: 'value', // transform 转换的对象,我们需要转换token对应的值
matcher: token => { // token 匹配函数,返回boolean。
return token.type === 'sizing' && token.value !== 0
},
transformer: token => { // transform函数,接受token,返回想要的内容
return `${token.value}px`
}
})
StyleDictionary.registerTransformGroup({
name: 'custom/scss',
transforms: StyleDictionary.transformGroup['scss'].concat([
'size/px',
'size/percent'
])
})
接着在config.json中使用叫做custom/scss
的 TransformGroup。
{
"source": ["./token.json"],
"platforms": {
"scss": {
"transformGroup": "custom/scss",
"buildPath": "build/scss/",
"files": [
{
"destination": "sizing.scss",
"format": "scss/variables"
}
]
}
}
}
最后检查输出内容。
// Do not edit directly
// Generated on Thu, 04 Aug 2022 07:08:59 GMT
$sizing-10: 10px;
$sizing-12: 12px;
$sizing-14: 14px;
$sizing-16: 16px;
$sizing-18: 18px;
$sizing-20: 20px;
$sizing-22: 22px;
$sizing-24: 24px;
$sizing-28: 28px;
$sizing-32: 32px;
$sizing-36: 36px;
$sizing-40: 40px;
$sizing-48: 48px;
掌握工具的使用,配合自动化脚本,可以让token2css的过程变得非常丝滑流畅。
结束语
style dictionary 是一个功能丰富,易于定制和拓展的优秀开源工具。多平台输出的能力,能够帮助我们实现样式的一次定义,多处使用。通过自定义的Transform,你还可以将Design Token导出为ESM,你可以在JavaScript代码中使用。如果你的项目使用了 CSS-in-JavaScript 的技术方案时,这非常有用。团队当前正在和设计团队一起验证基于 Design Token 的协作流程,style dictionary 是必不可少的一环。