写在前头

前阵子一直和组里的小伙伴共同“造轮子”,开发并维护了一套 PC 端 React UI 组件库,经过了一段时间的折腾,组件库从之前的 0.x、1.x 再到最近发布的 2.0.beta 的一个过程,这其中很多东西值得拿出来分享和讨论,有好的有失败的,今天就把组件库开发过程中的 DEMO 实时重现以及后期文档站的建设的技术选型以及实践简单做一个记录总结和大家一起做一个讨论。

调研和选型具体细节,后面找时间再梳理输出另外一篇文章,这里介绍在使用 Storybook 5 的过程中的一些问题点以及经验分享。

截止文章编写时 Storybook 6 正在进行 rc 版,作者也经历过将之前项目的 5.2.x 升级到 5.3.x 过程的阵痛,网络上关于使用 Storybook 的问题文章也比较少,除了 Storybook 官网文档以外一些问题点记录,因此成文,感兴趣可以继续阅读。有关 UI 组件库的建设,这里不做深入讨论。(就是我们暂时先不讨论 WHY 的问题,本文只讨论 HOW。)

首先,个人总结 Storybook 几点优势,也是基于下面几个优势最终决定选型它的原因

  • 开发环境 预览环境整体基于 Webpack 构建,开发环境接近实际生产环境
  • 多面手 支持技术栈类型较多,可以支持 React、Vue 等等技术栈组件展示;
  • 代码即文档 无论是开发初期我们使用的 *.stories.js,还是后期因为要统一文档站说明和一些 UI,我们改用 MDX 重写了使用说明,不过大部分 API 列表还是基于 Props 的实现定义 Interface 中 JSDoc 自动渲染,非常方便;
  • 实时性 展示环境实时可交互,通过 knob 插件可以让使用者直接修改组件属性直接看到效果;

支持 TypeScript

官方文档,支持 TypeScript 编写组件,文档中罗列了几个选项,出于使用习惯以及 Babel 与 Microsoft 的合作关系,推荐使用 babel-loader 方式。(虽然它很慢,所以要尽量控制编译的范围。)

Tips

当使用 babel 编译 TypeScript 的时候,存在两个问题

  • 描述文件 d.ts 组件的描述文件如何生成,babel-loader 本身不具备这个能力。
  • BUG 文档站生成过程中,默认 storybook 配置了 devtool,导致即使当前环境是 production 依然会编译生成 sourcemap,也就需要手动将 devtool 改为 false,这里应该给 storybook 提一个 issue。文档见这里,有一句说明:注意:sourceMap 选项是被忽略的。当 webpack 配置了 sourceMap 时(通过 devtool 配置选项),将会自动生成 sourceMap。

关于声明文件的生成有两个方案:

  • 手动通过 tsc 本身具备的能力,结合 --emitDeclarationOnly参数和输出目录来只输出 *.d.ts
  • 后期我们重构组件库打包方式,组内的小伙伴使用 gulp 一样的道理通过单独编译声明文件并输出到与组件代码同级目录

上面的方案核心就是单独编译声明文件即可,即便是微软官方提供的例子也是单独输出声明文件,详见这里,如果小伙们有其他好办法欢迎留言讨论。

文档站构建优化

1
build-storybook  -c .storybook -o docs-static

从打包命令中就可以看到,构建的配置支持 .storybook 文件夹下配置文件,默认读取 main.js 作为构建的补充,这里完全就是遵循 webpack 的配置,比如这里 webpackFinal: async (config) => { ...} 这段代码中拿到当前构建的全部 config 配置对象,那么既然拿到这个对象,那理论上就可以调整整个的构建过程。(当然,Storybook 构建确实使用了很多的 loader 和 plugin )。

静态站的打包和部署就和一个普通静态站部署没什么区别,外挂一个 HTTP 服务即可。由于 storybook 默认将每一个 stories (也就是组件)构建时进行了分包懒加载,访问每一个 stories 的时候内嵌的 iframe 展示时都只会请求当前组件的内容,首屏加载的内容也不是很多,经过简单的优化基本就可以实现很好的首屏加载。

样式覆盖取代主题定制

当然静态站上的主题样式覆盖,Storybook 还做得不够开放,虽然它开放了定制 theme 主题的方式,但是仍有一些细节处无法完全定制,我们的实现方式是通过手写 css 覆盖解决。

  • 默认样式

默认样式

  • 覆盖改造后

定制覆盖样式

组件菜单排序

默认情况下,左侧组件导航区的排序是按照字母排序规则,但当你的“业务方”想要一个所谓“有规划、基于设计原则”的排序进行展示就变的有一些难度,Storybook 预留了预览的 API 中就提供了一个可以自定义排序的方式,详见文档

