钉钉多维表设计系统(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 变量时,恢复了前端开发时应有的代码提示和类型检查的能力,让研发效率更上一层楼。

相关文章
|
6月前
|
XML JSON 前端开发
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(简单支持发起人与审批人的流程)
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(简单支持发起人与审批人的流程)
408 2
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(简单支持发起人与审批人的流程)
|
6月前
基于若依的ruoyi-nbcio流程管理系统增加仿钉钉流程设计(七)
基于若依的ruoyi-nbcio流程管理系统增加仿钉钉流程设计(七)
91 1
基于若依的ruoyi-nbcio流程管理系统增加仿钉钉流程设计(七)
|
6月前
|
XML JSON 前端开发
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(支持并行网关)
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(支持并行网关)
347 3
|
3月前
|
存储 安全 API
"解锁企业级黑科技!用阿里云视觉智能打造钉钉级人脸打卡系统,安全高效,让考勤管理秒变智能范儿!"
【8月更文挑战第14天】随着数字化办公的发展,人脸打卡成为企业考勤的新标准。利用阿里云视觉智能开放平台构建类似钉钉的人脸打卡系统,其关键在于:高精度人脸识别API支持复杂场景下的快速检测与比对;活体检测技术防止非生物特征欺骗,确保安全性;云端存储与计算能力满足大数据处理需求;丰富的SDK与API简化集成过程,实现高效、安全的考勤管理。
94 2
|
5月前
|
人工智能 移动开发 IDE
安利几款与钉钉平台无缝集成打通账号认证的企业文档管理系统
钉钉是很多中小企业都爱用的产品,开通账号就能直接使用了,应用生态非常丰富,尤其是AI技术的应用,走在行业前列。但仍有很多企业对于全面拥抱SaaS服务充满了顾虑,尤其在内部资料的管理这块,即使钉钉在线文档已经提供了非常优秀的协作体验,不少客户仍更偏爱私有部署在局域网里面的企业文档管理系统。那么能将企业内部部署的文档管理系统集成到钉钉平台上面,和钉钉文档并行使用呢?市面上又有哪些企业文档管理系统软件支持与钉钉的集成呢?这也是很多企业客户的疑问。
安利几款与钉钉平台无缝集成打通账号认证的企业文档管理系统
|
6月前
|
XML JSON 前端开发
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(排它条件网关)
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(排它条件网关)
115 3
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程json转bpmn的flowable的xml格式(排它条件网关)
|
5月前
|
JSON 分布式计算 DataWorks
DataWorks产品使用合集之能否支持从结果表取出示警信息并且打通钉钉进行告警
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
|
6月前
|
前端开发
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程初步完成转bpmn设计(还有bug,以后再修改)
基于若依的ruoyi-nbcio流程管理系统仿钉钉流程初步完成转bpmn设计(还有bug,以后再修改)
93 0
|
存储 弹性计算 安全
成功案例-钉钉 | 学习笔记
快速学习 成功案例-钉钉
403 0

热门文章

最新文章