一、前言
TDesign 是腾讯各业务团队在服务业务过程中沉淀的一套企业级设计体系,于2021年12月底正式对外开源。TDesign 用到了哪些广受欢迎的开源技术,选择这些技术的原因是什么?TDesign 官方支持了众多开发技术栈,为什么选择各仓库原生开发而不是转译生成代码?本篇会从从仓库目录结构开始,通盘分析 TDesign 的技术选型和原因。
我们先来看一下 TDesign 组件库仓库的目录结构,帮助理解代码的整体结构。TDesign 官方支持了众多技术栈,当开发同学进行技术栈互转的时候,无需太多学习组件库的成本,基本可以直接上手开发。
二、仓库目录结构
代码语言:javascript复制.
├── script // 组件库构建等脚本
├── site // 官网站点目录
├── src // 组件主要逻辑目录
│ ├── common // 以 submodules 方式引入
│ │ ├── style // 组件库 UI 开发
│ │ │ ├── web // 桌面端组件样式
│ │ │ ├── mobile // 移动端组件样式
│ │ └── js // 组件库公用函数
│ ├── button // button 组件开发目录
│ └── ...
├── test
│ ├── ssr // 服务端渲染测试脚本
│ └── unit // 组件单元/集成测试脚本
└── package.json
每个仓库对应一个组件库技术栈实现,包含各自技术栈实现代码和一个 tdesign-common 子仓库:
https://github.com/Tencent/tdesign-common
子仓库作为公共库包含:
- 一些公共的工具函数,组件抽象逻辑代码
- 组件库 UI 开发内容,既 HTML 结构和 CSS 样式(React/Vue 等多技术栈共用)
TDesign 的大部分技术栈仓库都参照了以上结构,微信小程序和 Flutter 等仓库因为技术栈特性限制结构略有不同,也没有以子仓库方式引入 Less 实现的组件样式。
2.1 组件开发目录
聚焦到组件维护最常访问的目录,一个典型的组件开发结构如下:
代码语言:javascript复制button
├── __tests__
├── _example // 官网 demo 实现
├── button.md // 官网文档
├── button.tsx //组件逻辑实现
├── index.ts
├── style
│ ├── css.js
│ └── index.js // 按需引入 common submoudle 中对应组件样式
└── type.ts // 组件 API 定义
button.md、type.ts 都由 API 管理工具自动生成,以保证各个技术栈实现一致。button.tsx 中是组件的主要维护代码。
TDesign 使用 style/ 目录来显式引入对应 Button 组件的 Less/CSS 实现,以实现用户使用 npm 包时可以按需引入组件样式,通过构建工具 tree shanking 掉未引入的组件样式,减小业务包体积。
2.2 构建产物目录
TDesign 各个技术栈 npm 包都遵循如下目录结构规范,以便让用户根据自己的需求引入不同产物:
代码语言:javascript复制├─ dist // umd
│ ├─ tdesign.js
│ ├─ tdesign.js.map
│ ├─ tdesign.min.js
│ ├─ tdesign.min.js.map
│ ├─ tdesign.css
│ ├─ tdesign.css.map
│ └─ tdesign.min.css
├─ esm // esm
│ ├─ button
│ │ ├─ style
│ │ │ └─ index.js
│ │ ├─ button.js
│ │ ├─ button.d.ts
│ │ ├─ index.js
│ │ └─ index.d.ts
│ ├─ index.js
│ └─ index.d.ts
├─ es // es
│ ├─ button
│ │ ├─ style
│ │ │ ├─ css.js
│ │ │ ├─ index.css
│ │ │ └─ index.js
│ │ ├─ button.js
│ │ ├─ button.d.ts
│ │ ├─ index.js
│ │ └─ index.d.ts
│ ├─ index.js
│ └─ index.d.ts
├─ lib // cjs
│ ├─ button
│ │ ├─ button.js
│ │ ├─ button.d.ts
│ │ ├─ index.js
│ │ └─ index.d.ts
│ ├─ index.js
│ └─ index.d.ts
├─ LICENSE
├─ CHANGELOG.md
├─ README.md
└─ package.json
各产物说明见:https://github.com/Tencent/tdesign-common/blob/develop/develop-install.md
如此,通过统一的开发和产物目录结构要求,TDesign 多个技术栈框架得以抹平一部分差异,同步迭代发展。
三、开源技术选型
通过上面章节,相信你已经对 TDesign 的代码结构有了初步了解,下面详细介绍下在组件设计/开发过程中的一些技术选择。
3.1 TypeScript JSX
全面拥抱 TypeScript,Eslint 规范基本遵循 Airbnb 规范:
https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base
对于组件库这样的前端基础设施产品来说 TypeScript 是最适合的使用场景,每一个封装好的组件都像一个等待接收入参的函数,全程 TypeScript 检查和推断,能够很大程度上保证使用者传入的各类参数在组件内部被正确对待和执行。
在 TDesign 官方维护的几个组件库中,除了小程序/Angular 等存在技术栈平台限制的问题,其他技术栈大多采用了 JSX 的方式来维护组件,并没有像 Vue 更常用的 Template,决策基于 JSX 开发带来的如下额外优势:
- Vue 等技术栈中的 Template 配合相关语法糖能使得开发体验比较接近原生 HTML,但对于组件这种本身具有高内聚、高灵活性的场景来说,JSX 是更合适的方案:Template 对 TS 对支持不够完善,往往需要额外类型定义;在处理大量结构赋值或者条件判断时 Template 反而不如 JSX 直观。
- Vue、React 等技术栈都可以复用大量相近的代码,能够降低跨技术栈维护成本。
3.2 组件样式维护
为了避免重复开发,多个仓库的 UI 样式稿也都尽可能使用同一份,元素的层级和类名一致,各个框架组件库按照上述仓库目录结构的要求,以 submodule 的方式引入一同使用。
所有组件样式均采取 Less BEM CSS Variables 方式来开发和维护。
Less
组件样式使用 Less 实现,Less 的 Mixins、Functions 等特性可以帮助我们更好的组织、维护组件库样式代码。每个组件样式实现文件说明:
代码语言:javascript复制// https://github.com/Tencent/tdesign-common/tree/develop/style/web/components/button
button
├── _docs.less // 组件官网展示相关样式
├── _mixin.less
├── _var.less // 组件变量 token
├── _index.less // 样式实现
└── index.html // DOM 结构示意
BEM
出于降低贡献者门槛等方面的考虑,TDesign 没有额外引入插件或包来处理 CSS 全局污染问题,也没有选择 CSS in JS 等方案,这会在一定程度上降低代码可读性,增加贡献者心智负担。在 CSS 样式命名上我们遵循 BEM 命名规范:http://getbem.com/
通过 BEM 很大程度上已经可以有效避免组件间样式的相互污染,减少嵌套层级:
BEM 使用分隔符将类名区隔成三个部分:
代码语言:javascript复制[prefix]-[block]__[element]--[modifier]
- prefix:全局的前缀,这里指代表 tdesign 的前缀,也就是 t- - Block(块):组件的最外层父元素,这个类包含最通用和可重用的功能。 - Element(元素):组件内可包含一个或多个元素,元素为块添加了新功能。无需重置任何属性。 - Modifier(修饰类):块或元素都可以通过修饰词来表示为变体。
详细指引见 CSS 命名规范:https://github.com/Tencent/tdesign-common/blob/develop/css-naming.md
CSS Variables
Less 作为预处理器并不能支持我们实现在线切换主题样式的能力(毕竟浏览器并不能识别 Less 声明的变量),因此 TDesign 使用原生的 CSS Variables 来维护组件库通用 Design Token:
代码语言:javascript复制--td-brand-color: orange;
--td-warning-color: yellow;
--td-error-color: red;
--td-success-color: green;
这样在浏览器端更改这些变量的值就可以实时影响全局组件库样式。全部全局 Design Token 参见:
https://github.com/Tencent/tdesign-common/blob/develop/style/web/theme/_light.less
3.3 测试方案
前期在技术选型阶段,TDesign 也根据当时的社区流行情况及自身实际需求做了测试方案相关的调研,详细情况参见测试方案选型调研:https://github.com/Tencent/tdesign/wiki/Test-Selection
Mike Cohn 在他的著作《Succeeding with Agile》一书中提出了“测试金字塔”这个概念,金字塔里面包含四类测试:单元测试,集成测试,端到端测试以及手工回归测试。因其比较合理的分类,TDesign 测试方案也主要参考了这几种:
单元测试,大家都比较熟悉,主要用来检测一个模块、一个函数或者一个类是否正常工作的,属于白盒测试。而集成测试则是在单元测试的基础上,将各个模块组装后进行测试。有些模块单独工作可能没问题,但组装在一起之后却不一定能正常工作,因此,对于某些模块或场景,集成测试也是非常必要的。
除了白盒测试,还有黑盒测试,最常见的黑盒测试便是手工回归测试,这类测试是几乎所有项目都会进行的测试,此处不再赘述。我们再看另一种黑盒测试,端到端(E2E)测试,这类测试则是站在用户的角度进行,无论内部实现如何,API 是什么,都没有关系,唯一关心的就是 UI 运行结果是否符合需求预期。
测试方案是一个整体,从不同的维度编写测试用例,各司其职。越是底层的测试用例,涉及到的内容就越单纯,影响范围越小,测试用例写起来也就越轻松。越往上层,影响测试用例失败的原因就越多。因此,应该更加重视底层的单元测试书写,它是保障上层测试精简必要的重要因素。能在单元测试中就开发的用例,就一定不要写在更上层的测试分类中。上层端对端测试用例和手工测试用例理应越来越少,如此,可避免后期测试代码维护成本太高。
目前各组件库除了 Angular 技术栈使用了官方推荐的 Jasmine,其他技术栈均使用 Jest 作为主要测试工具。基本测试脚本目录如下:
代码语言:javascript复制// 例:https://github.com/Tencent/tdesign-vue-next/blob/develop/test/ssr/ssr.test.js
test
├── e2e
├── ssr
│ ├── __snapshots__
│ └── ssr.test.js // 组件库整体服务端渲染脚本
└── unit
├── affix // 单个组件测试脚本
└── ...
仓库中的每次 PR 提交都会触发以下测试校验:
3.3.1 单元测试
目前主要单元测试用例都依赖贡献者手写维护,对组件所有 API 及交互等进行测试,我们正在逐步改为自动生成 手动维护的方式:
- 对于 API 相关的偏静态检查的部分,如 API 是否实现、枚举值等通过工具自动生成各技术栈测试脚本
- 开发者手动维护对组件交互等比较复杂的测试用例
例:https://github.com/Tencent/tdesign-vue/blob/develop/test/unit/button/index.test.js
3.3.2 集成测试
这部分测试主要依赖于组件在官网实现的各类 demo,脚本会对比每次 pr 时组件 demo 的 snapshot 是否有变动,如果 snapshot 未通过则会 block pr review,需要开发者检查本次变动是否符合预期。
例:https://github.com/Tencent/tdesign-vue/blob/develop/test/unit/button/demo.test.js
3.3.3 SSR 测试
组件可能会在非浏览器环境内渲染,这通常会在服务端渲染直出等场景下出现,为了防止组件实现中在错误的时机使用了浏览器环境下才支持的某些 API,我们也通过对组件 demo 的 snapshot 比对进行了 SSR 测试:
https://github.com/Tencent/tdesign-vue/blob/develop/test/ssr/ssr.test.js
目前我们还缺少对组件库 e2e 的测试,在完善自动单元测试方案且组件库整体测试覆盖率达到我们期望的标准后,我们会启动增加 e2e 测试相关的尝试。
后续我们还会有关于组件库测试方案更详细的介绍文章,敬请期待。
3.4 跨框架实现方案
TDesign 官方支持了众多前端开发技术栈,怎样才能保证这些技术栈框架的实现都保持一致?我们总结了以下三种方式来实现目标:
- 交互和 UI 实现复用
- 组件 API 统一维护
- 部分代码复用
3.4.1 3API 设计
组件 API 设计是组件库开发的核心,TDesign 官方提供了多个技术栈实现版本,我们通过一系列工具和流程来保障各实现产物一致,组件的描述文档、type 定义等文件均通过工具自动生成,开发者无需手写维护:
在制定 API 时我们会更看重一致性和可扩展性原则:
- 用户使用是否体验一致,不应当存在例如 A 组件 size 枚举值为“small/ medium/large”,B 组件为 “small/default/large” 的情况,增加用户记忆成本
- API 描述应该尽量与技术栈实现无关,Vue/React 技术栈都支持用户自定义内容的部分,TDesign 中统一表述为抽象的 TNode 概念
- API 数量并非越多越好,尽量通过 API 的扩展性或组合来实现更多场景需求
详细 API 指引见:https://github.com/Tencent/tdesign/wiki/component-api-guide
3.4.2 代码复用
目前,业界上主流的组件库如 Ant Design 或 ElementUI ,都是主要维护单一框架,字节开源的 Semi Design 采用了 Fundation 的模式:大致的实现思路是通过抽象通用、无框架、无 UI 操作的类:Fundation,然后执行不同框架实现的 Adapter。
我们没有额外提出自己的抽象,而是采用业界已经比较流行的技术方案通过分层的方式在不同层级复用代码:
- 基础层
- 应用层
- 框架层
其中,基础层是与框架无关,与 UI 无关的纯逻辑计算相关的代码。
应用层,则可以在不同框架之间打通,比如 Vue 和 React 使用 hooks 的分案来做到两个框架直接的复用,其中可以复用基础层的方法,技术栈的差异如 Vue 的 Composition API 与 React Hooks 在这一层抹平。
框架层,则是需要基于不同的框架去实现代码。这一层则需要做到同框架不同终端之间的复用,如 Vue 和 Vue Mobile,甚至是框架,不同版本之间的复用,如 Vue 2.x 和 Vue 3.x 之间的复用。
这样做的优势是尽量少的引入额外概念,大部分组件逻辑还是使用 Vue 或 React 技术栈常用的语法来开发,在降低维护成本的同时,没有显著提高贡献者的技术门槛。
3.4.3 Composition API
TDesign 项目启动时 Vue 3.x 还未发布正式版本,公司内大部分线上项目也都还在使用 Vue 2.x,因此我们优先完成了适配 Vue 2.x 版本的组件库版本开发,后续随着 Vue 3.x 越来越成熟,我们新建了 :
https://github.com/Tencent/tdesign-vue-next
仓库来独立适配 Vue 3.x,这样可以在尽量减少重构对已经在使用 TDesign Vue 2.x 版本包的业务影响,也可以尽情享受 Vue 3.x 中的新特性,不必考虑兼容到低版本的问题。
但上述 Vue 3.x 仓库建立之初组件大部分从 vue2 直接搬运过来,大部分仍在使用 optionsAPI,且随着 TDesign 内组件种类越来越多两个仓库的维护成本也在逐渐增大,人工同步、转换代码也比嫁繁琐,为此我们也在进行 Vue2 与 Vue3 的融合重构方案。
相关 issue 讨论:
目前整体 Vue 社区都在向 Vue 3.x 方向迁移,相关生态已经逐步完善,TDesign 也在整体向 composition API 方向重构,新组件都优先使用 Vue 3.x 实现。目前 TDesign 的 Vue 2.x 也已经引入了 @vue/composition-api,大部分组件已经用 composition API 重构,可以做到跟 Vue 3.x 实现版本共享大多数代码。
详细方案参见:https://github.com/TDesignOteam/tdesign-kit/wiki/vue2-vue3-composition-api
Vue 社区刚刚发布了 2.7 版本作为 Vue 2.x 的最后一个 stable 版本,已经支持 Composition API 的全部特性,我们也会逐步引导用户直接升级 Vue 依赖。
3.5 工程化实践
对外开源后,我们选用了社区更流行的 GitHub Actions 来作为仓库自动化执行软件开发工作流的,包括 issue 流转、pr 等。
以 issue 流转过程为例:
依靠 GitHub actions 的强大能力,我们将整个 issue 流转的过程完全自动化,用户提出的 issue 可以自动根据之前维护的组件负责人信息自动分配给对应的同学处理,相关技术栈的企业微信群里也会有实时消息通知和每日汇总 issue 列表,防止漏过处理。
相关 Action 配置参见各仓库 .github 目录:
https://github.com/Tencent/tdesign/tree/main/.github
四、后续计划
随着前端界新技术不断推陈出新,TDesign 也在不断调整更新自己的技术选型,目前在讨论或推进中的选型调整有如下方向:
- 组件样式由 CSS Variables Token 定义 Less 组件样式实现,尝试改为全部 CSS3 维护,将所有全局样式 Token 和组件层级的 Token 全部开发给开发者自定义使用,不再因样式预编译环节导致组件样式定制受限的问题。
- 使用 Vitest 替换现有 jest 方案:减少各仓库测试配置复杂度,统一通过工具生成一部分组件测试用例代码,为什么选择 Vitest 可以参考: https://cn.vitest.dev/guide/why.html
- 跨框架代码复用方案改造:现在很大程度上只实现了通过 Composition API 能力在 Vue2 和 Vue3 两个仓库间复用组件逻辑,接下来 TDesign 团队还会一直探索平衡技术复杂度/代码复用程度的合理方案。
如果你也对以上技术选型方案的改进计划感兴趣,欢迎在 discussions 社区:https://github.com/Tencent/tdesign/discussions
发起讨论,参与到 TDesign 技术栈长期演进优化的活动中来。
撰文:哲哥
主创团队:腾讯 TDesign Oteam