钉钉多维表设计系统(Design System)(1) —— Design Token 视觉语言

简介: 一、背景2022 年 11 月,钉钉多维表进行全新改版。改版过程中,出于解决体验一致性、提升研发效率、降低沟通成本的需求,急需构建一套设计系统(Design System)以供使用。这套系统由以下三个部分构成:we-design-next 组件库、WCON 图标交付方案、Design Token 视觉语言本期主要对 Design Token 的使用与实现进行讲解二、Design Token 概述为

一、背景

2022 年 11 月,钉钉多维表进行全新改版。改版过程中,出于解决体验一致性、提升研发效率、降低沟通成本的需求,急需构建一套设计系统(Design System)以供使用。

这套系统由以下三个部分构成:we-design-next 组件库、WCON 图标交付方案、Design Token 视觉语言

本期主要对 Design Token 的使用与实现进行讲解

二、Design Token 概述

为了保证视觉一致性,设计团队制定了一系列原子样式,包含了颜色、圆角、阴影、字体/字号等。并且研发侧也做了对齐实现,保证设计同学和研发同学使用的原子样式是一致的。

我们将这些原子样式称之为 Token,并约定设计侧与研发侧均能且只能使用已有的 Token,不得自行定义新样式,通过这种方式来保障了视觉的统一。

设计侧将这些原子样式做成了 Master Go 组件,以便可以方便的进行复用。研发侧采用了一种巧妙的方案来进行了对齐实现,兼顾了运行性能与研发效率,在实现了方便使用 Token 的基础上,还能进行无重渲染换肤。具体实现方式是什么这里先卖个关子。

这里先介绍下使用方法,后面再聊下具体的实现。也许聪明的你从下文的使用方法中就可以猜出背后的实现原理了。

三、使用方法

步骤1. 获取常量名

打开设计稿,选中要实现的组件,通常会在右侧属性面板有一个常量名

注意:如果发现没有常量名,说明使用了视觉规范以外的属性。请联系设计师进行修正。

步骤2. 在代码中使用

 强烈推荐  方式一:CSS 常量

常用于组件的声明中,这种方式实际上获取到的是 CSS 变量(var(--xx-xxx))。

强烈推荐这种方式,CSS 变量的优势在于动态切换主题时,不必触发组件的 React 重新渲染即可完成视觉的转换,性能、体验比较好。

themeCss.color.common_brand
themeCss.radius.small
themeCss.shadow.small

DEMO

import { themeCss } from '@ali/notable-common';

const DemoStyledComponent = styled(div)`
	background-color: ${themeCss.color.common_brand};
	radius: ${themeCss.radius.small};
	shadow: ${}
`;

方式二:JS 常量

