闲鱼大终端UI组件库——FishUI建设之路

lxf2023-03-13 10:01:01

作者:闲鱼技术——灼升

背景

随着闲鱼前端架构的不断演进,一些关键技术设施需要结合业务特征逐步自建,技术方案也要拥抱社区来提升可扩展性。一方面, 闲鱼跨端开发框架kun 让前端开发者使用JS/CSS/HTML即可交付终端页面,同时兼顾了动态性和高性能,另一方面,前端UI框架也正从集团 rax 逐步转向社区 React 方案。

在这个大背景下,围绕 kun 和 web 两个容器的跨端组件建设也势在必行,因此 Fish UI 应运而生,它将全面拥抱 react 生态,并借助 kun 容器的能力,为闲鱼终端开发者提供一套高易用性和稳定性的跨端(kun & web)UI组件库。

FishUI 的最大的技术创新点在于跨端,它通过跨端组件的形式对齐 kun 和 web 两个容器的体验标准,让使用者更加专注于业务逻辑而非容器差异,从而实现为业务开发提效的目标。围绕着该创新和目标,一些可预知的技术难题也随之出现,比如组件库的工程结构如何组织、分端构建如何做、开发规范是什么样等等,这些问题将在下文中一一得到解答。

整体设计

分层设计

Fish UI 作为兼容 kun 和 web 两端的组件库,包含底层的kun容器组件和上层的跨端组件,其分层结构大致如下:

闲鱼大终端UI组件库——FishUI建设之路

其中【跨端组件】一层是 Fish UI 最上层的封装,是抹平 kun 和 web 容器差异的一层,同时也是业务开发者使用频率最高的一层。

理想情况是【跨端组件】要尽可能依赖底层【w3c规范组件】组合扩展而成,要足够丰富,而【kun 扩展组件】一层则希望尽可能做薄,因为其与kun 容器强绑定,是为了解决特定场景问题而产生的,不具备通用性。

本文后续也主要讲解围绕【跨端组件】这一层的相关建设内容。

跨端设计

FishUI 最核心的特点在于【跨端兼容 kun 和 h5】,也是与其他组件库最大的差异点。

通常情况下,业务开发者需要手动判断环境,分别实现两个容器对应的代码,比如渲染一张图片的代码如下:

闲鱼大终端UI组件库——FishUI建设之路

通过 FishUI,我们期望的调用方式应该是这样

闲鱼大终端UI组件库——FishUI建设之路

通过对比,可以看出 FishUI 跨端设计的核心功能在于:

  • 自动根据 window.kun 判断环境,执行对应环境代码
  • 自动抹平 kun 和 web 两端的属性差异,无需用户手动处理
  • 对外提供统一的 TS 接口声明
  • 减少模板代码量,提升代码简洁度

因此,FishUI 跨端组件的分端设计大致如下(以 image 为 例):

闲鱼大终端UI组件库——FishUI建设之路

技术选型

语言

我们选择了 react + typescript + moduleCSS 作为组件开发语言。

其中样式方案我们使用了 moduleCSS,主要原因是为了避免多人协作开发时存在的样式污染问题。

虽然 【less/sass + 统一前缀】也可以解决样式污染问题,比如规定所有组件类名都是 fish-ui-xxx 的格式,其他样式都在该类名的作用域下。但是由于没有对类名进行 hash ,导致组件使用者在开发过程中可以通过类名来覆写组件内部元素样式。这个行为是一把双刃剑,好处是组件样式扩展性好,使用者遇到组件样式不满足需求,很容易覆写组件内部样式。坏处则是组件失去了向后兼容的特性,比如使用者自定义了 .button-inner-text 样式,后来某天 Button 组件内部dom结构变更,或者类名调整了,所有使用者的自定义样式都会失效,容易造成线上问题。

权衡利弊,最终确定了 moduleCSS 的方案,保证组件内部样式,不允许使用者随便侵入,如果有自定义样式需求,可以通过增加组件属性,或者 css vars 的方式来给使用者开口子。

目录结构

采用了 monorepo 的单仓多包管理架构,一个组件对应一个 npm 包。

相比于【多仓多包】和【单仓单包(多组件一个包)】,monorepo 同时具备以下几点优势:

  • 使用时,支持按需引入单个 npm 包。
  • 开发时,按照目录隔离,各包独立迭代发版,避免中心化的协作方式,同时兼顾一次性改多个组件的开发体验。
  • 在集团内部搭建场景中,目前只能做到包粒度的 treeShaking,因此更细粒度的多包会比一个大整包会更有加载优势。

至于单个 package 的结构,我们则直接遵循了集团内 ICE PKG 的规范,它提供了单 React 组件的开发/调试/构建/发包的全套方案。

闲鱼大终端UI组件库——FishUI建设之路

依赖管理

整体选用 pnpm 进行依赖包安装管理,使用 lerna + nx 进行多包版本管理 & 脚本执行管理。

包管理方案 pnpm

