探讨 JavaScript Design Tokens 作为统一设计语言的载体,如何在不同平台 (Web, Mobile) 间实现样式和组件的一致性。

好的,各位靓仔靓女,欢迎来到今天的“JS Design Tokens:一统江湖,代码也能说人话!”专场。今天咱们不搞虚的,直接上干货,看看这 Design Tokens 到底是个啥玩意儿,怎么就能让我们的 Web 和 Mobile 应用穿上同款战袍。

开场白:设计语言的“翻译官”

想象一下,你是个设计师,精心设计了一套颜色、字体、间距方案,美滋滋地交给前端和移动端开发。结果呢?前端用 CSS 写了一堆变量,移动端 Android 用 XML 定义了一堆属性,iOS 可能直接写死在 Swift 代码里。过几天,设计改了,你得吭哧吭哧地通知所有人,让他们手动改代码。

是不是感觉头大?这就是设计语言没有统一的“翻译官”导致的。而 Design Tokens,就是这个“翻译官”。它把设计决策(颜色、字体、间距等等)抽象成平台无关的变量,然后通过工具转换成各个平台能理解的代码。

什么是 Design Tokens?

简单来说,Design Tokens 就是一些命了名的值,代表了设计决策。这些值是平台无关的,可以是颜色值、字体大小、间距、动画时长等等。

举个例子,假设我们的品牌颜色是“活力橙”,字体是“微软雅黑”,间距是 16px。用 Design Tokens 表示,可能是这样的:

{
  "color": {
    "brand": {
      "primary": "#FFA500",
      "secondary": "#FFD700"
    }
  },
  "font": {
    "family": {
      "base": "Microsoft YaHei, sans-serif"
    },
    "size": {
      "base": "16px",
      "heading": "24px"
    }
  },
  "spacing": {
    "base": "16px",
    "large": "32px"
  }
}

这个 JSON 文件就是我们的 Design Tokens。它描述了颜色、字体、间距这些设计决策,而且是抽象的、平台无关的。我们没有说 #FFA500 是 CSS 变量还是 Android XML 属性,只是说它是“品牌主色”。

Design Tokens 的好处:

  • 一致性: 确保所有平台使用相同的设计决策,避免视觉差异。
  • 可维护性: 设计变更只需要修改 Design Tokens,然后重新生成各个平台的代码,不需要手动修改。
  • 可扩展性: 可以轻松添加新的设计决策,或者修改现有的设计决策。
  • 可复用性: 可以在不同的项目中使用相同的 Design Tokens。
  • 设计和开发协作: 设计师可以使用 Design Tokens 作为沟通语言,减少歧义。

实战:如何使用 Design Tokens

接下来,我们来一步步地演示如何使用 Design Tokens。

1. 定义 Design Tokens

首先,我们需要定义我们的 Design Tokens。可以使用 JSON、YAML 等格式。

# tokens.yml
colors:
  brand:
    primary: "#007bff"
    secondary: "#6c757d"
  text:
    primary: "#212529"
    secondary: "#6c757d"
fonts:
  family:
    base: "Arial, sans-serif"
    heading: "Helvetica, sans-serif"
  size:
    base: "16px"
    heading: "24px"
spacing:
  small: "8px"
  medium: "16px"
  large: "24px"

2. 选择转换工具

接下来,我们需要一个工具,将 Design Tokens 转换成各个平台能理解的代码。有很多选择,比如:

  • Style Dictionary: 由 Amazon 开发,功能强大,支持多种平台。
  • Theo: 由 Salesforce 开发,轻量级,易于使用。
  • Specify: 一个设计数据平台,提供了 Design Tokens 的管理和转换功能。

这里我们选择 Style Dictionary,因为它功能强大,社区活跃。

3. 安装 Style Dictionary

首先,我们需要安装 Style Dictionary:

npm install style-dictionary --save-dev

4. 配置 Style Dictionary

我们需要创建一个配置文件,告诉 Style Dictionary 如何转换 Design Tokens。