常用于 JS / TS 逻辑中,这种方式获取到的是真实的静态值(#ABCDEF),不会动态变化。

因此当动态切换主题后,通常需要手动重新执行渲染逻辑。

theme.color.common_brand

handleClick = useCallback(() => {
  controller.setHighlightColor(theme.color.common_brand)
})

四、实现原理

上面的使用方法一有提到我们 Token 的表现形式是 CSS 变量。使用 CSS 变量有很多好处,最主要的是切换主题时,不需要组件重新渲染,用户体验极佳。与之相对的是通过 js 方案实现的主题系统,通常在切换主题时,需要组件进行重新渲染,或者页面整体刷新。

不过使用 CSS 变量对开发效率是有影响的,由于 CSS 变量就是一段纯粹的字符串,因此开发同学在书写样式时,既没有代码提示,也没有代码检查。因为设计侧的 Token 和研发侧的 Token 虽然大体上是一一对应的,但具体的表现形式可能不同,例如设计侧采用 / 斜杠作为分割符,而研发侧是不可能有 / 斜杠的。所以研发同学不仅写起来费力,而且很容易写错。

但你可以看到,我们的研发同学在使用 Token 的时候,完全是按照 JS 的方式来使用的,这种情况下是具有完整的代码提示能力与类型检查的,只需要写一些大概的字符,例如 common_brand 就可以正确匹配。

那我们是如何实现的呢?是人工写了一个超大的对象,一个一个手工写了映射关系吗?例如 const theme = { color: { common_brand: 'var(--color-common-brand)' } };

肯定不是,且不说这种方式的工作量有多么大,这种方式违反了最基本的开闭原则,每当 Token 有变动都需要更新这个大对象,这是不可接受的。

相信有经验的前端同学在看了上面的使用方法之后,基本就已经猜出了实现的原理。这里也就不卖关子了,没错,就是基于 Proxy 将 js 对象转换为了 css 变量。使用这种方式,既可以享受 CSS 变量的高性能,也可以享受 JS 变量带来的自动补全与类型检查,大大提高了研发效率。开发同学在开发时面对的是 JS 对象,而浏览器在解析时,又将其视为 CSS 变量。

具体实现上,我们实现了一个 theme 模块,该模块包含以下内容:

  • 主题管理器:ThemeManager
  • 主题接口:ITheme。
  • 一些实现了主题接口的主题包:例如 Common 主题与 Dark 主题
  • 转换逻辑

该模块最终共导出几个常量:themeManager 用来管理主题、theme 用来提供对值的访问、themeCss 用来提供对 Token 的访问、themeCssVarsDeclaration CSS 变量声明集合,用来提供 CSS 变量的值、themeList 用来提供当前启用的主题列表。

themeCss 的转化如下:

/**
 * 可变 CSS 变量访问符
 *
 * 使用该访问符,可以实现动态主题
 *
 * 用法:css`color: ${themeCss.color.common_blue1}`
 *
 * 返回值:var(--color-common-blue1)
 */
export const themeCss = new Proxy({}, {
  get(target, property) {
    const currentTheme = themeManager.getTheme();
    if (isThemeProperty(currentTheme, property)) {
      return proxyPropertyToCssVarAccessor(property, target[property]);
    }
  },
}) as Theme;

export const isThemeProperty = (theme: Theme, property: unknown): property is keyof GenericStyleProperty => {
  return Reflect.has(theme, property);
};

/**
 * 将被代理对象的属性访问符劫持,访问时返回对应的 Css 变量访问符
 *
 * 访问 foo.bar 时,返回 'var(--foo-bar)'
 *
 * @param objectName `foo`
 * @param object `{ bar: 1 }`
 * @returns var(--foo-bar)
 */
export const proxyPropertyToCssVarAccessor = (objectName: string, object: object) => {
  return new Proxy(object, {
    get(_, property) {
      if (typeof property === 'string') {
        return `var(--${objectName}-${property.replace(/_/g, '-')})`;
      }
    },
  });
};

在转换后,外界任何对 themeCss 的访问,最终都会被转换为 CSS 变量的形式

themeCssVarsDeclaration 的转换如下:

/**
 * 全局 CSS 变量声明
 */
export const themeCssVarsDeclaration = availableThemes.map(generateCssVars).join('\n');

export const generateCssVars = (theme: Theme) => {
  const cssVars: string[] = [];
  const { name, color, radius, shadow } = theme;
  (Object.keys(color)).forEach((key) => {
    cssVars.push(`  --color-${key.replace(/_/g, '-')}: ${color[key]};`);
  });
  (Object.keys(radius)).forEach((key) => {
    cssVars.push(`  --radius-${key.replace(/_/g, '-')}: ${radius[key]};`);
  });
  (Object.keys(shadow)).forEach((key) => {
    cssVars.push(`  --shadow-${key.replace(/_/g, '-')}: ${shadow[key]};`);
  });
  const selector = name ? `[ddTheme=${name}]` : ':root';
  const result = `${selector} {\n${cssVars.join('\n')}\n}`;
  return result;
};

例如,有如下两个主题:

  const defaultTheme = {
    color: {
      common_blue1: '#5C71DD',
    },
  };

  const darkTheme = {
    name: 'dark',
    color: {
      common_blue1: '#000000',
    },
  };

最终会转换为:

const themeDeclarations = `
  :root {
    --color-common-blue1: #5C71DD;
  }

  [ddTheme=dark] {
    --color-common-blue1: #000000;
  }
`

开发时,需要做如下工作:

  1. 在应用的全局样式中引用 themeCssVarsDeclaration,以便完成对 CSS 变量的声明
  2. (可选)在应用的初始化过程中,
    1. 调用 themeManager.mount() 设置挂载 DOM ,以便限制主题作用域。如不设置,则默认挂载为 document.body
    2. 调用 themeManager.setTheme() 进行主题的设置。如不设置,则使用默认主题
  3. 在需要使用 Token 的地方直接调用 ${themeCss.color.common_brand} 即可

切换主题的实现方法:

当用户切换主题时,themeManager 会将挂载 DOM 的 ddTheme 属性设置为对应的主题名字,这样各个子 DOM 样式中的 CSS 变量会命中含有对应主题名字选择器的 CSS 变量声明,浏览器自动使用新的 CSS 变量值对其 DOM 进行重绘,这样就完成了主题切换

五、方案对比

StyleComponents ThemeProvider

优点:业界知名的成熟方案,本身也契合我们项目中广泛应用的 styled-components 方案

缺点:切换时,由于采用的是 ThemeProvider ,组件会重新渲染。重新进行昂贵的 js 计算。

多 CSS 方案(Token)

优点:常用的成熟方案,准备多套包含不同内容的 CSS,切换主题时加载对应的 CSS。每个主题的 CSS 可以通过 CSS 预处理例如 sass / less 等变量进行替代,再利用 webpack 等插件进行生成。fusion 等提供了相应的支持,使用时只需要将主题包引入到工程中打包即可。

缺点:打包配置复杂、打包出的样式文件有冗余、动态加载 CSS 会有延迟、包体积会增大不少

多 CSS 方案(className)

优点与缺点与上文类似,主要是切换主题的实现方式是切换 DOM 上的 className,

六、结语

CSS 变量是比较成熟的方案了,由于时代的发展,目前的业务也不太需要兼容 IE、Safari 10、Chrome 50 以下这些老古董了,因此我们可以放心大胆的使用 CSS 变量,享受现代前端发展的结果,乘上用户体验飞翔的翅膀。

不过目前业界中对 CSS 变量的使用仍然基本上是采用直接书写 var(--xxx-xx) 字符串的方式,包括 fusion 等业界先驱也是如此,这无疑限制了开发者们的开发效率。

本文的解决方案很好地解决了这个问题,在开发同学使用 CSS 变量时,恢复了前端开发时应有的代码提示和类型检查的能力,让研发效率更上一层楼。

相关文章
|
4月前
|
BI
123. SAP ABAP 显式增强技术之 New BAdI 的技术原理介绍
123. SAP ABAP 显式增强技术之 New BAdI 的技术原理介绍
|
4月前
|
BI
SAP ABAP 显式增强技术之 New BAdI 的技术原理介绍试读版
SAP ABAP 显式增强技术之 New BAdI 的技术原理介绍试读版
|
11月前
|
存储 Java BI
如何通过增强(Enhancement) 的方式给 SAP ABAP 标准程序增添新功能试读版
如何通过增强(Enhancement) 的方式给 SAP ABAP 标准程序增添新功能试读版
|
11月前
|
Web App开发 JSON 前端开发
SAP UI5 进阶 - JSON 模型字段里的值,显示在最终 UI5 界面上的奥秘分析试读版
SAP UI5 进阶 - JSON 模型字段里的值,显示在最终 UI5 界面上的奥秘分析试读版
|
3月前
|
机器学习/深度学习 Java 开发工具
【能力展现】魔改ZXING源码实现商业级DM码检测能力
【能力展现】魔改ZXING源码实现商业级DM码检测能力
102 1
|
4月前
|
BI
如何使用动态 ABAP 程序生成技术,对 ABAP 系统标准的报表行为进行微调试读版
如何使用动态 ABAP 程序生成技术,对 ABAP 系统标准的报表行为进行微调试读版
|
4月前
|
人工智能 程序员 API
【AI大模型应用开发】1.0 Prompt Engineering(提示词工程)- 典型构成、原则与技巧,代码中加入Prompt
【AI大模型应用开发】1.0 Prompt Engineering(提示词工程)- 典型构成、原则与技巧,代码中加入Prompt
202 0
|
4月前
|
BI
SAP ABAP 显式增强技术之 New BAdI 的实战介绍 - 如何创建和激活增强实现试读版
SAP ABAP 显式增强技术之 New BAdI 的实战介绍 - 如何创建和激活增强实现试读版
|
4月前
|
BI
124. SAP ABAP 显式增强技术之 New BAdI 的实战介绍 - 如何创建和激活增强实现
124. SAP ABAP 显式增强技术之 New BAdI 的实战介绍 - 如何创建和激活增强实现
|
前端开发 UED 开发者
钉钉多维表设计系统(Design System)(1) —— Design Token 视觉语言
一、背景2022 年 11 月,钉钉多维表进行全新改版。改版过程中,出于解决体验一致性、提升研发效率、降低沟通成本的需求,急需构建一套设计系统(Design System)以供使用。这套系统由以下三个部分构成:we-design-next 组件库、WCON 图标交付方案、Design Token 视觉语言本期主要对 Design Token 的使用与实现进行讲解二、Design Token 概述为
393 0
钉钉多维表设计系统(Design System)(1) —— Design Token 视觉语言