在 monorepo 场景下,npm/yarn 等包管理器等都有自己的 workspace 规范,对于多包的依赖,可以扁平化提升到最外层的 node_modules 目录下,解决了依赖重复安装的问题,但是又带来了其他的问题:

  • 幻影依赖:由于依赖扁平化到最外层之后,包 A 可以直接 import 包 B 的依赖。
  • 分身依赖:指多包依赖了某个包的多个版本,只有一个版本可以提升到最外层 node_modules 下,其他版本还是会嵌套安装,导致重复安装,如下图中 B 包就被安装了多次。

闲鱼大终端UI组件库——FishUI建设之路

  • 项目重复安装:比如某个依赖在 10 个项目中使用,就会有 10 份代码被安装到磁盘中。

以上问题,都可以在 pnpm 中解决:

  • 多项目重复安装问题:pnpm 会全局缓存已经安装过的包,然后 硬链接 到项目中的 node_modules 目录下,避免了重复安装的问题,且大大提升了安装速度。
  • 幻影依赖 和 分身依赖问题:pnpm 安装的依赖并非是扁平化结构,而是通过 软链接 的形式链接到 node_modules 目录下的 .pnpm 中,这样就同时避免了幻影依赖和分身依赖的问题。

包版本方案 lerna + nx

lerna 为比较通用的 monorepo 管理工具,既能进行版本管理,又可以兼顾构建工作流、发布包等流程方面的管理,已经是业界非常主流的多包管理方案。

鉴于单npm包修改后的版本更新,我们不希望影响到其他未修改的 npm 包的版本,因此我们版本管理策略采用 independent 模式

闲鱼大终端UI组件库——FishUI建设之路

此外,lerna 6.x 可以配合 nx 来优化整个工作流程,具体表现为:

  • 缓存脚本结果,避免重复执行,可以显著提升脚本运行速率。
  • 基于多包的拓扑结构来执行脚本,比如 a 包 start 执行前,会自动先运行依赖的 b/c 包的 build 命令。
  • 可以提供图形化的多包依赖拓扑图。

闲鱼大终端UI组件库——FishUI建设之路

分端构建

分端构建保证了业务侧不会加载冗余代码,提升页面加载体验。

默认情况下,每个组件都采用了 ICE PKG 自带的构建方式,使用 rollup + swc 来构建出 esm 产物。

最终一个组件 npm 包的入口文件 index.js 大致如下:

闲鱼大终端UI组件库——FishUI建设之路

由上可以看出,组件本身没有提供分端构建,而是在运行时通过 window.kun 来区分是 kun 环境,还是 web 环境,从而执行对应环境的代码。

这对于极致的加载性能是个不能妥协的巨大问题,因此我们在内部统一的构建工具fish-app中,增加了分端构建的能力,这样在所有业务场景中使用 Fish UI 组件的时候,最终产物就只会包含特定环境的代码。

分端构建的核心逻辑是:

  • 在 webpack 构建过程中,新增自定义 loader,在该 loader 中使用 @babel/parser 分析源码 ast,并将 window.kun,转化为 true 或者 false(取决与是否是 kun 环境)。
  • 然后结合 webpack 的 treeShaking,就会自动剔除 deadCode。

上面的代码,在构建 kun 环境代码时,经过第一步 babel 的转化后,就是这样:

闲鱼大终端UI组件库——FishUI建设之路

然后经过第二步 webpack 的 treeShaking 后,变成这样:

闲鱼大终端UI组件库——FishUI建设之路

这样在特定环境,就只会加载对应环境的代码,实现了分端加载。

自动化测试

单元测试是保障组件质量和稳定性的重要一环。

ICE PKG 本身并没有提供自动化测试相关能力,因此我们基于 React 官方推荐的工具链来补充单元测试的部分,使用 jest 作为测试框架,使用 React Testing Library 作为React组件测试工具集。

Enzyme 也是非常流程的 React UI 测试工具,但是我们依旧采用了 React Tesing Library 的主要原因是:React Testing Library 更加推崇站在用户的视角去测试,Enzyme 则更加带有开发者视角,从二者的 api 设计上就可以看出一二,React Testing Library 几乎屏蔽了所有侵入到组件内部实现的api,正如用户一样,用户是不会在乎组件的内部的 props/state 是如何变化的,只有开发者在乎。所以 React Testing Library 成为了我们最终的选择。

基于以上,一个简单的组件单元测试,会是如下的效果:

闲鱼大终端UI组件库——FishUI建设之路

代码规范

代码规范在多人协作开发的场景,可以大大提升代码的可维护性,从而保证高效响应每一次的业务需求。

我们遵循闲鱼前端统一代码规范,包括如下:

  • 编码规范 (包含 HTML/JS/TS/React 等规则)
  • commtlint 规则

这部分也可以参考 f2elint,这是《阿里巴巴前端规约》的配套 Lint 工具,可以为项目一键接入规约、一键扫描和修复规约问题,保障项目的编码规范和代码质量。

API 设计规范

除了代码规范外,我们还约定了组件 API 的设计规范,比如 api 命名风格等,有利于形成统一的使用心智,提升易用性

