使用 Style Dictionary 管理 Design Token

https://www.notion.so/images/page-cover/rijksmuseum_avercamp_1608.jpg

在之前的文章中,我介绍了Design Token 相关的基本概念。本文将介绍如何使用现有的工具来管理 Design Token。

什么是Style Dictionary

社区上处理和解析Design Token的工具很多,这有一个可参考的目录。比较受欢迎的有 Style DictionaryTheo 等。我选择 Style Dictionary,因为社区受关注度更高,更新也比较及时,最近正在计划做4.0版本

df3638d13ec16eb8

Style Dictionary 是一个构建系统。使用它可以让你一次性定义风格,供任何平台或语言使用。在一个地方可以创建和编辑你的样式,通过一个命令就可以把这些规则导出到你需要的所有地方,iOS、Android、CSS、JS、HTML、Sketch 文件、样式文档,或者任何你能想得到的地方。它可以通过npm作为CLI使用,也可以像普通的Node.js模块一样使用。

2cd31ac27d84efbe

简单案例,快速上手

为了帮助开发者快速上手,官方很贴心地提供了一些实例。让我们来看一下简单的实例,从中了解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的能力和工作原理,让我们来看看它的架构设计。下面是官方给到的架构图。

dcd0f990884e683f

一图胜千言,这张图已经很直观地展示其工作原理。

  • 第一步:解析配置文件。

  • 第二步:找到所有 token 文件。config中的 includesource 组合决定了搜索查找的范围。

  • 第三步:将所有的token文件进行一个深度合并。相同的结构将会覆盖。

  • 第四步:遍历config中定义所有platform,执行一下操作:

    • 执行 token转换,提取token中的value。这会深度遍历每一个对象,直到找到value这个key。

    • 解析别名和引用。找到value之后,如果其值是别名或者引用,比如"{size.font.base.value}” ,就用转换后的值替换之。

    • 根据每一个platform配置中的format格式,输出不同的文件。

    • 在输出文件之后,还可以执行自定义的Actions。

当上述四个步骤都成功执行并结束后,你就能得到你想要的输出产物了。

与设计师协作

浅谈Design Token中我曾经提到过,设计师可以通过 Figma 等设计工具,以文件的形式将 design token 提供给开发人员。现在,身为开发人员的我们,可以使用 Style Dictionary 对token进行处理,输出我们想要的文件。

604ed3dd6361a029

我们将所有的tokens文件托管到内部的gitlab仓库中。设计师定义好token之后,将文件上传到仓库,然后告知到对应的工程师。工程师使用基于Node.js开发的脚本,实现token的下载和转换输出。

ebd7ea7705f10a20

使用 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 是必不可少的一环。