// config.js
module.exports = {
  source: ["tokens.yml"], // Design Tokens 的源文件
  platforms: {
    web: {
      transformGroup: "css", // 使用 CSS 转换组
      buildPath: "dist/web/", // 输出目录
      files: [
        {
          destination: "variables.css", // 输出文件名
          format: "css/variables", // 输出格式
          options: {
            showFileHeader: false,
          },
        },
      ],
    },
    android: {
      transformGroup: "android", // 使用 Android 转换组
      buildPath: "dist/android/", // 输出目录
      files: [
        {
          destination: "colors.xml", // 输出文件名
          format: "android/colors", // 输出格式
          options: {
            showFileHeader: false,
          },
        },
        {
          destination: "dimens.xml", // 输出文件名
          format: "android/dimens", // 输出格式
          options: {
            showFileHeader: false,
          },
        },
      ],
    },
    ios: {
      transformGroup: "ios", // 使用 iOS 转换组
      buildPath: "dist/ios/", // 输出目录
      files: [
        {
          destination: "Colors.swift", // 输出文件名
          format: "ios/colors.swift", // 输出格式
          options: {
            showFileHeader: false,
          },
        },
        {
          destination: "Typography.swift", // 输出文件名
          format: "ios/typography.swift", // 输出格式
          options: {
            showFileHeader: false,
          },
        },
      ],
    },
  },
};

这个配置文件告诉 Style Dictionary:

  • 我们的 Design Tokens 源文件是 tokens.yml
  • 我们需要生成 Web、Android 和 iOS 三个平台的代码。
  • 对于 Web 平台,使用 css 转换组,输出 CSS 变量文件 variables.css
  • 对于 Android 平台,使用 android 转换组,输出颜色文件 colors.xml 和尺寸文件 dimens.xml
  • 对于 iOS 平台,使用 ios 转换组,输出颜色文件 Colors.swift 和字体文件 Typography.swift

5. 运行 Style Dictionary

现在,我们可以运行 Style Dictionary 了:

npx style-dictionary build

运行完成后,会在 dist 目录下生成各个平台的代码。

6. 使用 Design Tokens

现在,我们可以在我们的项目中使用 Design Tokens 了。

Web (CSS):

/* dist/web/variables.css */
:root {
  --color-brand-primary: #007bff;
  --color-brand-secondary: #6c757d;
  --color-text-primary: #212529;
  --color-text-secondary: #6c757d;
  --font-family-base: Arial, sans-serif;
  --font-family-heading: Helvetica, sans-serif;
  --font-size-base: 16px;
  --font-size-heading: 24px;
  --spacing-small: 8px;
  --spacing-medium: 16px;
  --spacing-large: 24px;
}
<style>
  body {
    font-family: var(--font-family-base);
    font-size: var(--font-size-base);
    color: var(--color-text-primary);
  }

  h1 {
    font-family: var(--font-family-heading);
    font-size: var(--font-size-heading);
    color: var(--color-brand-primary);
  }

  .button {
    background-color: var(--color-brand-primary);
    color: white;
    padding: var(--spacing-medium);
    border-radius: 5px;
  }
</style>

Android (XML):

<!-- dist/android/colors.xml -->
<resources>
  <color name="color_brand_primary">#007bff</color>
  <color name="color_brand_secondary">#6c757d</color>
  <color name="color_text_primary">#212529</color>
  <color name="color_text_secondary">#6c757d</color>
</resources>
<!-- dist/android/dimens.xml -->
<resources>
  <dimen name="spacing_small">8dp</dimen>
  <dimen name="spacing_medium">16dp</dimen>
  <dimen name="spacing_large">24dp</dimen>
  <dimen name="font_size_base">16sp</dimen>
  <dimen name="font_size_heading">24sp</dimen>
</resources>
<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="Hello World!"
  android:textColor="@color/color_text_primary"
  android:textSize="@dimen/font_size_base"
  android:padding="@dimen/spacing_medium" />

<Button
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="Click Me!"
  android:backgroundTint="@color/color_brand_primary"
  android:padding="@dimen/spacing_medium" />

iOS (Swift):

// dist/ios/Colors.swift
import UIKit

struct Colors {
    static let colorBrandPrimary: UIColor = UIColor(red: 0.000, green: 0.478, blue: 1.000, alpha: 1.0)
    static let colorBrandSecondary: UIColor = UIColor(red: 0.424, green: 0.463, blue: 0.490, alpha: 1.0)
    static let colorTextPrimary: UIColor = UIColor(red: 0.130, green: 0.145, blue: 0.161, alpha: 1.0)
    static let colorTextSecondary: UIColor = UIColor(red: 0.424, green: 0.463, blue: 0.490, alpha: 1.0)
}
// dist/ios/Typography.swift
import UIKit