组件 api 主要包含两部分:

  • 固定属性,每个组件都要有的属性
  • 自定义属性,组件特有的属性

固定属性

保留 className 和 style 属性,方便样式使用时扩展。

闲鱼大终端UI组件库——FishUI建设之路

自定义属性

闲鱼大终端UI组件库——FishUI建设之路

其他 API 设计建议

  • 组件应该具有可组合性(类似 HTML 标签,具有语义化作用)

闲鱼大终端UI组件库——FishUI建设之路

  • 组件的受控与非受控
    • 对于受控属性,应当提供类似 value + onChange 的组合
    • 对于非受控属性,应当提供类似 defaultValue 属性
  • 组件应该具有可扩展性
    • 合理使用 children 和 renderFunction
  • 合理使用缩写
    • 非必要不缩写,减少误读率
    • 但是对于一些常见的缩写规则可以保留,比如 text4Title,date2String 等。

文档站点

文档站点是展示组件库的窗口,良好的文档可以提升组件库整体的易用性。

在 FishUI 中,单个组件的文档产出为 markdown 格式,我们采用 docusaurus 来动态生成组件的文档站点,主要基于以下几点原因:

  • 基于 React 技术栈,社区插件多,生态成熟,比如 React/Webpack/Midway 等众多站点都是基于 docusaurus 构建。
  • 扩展性好,支持 mdx,允许在 markdown 中无缝插入 jsx 代码,还可以 import 组件。
  • 支持文档搜索,基于 algolia。
  • 跟集团内部结合比较好,比如
    • 是 ICE PKG 的默认文档方案
    • DEF(集团前端工程基座)中有基于 docusaurus 的 FaaS 静态站点方案

除了 docusaurus 默认提供的 markdown 渲染能力外,我们还扩展了如下能力:

  • Playground ,可以编辑组件示例代码,并实时预览
    • 原理是基于 @babel/standalone 来对用户代码实时进行编译,然后使用 new Function 动态执行编译后的代码。
  • KUN 和 h5 的扫码预览能力
    • 原理是分别构建组件示例在 kun 和 h5 下的产物,并生成二维码注入到原本的 markdown 文件中。

最终单个组件的文档如下:

闲鱼大终端UI组件库——FishUI建设之路

难点和挑战

随着组件开发进度不断推进,也暴露出了开发层面的一些问题:

  • React框架的事件机制不支持在kun标签上绑定自定义事件,例如不支持在 kun-image 标签上直接增加 onAppear 属性**。**这个问题可以手动调用 dom 的 addEventLisenter 来绑定,也是目前我们选择的解决方式,但是这种办法略微繁琐和笨拙,后续计划采用运行时方案,hack 整个 React.createElement,劫持组件的props并动态绑定事件和增加属性。
  • 跨端的属性适配问题。kun 和 web 两者本身就有很多本质差异,一些属性可以 kun 向 w3c 标准适配来解决,但是有时候强行抹平这些差异将付出很大的开发和维护代价,另外 kun 从一开始就不试图去达到完备的 w3c 标准,因此我们的跨端组件允许针对 kun 和 web 分开设置属性,最终能达到一致的效果即可。
  • 跨端的单测问题。基于 jest + jsdom 框架,单测无法覆盖到 kun 环境,kun 端的组件大多是 kun 标签的桥接实现,因此这部分单测暂时由客户端保证,后续计划前端增加对 kun 环境的 mock 。

除了以上开发层面的问题外,项目推进的最大难点是在闲鱼终端融合的初步阶段,客户端和前端同学看待问题的视角存在差异,技术栈也未完全对齐,因此单一组件很难由单一同学独立负责,导致开发维护和沟通的成本较高。另外为了保证组件库的标准化能力,需要大量的规范约束和工程能力支撑,比如 api 设计规范、交互视觉规范、代码开发规范、统一的脚手架能力、统一的 CI/CD 能力等,这些内容并不是一步到位,而是随着各方协作者的实践和反馈而持续迭代完善,这个过程注定会非常曲折。

总结与展望

FishUI 刚刚起航,目前我们产出第一批组件刚刚进入业务试用的阶段,FishUI 的引入将使得跨端工程的开发相比之前大大提效,主要体现为:

  • 跨端体验标准更加统一
  • 跨端的模板代码量减少
  • 跨端适配问题变少,开发时间缩减

但是放眼未来,FishUI 还有仍旧很多的工作要做,包括:

  • 加强与视觉交互的联结
  • 跨端的开发和调试体验优化
  • 多前端框架支持
  • 更多的业务场景落地

此外,随着闲鱼终端融合的不断深入,我们期望 FishUI 能够成为前端开发者和客户端开发者学习彼此的实验场,最终能有更多终端开发者借助 FishUI 的场景成长起来,并助力 FishUI 更好地向前迭代。总之,FishUI 未来的路还很长,相信终有一天,它将出现在闲鱼终端的各个线上场景,真正助力闲鱼业务高质量向前发展,敬请期待~