那么,需要我们做什么呢,那就是手动去实现一个简单的排序算法,让组件 stories 按照期望的方式进行排序。

这里简单描述一下个人实现思路:

  1. 数据准备阶段:将业务方期望的菜单分类好,我的做法是将期望菜单顺序拷贝至 Excel 中进行排序,之后复制回编辑器批量操作,制作一个简单 map 作为字典,作用单一就是根据 key 返回序号;
  2. 获取组件待排序属性:将每一个组件的 stories 中定义的 title 拿到用作排序时,作为 key 备用在字段中查询;
  3. 编写排序规则:这里就是一个简单的排序算法,两两比较即可;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// preview.js
...
storySort: (a, b) => {
/**
* 排序逻辑
* 将文档内容中所有内容去除中文、改小写,去空格,再去已经排序好的数据处理
* ex. 按钮 Button => button
*
* 排序规则 0: 相同位置 -1: 前 1: 后
*/
const aSinpleName = a[1].kind
.replace(/[\u4e00-\u9fa5\/]/g, "")
.toLowerCase()
.trim();
const bSinpleName = b[1].kind
.replace(/[\u4e00-\u9fa5\/]/g, "")
.toLowerCase()
.trim();
const aSortNum = getSortNumByMenu(aSinpleName); // 获取字典中排序编号
const bSortNum = getSortNumByMenu(bSinpleName);
// 自有组件,直接排在最后。PS. 非官方定义组件,二等公民无奈被放在最后
if (!aSortNum && bSortNum) return 1;
if (aSortNum && !bSortNum) return -1;
if (!aSortNum && !bSortNum) return 0;
return aSortNum > bSortNum ? 1 : -1;
},
...

addon-docs 插件

Storybook 中文档即代码功能的实现还是很有意思的,通过强大的 @storybook/addon-docs 插件(在 5.3.x 版本开始逐步废弃 @storybook/addon-info 插件,基本不再维护)可以实现文档中众多元素的直接渲染,我们经常用的组件有 import { Meta, Story, Preview, Props, Source, Title, Subtitle } from "@storybook/addon-docs/blocks"; 这些,其中着重介绍 PreviewProps 组件。

Props 组件

可以结合 JSDoc 自动生成文档,如下图:

感兴趣的同学可以抽时间研读了一下具体实现,源码地址

这里描述一下自己看源码总结的实现思路,借助 JSDoc 的 parser 读取每一个参数 component 中 props 的属性的注释内容,并且与当前属性建立对应关系后 render 一个显示的 table 就自动生成了下面的文档。

现存问题记录

  • 在暗黑模式的适配上还存在问题
  • 演示代码段,主题不可定制(查看源码知道虽然使用了 highlight 组件但是固定死了主题,并没有入口去修改)
  • 国际化支持不全,目前没有需求可以暂时不考虑

走过的弯路

插件选择失误,由于最一开始的选择错误,错误地选用 addon-info 已经被废弃的插件,导致后面升级后,在静态文档站需要定制化的需求出现时,原有 addon-info 出现的问题层出不穷,不得不切换至 addon-doc 插件,索性直接使用 mdx 对所有组件文档进行了一次重写,好在大部分代码和 DEMO 可以直接复用和自动生成。

写在最后

当然,我们目前并没有使用 Storybook 的全部功能,文章的最后也会列举调研期间收集到的非常牛团队开发的组件库和文档实例,那么我们可以学习到什么呢?

首先,我们所谓的重复造出来的轮子到底是有意义?答案虽然是肯定的,业界已经有很多成熟的案例,很多公司、部门、团队都可能有自己的库,早在我们在开始开发实现之初,但是我们为什么要做呢?这就是另外一个话题。我们不深入这个话题,仅从过程中讨论,我认为不仅仅只考虑组件的设计、易用性(API 等)、稳定性(质量)、组件的抽象、以及复用性等等这些问题,同时也应该作为组件使用者的角度来思考,从适用场景出发,当使用者决定是否使用某一组件时,组件的文档等等就是一个非常重要支持点,这里刚好 Storybook 帮我们解决了这其中的几个问题,看到这里感兴趣的小伙伴可以留言讨论,当你的团队或者新项目在选择 UI 库时基于哪些考虑?

再者,聚焦,Storybook 团队聚焦在组件的开发环境,单一功能强大通过拼接实现更多的功能,这种思想一定要多运用在实际情况中,但是也要注意区分场景,没必要成为“为了用而用”。

最后,UI 组件库应该是前端团队,最容易想到也是最难做好的一个 KPI 产物,这其中还有很多值得思考和讨论!

基于 Storybook 优秀实例

参考