struct Typography {
    static let fontFamilyBase: String = "Arial, sans-serif"
    static let fontFamilyHeading: String = "Helvetica, sans-serif"
    static let fontSizeBase: CGFloat = 16.0
    static let fontSizeHeading: CGFloat = 24.0
    static let spacingSmall: CGFloat = 8.0
    static let spacingMedium: CGFloat = 16.0
    static let spacingLarge: CGFloat = 24.0
}
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let label = UILabel()
        label.text = "Hello World!"
        label.textColor = Colors.colorTextPrimary
        label.font = UIFont(name: Typography.fontFamilyBase, size: Typography.fontSizeBase)
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)

        let button = UIButton(type: .system)
        button.setTitle("Click Me!", for: .normal)
        button.backgroundColor = Colors.colorBrandPrimary
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = UIFont.systemFont(ofSize: Typography.fontSizeBase)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)

        // Auto layout constraints (example)
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true

        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 50).isActive = true
        button.widthAnchor.constraint(equalToConstant: 150).isActive = true
        button.heightAnchor.constraint(equalToConstant: 40).isActive = true
    }
}

高级用法:自定义 Transform 和 Format

Style Dictionary 提供了很多内置的 Transform 和 Format,但有时候我们需要自定义。

  • Transform: 用于修改 Design Tokens 的值。比如,我们可以将颜色值从 HEX 转换为 RGB。
  • Format: 用于生成特定格式的代码。比如,我们可以生成 CSS Modules 的代码。

自定义 Transform:

// config.js
module.exports = {
  source: ["tokens.yml"],
  transform: {
    "size/pxToRem": {
      type: "value",
      transformer: (token) => {
        if (token.unit === "pixel") {
          return `${token.value / 16}rem`;
        }
        return token.value;
      },
    },
  },
  platforms: {
    web: {
      transforms: ["name/cti/kebab", "size/pxToRem", "color/hex"],
      buildPath: "dist/web/",
      files: [
        {
          destination: "variables.css",
          format: "css/variables",
          options: {
            showFileHeader: false,
          },
        },
      ],
    },
  },
};

这个配置定义了一个名为 size/pxToRem 的 Transform,用于将像素值转换为 rem 值。

自定义 Format:

// config.js
module.exports = {
  source: ["tokens.yml"],
  format: {
    "css/modules": ({ dictionary, options, file }) => {
      const className = options.className || "tokens";
      let output = `.${className} {n`;
      dictionary.allTokens.forEach((token) => {
        output += `  --${token.name}: ${token.value};n`;
      });
      output += "}n";
      return output;
    },
  },
  platforms: {
    web: {
      transforms: ["name/cti/kebab", "size/pxToRem", "color/hex"],
      buildPath: "dist/web/",
      files: [
        {
          destination: "variables.module.css",
          format: "css/modules",
          options: {
            showFileHeader: false,
            className: "my-design-tokens",
          },
        },
      ],
    },
  },
};

这个配置定义了一个名为 css/modules 的 Format,用于生成 CSS Modules 的代码。

Design Tokens 的最佳实践:

  • 语义化命名: 使用有意义的名称,避免使用 #FFA500 这样的字面值。
  • 分层: 将 Design Tokens 分成不同的层级,比如 Core Tokens、Component Tokens、Theme Tokens。
  • 版本控制: 使用版本控制工具(比如 Git)管理 Design Tokens。
  • 自动化: 使用自动化工具(比如 CI/CD)自动生成各个平台的代码。
  • 与设计工具集成: 将 Design Tokens 与设计工具(比如 Figma、Sketch)集成,实现设计和开发的同步。

分层示例:

  • Core Tokens: 定义最基础的设计决策,比如颜色、字体、间距。
  • Component Tokens: 定义组件的样式,比如按钮的颜色、字体大小。
  • Theme Tokens: 定义主题的样式,比如亮色主题、暗色主题。
{
  "core": {
    "color": {
      "primary": "#007bff",
      "secondary": "#6c757d"
    },
    "font": {
      "size": {
        "base": "16px"
      }
    },
    "spacing": {
      "base": "16px"
    }
  },
  "component": {
    "button": {
      "primary": {
        "background": "{core.color.primary}",
        "text": "#fff",
        "padding": "{core.spacing.base}"
      }
    }
  },
  "theme": {
    "light": {
      "background": "#fff",
      "text": "{core.color.primary}"
    },
    "dark": {
      "background": "#000",
      "text": "#fff"
    }
  }
}

总结:Design Tokens,让设计和开发不再鸡同鸭讲

Design Tokens 是一个强大的工具,可以帮助我们实现设计语言的统一,提高开发效率,降低维护成本。它就像一个“翻译官”,让设计和开发不再鸡同鸭讲,而是能够用同一种语言沟通。

所以,赶紧用起来吧!让你的 Web 和 Mobile 应用穿上同款战袍,走向人生巅峰!

今天就讲到这里,感谢各位的收听!咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注