前言
无论是在前端刀耕火种的 jQuery/YUI 时代,还是到现在基于数据驱动 UI 的 React/Vue 时代,物料/组件一直是前端永恒的话题。基于大量重复逻辑的封装可以很显而易见地提升前端 UI 的构建效率,简单而直接,因此无论技术栈如何变化,物料工作都是排在各个前端团队的首要位置解决。
在 2021 年的现在来看,基于 React/Rax 体系下的基础组件体系已经基本完善,既有蚂蚁良好设计语言的 AntDesign[1],也有集团基于 DPL 快速定制的 Fusion[2](阿里中后台 UI 解决方案,已开源),在基础组件的层面功能日趋完善,各个业务团队之间在这个层面的低级重复建设也越来越少,这是非常好的结果。但在业务组件体系的构建上,目前还呈现着百花齐放的局面,由于技术栈的不断扩充(可视化、小程序等),业务组件的开发上还存在着很多诸如工程体系混乱,开发链路不通的问题。
来到企业智能的 5 年多的时间里,经历了团队物料体系从最初的 Arale/kuma,到基于 react-component 的 UXCore/SaltUI,再到现在全面与 Fusion 融合。基础层面的变动也带来了业务组件领域的工具链的不断变化。写这篇文章,一方面记录一下自己在这方面做的一些工作和中间的思考,另一方面也希望能在社区里获得大家的一些宝贵建议,以得到一些新的启发。
一 一个业务组件要开发几遍?
1 困境
某天,同事 A 找到我,说 TA 的业务里需要构建一个业务组件包,涵盖了 PC 端,小程序和对应的可视化组件,当时我正在做一些关于前端业务能力构建的相关工作,所以想来问下我的建议。这个问题看似是很简单,但实际分析下来却发现有很多问题。
首先,PC 的业务组件当时是使用 飞冰[3](iceworks,阿里 GUI 构建工具) + deep 脚手架模板(deep 出自企业智能用户体验团队)的方式来开发的,好处是可以和 Fusion 深度打通,发布和同步物料到 Fusion 对应的 deep 站点都比较方便。小程序/移动端的组件,当时基于 Rax 的动态化小程序组件方案 Fusion Mobile 和 Deep Mobile 刚刚起步,业务组件的开发还没有自己的标准,唯一一套可用的是之前政务钉钉的前端团队做的 gdt-utils 来承载。
而可视化组件,则是由乐高(阿里企业级可视化搭建平台)团队提供的 vdev 工具链,而当时还有一个问题是,由于 PC 端基于 React 框架,而小程序/移动端则基于 Rax 框架,导致可视化开发时也无法通过初始化一个包来完成 PC 和小程序端的开发。
总结起来,开发一个涵盖三种模式的业务组件,需要适应和学习三种工具链,初始化四个包,发布四个 npm package,而后续使用中,使用者需要记住四个包名以及他们对应的使用场景,和在至少两个地方看他们的使用方法。同时在维护的阶段,PC 和 移动端相似的模型、请求、数据处理逻辑也无法得到复用,如果想要复用怎么办?不好意思,那就需要再发一个工具包在几个包之间做流转。这些都在无形中增加了开发一个业务组件的成本,同学们的精力就在这些框架、包和联调的过程中消耗掉了。
2 融合开发
虽然从结果来看,这样子可以完成开发,但从上手门槛和开发效率上来说确实不如人意,一是需要写大量的文档来把这件事情说清楚,二是仅仅为了开发一个业务组件,需要这么大的成本真的是一个好的方案吗?那可不可以不要那么多东西,就一个工具链,一个包解决所有问题,这样不好吗?好是好,但真要这么做之前,有很多问题需要解决。
最大的问题在于,如何将 Rax 和 React 两种框架有机结合在一个工具链中。这看似是一个不怎么困难的问题,貌似是只要把以前的 React 和 Rax 的 webpack 配置拿过来各跑各的就行了,但融合之后有些东西是合成一份的,比如 demo 文件,对于一个希望融合开发的同学来说,自然是不希望,写几份几乎相同的 demo,仅仅是因为不同的运行框架带来的引用不同。相似的还有可视化组件当中的 prototypeView 文件,这个文件用于在乐高设计器中渲染组件,这里需要一点乐高组件开发的知识,prototypeView 文件的大致代码组成是这样的:
这样的设计是在可视化组件都是 React 框架的前提下设计出来的,在以往的实践中是没有问题的。
但融合开发后,就像上面代码中的注释一样,如何驱动一个 React 组件和一个 Rax 组件混合打包,并且可以正常渲染,需要有解决的办法。其次,从工程的角度,选取哪个工具链来实现这个融合开发的能力?显然目前的每条工具链都不具备直接实现的能力,那是选取其中某一个进行增强,还是另起炉灶结合三者的能力?各有利弊,需要权衡。还有一个必须考虑的点,就是如何尽量减少对开发者原有心智的改变,以及尽量保持对 Fusion 物料能力的匹配(如同步到 Fusion 站点),这样才可以让开发者以尽量少的代价迁移到新的开发模式上来。确认了这些问题之后,我们开始着手通过融合开发来解决这个开发困境。
3 着手解决
确定目录架构
首先我们需要确定融合开发包(后简称“融合包”)的目录结构:
从源码结构上,我们基本沿用了可视化组件的文件结构,这样一方面满足了可视化组件特殊的要求(如设置器的配置,设计态的渲染等),另一方面通过制定 package.json 的 main 字段,也可以实现直接引用包名调用组件的需求。
而技术栈上则全面使用 ts 和 scss,和 Fusion 组件对齐,同时方便使用 scss 变量和 css 变量。
在构建产物上,我们则结合了几个工具链产物的特点,首先构建 babel 转译过的 es5 文件和对应的声明文件,用于一般项目中。其次构建保留的 import/export 的 esmodule,用于对包体积有要求的项目做 treeShaking。而后,将对应的 md 构建成可直接浏览器预览的 demo 放在 build 文件夹中与 Fusion 组件对齐,再将 lib 文件夹下的文件复制到 build 目录中,方便乐高构建和识别(乐高构建的时候会直接去找 build 目录下的 view.js 和 view.mobile.js 等文件)。
工具链的选择
在工具链的选择上,我们最终选择了乐高的开发套件 vdev 作为底座进行扩展,这样做的考虑是可视化组件的整套开发体系较重,且未插件化,抽取迁移的成本较高。其次,相比其他两套工具链,vdev 在大团队的基础架构部门维护,沟通协作相对简单。剩余的部分我们并没有完全重造,而是尽可能地复用起现有的 build-scripts 体系下的插件。我们首先对 vdev 进行了改造,支持了 build-scripts 相关体系的调用,剩余的开发则基于 build-scripts 下的 plugin 开始开发,最终的大致链路如下:
pc.json 大致如下:
在开发的时候我们定下了一个原则,基于通用规则下的 PC、Mobile 和小程序预览,我们尽量通过官方的 build-plugin-component 来实现,不支持的功能或 bug 通过共建补齐或修复,而针对融合包的特殊规则,如融合需要的特殊配置处理,构建后的文件转移等逻辑,通过 build-scripts 体系下后序插件(我们这里命名为 build-plugin-vdev-component)可以对前序插件定义的配置做进一步处理的特性来增加。这样既减小了后续的维护成本,又可以通过 build-plugin-component 帮助到更多的业务。
确定好基础的架构之后,我进一步定义了融合包相关的 npm scripts:
- start:用于启动可视化部分的调试,包含设计器和预览
- startPC:用于启动 PC 部分的调试
- startMobile:用于启动移动端 Web 部分的调试
- startMiniapp:用于启动移动端小程序部分的调试
- build:用于生成上文提到的构建产物
其中除 start 外,其他都依靠 build-scirpts 插件体系来完成。
调试主要解决的问题
大致框架都确定后,就要开始具体的开发流程。start 相关的流程需要修改的不多,主要是把组件名 alias 到对应的入口(PC 到 view.tsx, Mobile 到 view.mobile.tsx),剩下的主要是补全 build-plugin-component 中缺失的功能,比如 Rax 部分里对 scss 的支持,inlineStyle 配置的支持。有个值得稍微提一下的点是关于对 demo 文件中 jsx 的处理。我们都知道 jsx 是 React 提出的 js 的加强型语法,用于将模板可以完全使用 js 语法来描述,实际在浏览器是不能直接识别的,一般的处理方式是通过 babel 转换成对应的转换语法,如把
转换成:
对于的 jsx 标签如
demo 文件的写法上是基于 PC 的视角的,引用的依赖是 react 的 Component 和 createElement,以及基础组件库 @ali/deep。在启动小程序调试时,react alias 到 rax,@ali/deep alias 到 @ali/deep-mobile。而 jsx 相关的,如
构建主要解决的问题
在 build 的过程中,由于要同时构建出 PC 和 Mobile 的 demo,而 build-plugin-component 只能启动 Rax 或者 React 其中一边的构建,显然这无法满足我们的需求,如果调用两次脚本,中间又存在着大量的重复操作,如文件夹的新增和删除等等。我们选择的是使用 build-plugin-component 的 rax 部分,并在后序插件 build-plugin-vdev-component 里另起一个 React 的 webpack task,同时完成 PC 和 Mobile 的 demo 构建。
在构建 es5 的文件时,也遇到了 jsx 的编译问题,由于源码中包含了一部分 react 文件也包含了一部分 rax 文件,在 start 时还可以按照 entry 的依赖链来设置 babel 配置,但是 build 时对源文件只过 babel 没有 webpack,所以要使用其他的办法来处理。我们采取的方式是先分析每个文件的 AST,从文件的包引用上分析出该文件是一个 React 还是 Rax 组件,然后采用不同的 babel 配置来进行编译。这里为什么没有采用和上文提到的 demo 里的同样的方式进行处理呢?原因是 demo 的方式对 createElement 的引入是强依赖,写法上比较受限,在源码部分我们希望能够让用户拥有更灵活的使用方式。
这样基本上在 ProCode 下的场景都可以正常使用了,但乐高环境下还不行,主要还是处在 prototypeView.tsx 这个文件的处理上,上面我们提到过,这个文件会同时引入 React 和 Rax 组件,虽然在设计态,只会有其中一个组件被真正渲染出来,但有些 API 是在组件类的声明时就会用到,例如 Rax.forwardRef。这就导致不做任何处理的情况下,在设计态会因为缺少 API 而报错无法使用。而乐高的组件保存构建,和一般的 Procode 项目,是每个组件单独构建的,而组件在构建时并不知道自己是要在乐高 PC(React 环境)还是乐高小程序(Rax 环境)里使用。所以我们在组件本地构建时会额外生成一个 prototypeView.rax.js,这个文件在内容和构建后的 prototypeView.js 并没有本质区别,但是提供给乐高打包构建一个额外的入口,乐高构建时探测到是一个融合包时对这两个文件入口配置不同的 webpack alias,在 prototypeView.js 里会将 rax 相关的 api alias 到 remaxjs 社区的 rax-compact 包上,用于乐高 PC 版设计态的展示(React 环境)。在 prototypeView.rax.js 中将 react 相关的 api alias 到 rax/lib/compact 上,用于乐高小程序设计态的展示(Rax 环境)。
4 最终效果
最终我们将业务组件的开发和使用过程,统一到了一个包(融合 vc 包),一个工具链(vdev:初始化、调试、发布),对 @ali/deep 和 @ali/deep-mobile 这两个基础组件包实现按需加载编译(也支持 Fusion/Antd)。使用的时候,无论 PC 端、小程序还是可视化的部分,都可以引用同一个包,没必要去记住不同的包名和他们对应的平台。正常情况下,我们可以通过直接引用包名来引入 PC 部分,通过 es/view.mobile(或者 lib/view.mobile) 引入移动端/小程序部分。另外,这里我们在 package.json 里将对应的入口也做了注册,这样可以通过 webpack resolve 配置也可以实现通过包名就直接引用移动端部分的效果。
二 重复的过程是否可以不做?
1 困境
融合开发的第一个版本,解决了开发者在项目工程维护、发布和使用者使用包时的耗费心智的问题,同时对于在 PC 和 Mobile 复用逻辑上有较大帮助。但开发者在开发过程中仍有很多重复劳动的过程。比如虽然已经有了完整的 ts 支持和接口声明,但是用户还是需要自己配置组件的 setter,配置 setter 组件的过程主要是指定 setter 对应的属性,默认值和类型。比如组件发布之后,还需要通过一套入驻机制,才能在物料中心给自己和他人查看 Demo。比如小程序调试,每次本地启动构建后,还要打开小程序的 IDE。这些都是每个业务组件开发中不得不做,但基本重复的工作,凡是重复的事情都是有规律可言,这些重复的事情能否不做?
2 自动生成 prototype.tsx
前面也提到融合开发包里面 PC 和 Mobile 的开发入口文件已经全面 ts 化和模板化了,入口文件大致如下(这里展示的是 PC 入口 view.tsx,Mobile 与此类似):
通过 AST 分析,我们可以定位到所有的 interface 声明以及 defaultProps 声明。typescript lint 中要求一个文件中只有一个 Class 声明,再加上 Component 的 ts 泛型,这些前提条件让我们可以定位到 props 的接口,从而得到每个 prop 的 name 和类型。考虑到业务组件的最重要目的是能够快速在乐高里使用起来,所以我们没有必要对每种类型和默认值做特别精细化的映射,我们可以为基础类型找到对应的 setter,对一些复杂的类型指定为一个 JSONSetter,对渲染函数钩子指定为 ActionSetter,对回调事件指定为 events。prototype.tsx 的模板文件也是一个比较标准的写法,因此我们可以比较方便地通过 AST 分析出已经定义过的属性,这些属性不需要再增加一遍,以及插入新属性代码的位置,通过直接生成这部分设置器代码,我们的组件基本上可以在不需要修改的情况下在设计器里正常使用起来(指可以配置各个属性,流畅地使用组件的各个功能,而非仅是展示)。
3 发布自动同步物料中心
组件发布只是第一步,一个业务组件只有能够被发现,文档可以被阅读,才是组件服务的开始。在企业智能,我们目前统一使用物料中心进行组件的展示。过去的业务组件只能通过物料中心后台配置的方式才能上架到物料中心,这个过程需要大量的手动填写的东西,耗时耗力。这部分我们也做了优化,在组件发布到 tnpm 后,vdev 来收集上架必要的信息,如组件 owner,组件分属业务领域,组件适配的端等等,物料中心提供 API 调用,直接将这些信息同步至物料中心,实现一键发布+上架物料中心的效果。当然,这个同步并非强制,对于一些测试版,可以选择不进行同步。
4 小程序 WebIDE 调试
原有小程序调试必须要在本地构建出一个符合小程序结构的项目目录,然后再使用小程序 IDE 打开对应目录才能开始调试,这个过程跨越多个工具链,学习成本和操作成本都比较高。恰好支付宝小程序团队推出了 @ali/mini 工具链,可以基于项目目录下一键启动 Lyra 浏览器模拟器进行调试。我们融合开发包也对 @ali/mini 做了集成,@ali/mini 需要的配置文件自动帮助组件开发者生成,通过一个命令就可以直接启动 WebIDE 进行调试了。
三 如何简单地开发/使用一个带服务的组件?
1 困境
上面两部分,我们主要围绕的是一个纯 UI 的业务组件开发,但我们也知道很多业务模块都是需要配合后端服务一起使用才能完整表达一个场景,最简单的例子就是一个搜人组件,不搭配服务就是一个列表样式有点特殊的选择组件。
对于这类组件,以前有两种策略,一是搭配一个跨域的 jsonp 接口,但随着目前安全越来越收紧的情况下,这类接口越来越少了。另一个就是搭配一个业务上的接口,但这类接口和组件联调非常麻烦,因为接口只有在对于的业务域名下才能使用,因为这个限制条件,就分出了两种不同的思路,一个是要在业务域上做个页面出来或者干脆直接在业务项目里去调,更有甚者,前端直接把后端服务起起来本地调,想想都感觉很麻烦,新接手的同学估计光搭个环境就要花很长的时间。
另一个策略就是前端使用 mock 的接口或者数据进行调试,这类环境上相对简单一点,但带来很大的联调成本,首先 mock 接口有时很难模拟线上真实数据,其次谁去一直维护这个接口一直和线上保持一致,带来了额外的工作量。开发不方便,使用也不方便,由于接口是业务自己的,导致本地或者乐高里启动调试的时候,接口无法调通,只能花力气部署到业务页面上才能使用起来看效果。或者是选择组件里不默认携带服务,真正使用的时候再去配对应的接口,就出现了拿着组件去找接口的情况,接口本身有很多种形式,能 run 起来的方式也不一样,能真正用起来看效果,也要花一段时间。
2 服务调用
上面的分析中可以看出,这类组件的主要难点在于服务的调用上,在没有跨域请求接口的情况下,本地开发举步维艰。所以核心问题在于提供一类服务,可以方便地在本地或者乐高环境调用,同时不用担心安全问题(数据泄露和服务提供方压力等),这就需要一个 API 网关来,这个网关可以解决服务调用的鉴权,以及对服务提供方进行保护。有了网关的情况下,下一步要解决的本地和乐高如何调用的问题,乐高调用相对来说比较简单,乐高本身是有后端服务的,只需要提供一个过程,在用户接入某个组件和服务的时候,到网关去自动申请日常服务的调用,由于日常服务大多已做了数据脱敏,且与真实数据隔离不能直接用于生产,所以日常服务的订阅申请和审批都是相对比较简单的,这个过程可以在开通组件的背后直接完成。本地调用则相对会比较麻烦,一般本地调试都是 node 起一个本地 server(如 webpack dev server 等),背后很难直接完成接口的调用。这里有两种解决思路,一个是服务端提供一个日常的跨域调用的转发接口,用于转发网关服务,如 /api/gateway?id=epaas.api.key,另一个则是服务端提供一个模板页面,前端 server 提供 js 和 css,并注入到对应的页面中,这样直接请求对应的接口即可。
3 实践
在 EI 的实践中,恰好有这样一个业务网关 ePaaS 来承载网关的职责,在乐高的使用侧,我们设计了一个业务能力模块来做对应的组件和服务开通,ePaaS 是通过应用间的服务订阅来实现跨业务调用的,所以我们在开通业务能力前会先让用户填写自己的 ePaaS,其实乐高预览时这些是不需要的,让用户填写是为了帮助用户一键完成自己 ePaaS 这些接口的订阅,而不需要用户再自己去 ePaaS 上一个个手动订阅了。
ePaaS 的功能介
当然 ePaaS 网关不是最终唯一的选择,我们也在积极拓展其他网关的接入。
四 未来还有什么?
- 构建效率提效:上面做的事情只是做到能,但还远称不上好,尤其是经常启动的调试命令,还有巨大的优化空间,每个人每次节省 10s,加起来也是好多时间。
- 深入业务:目前在企业智能的一些业务域下已经铺开,但还没有完全覆盖,这套东西是从业务里来的,所以也应该回到业务里去,业务的边界场景会带领我们逐渐进入深水区。
- 业务能力:能力是个比较虚的词,在我这里的理解,他是一个以一个业务功能为核心的,不定数量个服务和UI(页面、区块、组件)的集合,带服务的业务组件是我们在这个领域的第一步探索,接下来我们会把他继续做深入,包括业务能力的乐高入驻,业务能力的本地生产等等。
- LowCode:目前乐高已经具备了低代码拖拽生成业务组件的能力,但只能在低代码可视化的设计器里使用,而理想状态下应该是可以在各种状态下流通,但是否是业务上的痛点,这个还在收集中。
- 模型驱动?:这个点打了一个问号,是因为目前还只是一个想法的阶段。目前页面在企业智能已经实现了 ProCode、LowCode 和模型驱动三驾马车的生产方式。业务组件方面已经有了 ProCode 和 LowCode 的模式,那是否可以通过绑定模型的方式直接驱动 UI,通过配置生成业务组件,复用在业务里呢?这在一定程度上也可以解决现在能适用于模型驱动的标准页面少的问题。
五 结语
上面是我们在业务组件开发方面做的一些微小工作,核心的方向还是减少学习的成本,减少重复工作,以及将复杂的步骤变简单,通过这些方式来做到前端业务组件开发的提效。
最后,欢迎加入企业智能-用户体验平台部前端团队,我们是阿里企业服务的先行者和创新基地,企业智能在音视频会议、远程办公系统、大型 ERP 系统、企业运营活动、大型组织管理都有着丰富的实践和业务场景。团队技术涉及可视化搭建、跨端小程序、微前端、桌面端开发、模型驱动渲染、数据可视化、体验度量等等前端前沿方向,对这些业务场景和技术方向感兴趣的小伙伴欢迎来联系我。
相关链接
[1]https://ant.design/index-cn
[2]https://fusion.design/
[3]https://ice.work/