6

开放与收敛 - 搭建系统的资产体系设计

 2 years ago
source link: https://zhuanlan.zhihu.com/p/396327885
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

开放与收敛 - 搭建系统的资产体系设计

去过很多西方国家,却依旧过不好这一生

概要:这是我在 GIAC 2021 上做的分享,补充文字内容后整理成的文章

# 搭建系统行业现状

面向领域提供解决方案,提高生产效率

v2-ed77c9d39185ca8a748bb278641a9d97_720w.jpgvia 云凤蝶可视化搭建的推导与实现

搭建系统并不是一个稀奇的概念,从 Dreamweaver 开始,有大量的产品试图用装配式的开发解决应用生产的效率问题。它们面向特定场景做抽象、沉淀出最佳实践,再通过产品封装来加速整个制作过程。​

比如 Dreamweaver 认为 HTML 树是不重要的,应该通过所见即所得的自由拖拽生成;比如 SwiftUI 认为移动场景下,数据的流向与更新应该被高度简化,于是提供简洁的交互来快速建立组件与模型间响应式的数据流。

## 搭建系统要解决什么问题?

一言以蔽之,缺人。

前端沉重的上手门槛导致招聘培养非常困难,前端资源逐渐成为项目瓶颈,无法跟上爆发式的业务增长。现场我做了一个调查,写过前端页面的人中只有 1/3 是专职前端,剩下的大家都是被逼的没办法:服务早就写好了,但没前端资源,只能撸起袖子自己把页面做了。

v2-91a8c2fcee0df68a0a9d3a3e82cd58d7_720w.jpgvia How it feels to learn JavaScript in 2016

## 如何解决问题?

参考制造业的最佳实践

作为高精尖产品 iPhone,通过标准化手机的制作程序,将大量生产外包。苹果有 200 多个零部件供应商,400 多道组装工序,终端的富士康郑州工厂每天可以生产 50 万部 iPhone,零部件拆分和流水线的组装大大提升产能,苹果通过这种方式书写了普通工人批量制造高精尖产品的神话。

iPhone 的供应链,红框内是富士康 via 云凤蝶中台研发提效实践

汽车行业将整车拆分为零部件,外包给专业的零部件生产商,再通过流水线的组装完成最后一步。这些组装汽车的人,可能对汽车一窍不通,但只需要在流水线,按部就班的重复再重复就行了。

传统研发 vs 搭建研发 via 云凤蝶中台研发提效实践

还有我们所熟悉的每一个快餐店:

论如何制作汉堡包

通过上面几种抽象,传统的生产过程被重新解构为:

  • 擅长生产零部件的沉淀为领域专家:比如相机、螺丝、轮胎、面包和炸鸡,在领域内不断突破降低成本
  • 在终端门槛极低的流水线组装:可以招聘大量普通工人扩大生产,实现效率的不断提升

## 云凤蝶的实践经验

企业级应用制作平台

在如今的 Web 领域,无论是大公司还是小公司,都很少会从 span、div 标签写起,大家都会有自己的一套组件库,对于 Web 研发我们似乎也可以使用这样的思路来解决问题。

在蚂蚁集团,有 36% 的中后台应用以这种方式搭建,应用的平均复杂度在 20 个页面,上百个组件的量级。我们通过对中后台应用的抽象,结合设计规范,希望能重新定义应用的制作方式,解决三个问题:

  • 降门槛:非专业前端也能搭建,拓展新的角色在终端「组装汽车」,可以大量外包用工
  • 提效率:结合设计规范,简化应用制作的各种概念,比如样式、布局、数据流、网络请求
  • 60 分:很多中后台应用缺乏产品和设计,体验参差不齐。之前 Ant Design 的作者曾提过一个观点,antd 最大的价值是保证应用可以达到一个最低及格分

自然搭建系统在发展中也会面临各种问题,本文主要结合在云凤蝶的实践经验,聊一聊对于一个搭建系统来说,如何通过组件化的思路应对搭建系统发展上面临的各种挑战。

# 一个搭建系统要面临怎样的挑战

支持的业务复杂度提升,搭建系统的复杂度也会提升

每个人应该都写过最朴素的搭建系统,它的形式类似一个营销搭建系统。在业务开发中,我们会把页面中经常变化的内容抽象为模版的配置项,并在模版外进行配置,每当配置变化,模版无需重新构建和验证即可快速迭代,此时一个应用由一个巨大的模版组成,如下图 1。

随着需求复杂度的提升,搭建系统需要做更细粒度的拆分。比如双 11 有多个活动页面,页面间有一些共享模块,比如签到和抽奖。从复用和维护性的角度,我们会面向功能封装出一些业务组件,此时搭建系统也相应要进化为楼层式搭建,对业务组件做二次编排和配置。

原本的一个配置项变为配置项数组,按照顺序描述每个组件暴露的接口,此时的搭建系统已经能够承载一定对复用的需求,有了组件化的雏形,如下图 2。

搭建系统的演进过程

再往后发展,页面出现了更自由的组合关系:导航栏、父子嵌套、弹窗、表单表格,这是典型的中后台应用。我们必须进一步拆分,并且多数组件已经无法针对具体应用重新开发,必须沉淀更细粒度的通用型组件,把原本大量内置到模版的逻辑,拆出来表达为组件间关系,才能支撑业务的持续增长,如上图 3。

从最简单的营销搭建到中后台搭建,整个组件体系在技术上有很多挑战:

简单的营销搭建复杂的中后台搭建组件组成有限、无复用的业务组件可拓展、可复用基础组件打包方式一个应用一个 umd 包要处理组件间的公共依赖生命周期一次性长期迭代、版本碎片

# 组件体系 1.0 - 铸型

要建立一个怎样的组件体系,才能应对这些挑战呢?

## 组件从哪来?

首先要回答一个问题,去哪找组件?

NPM 是社区非常有生命力的组件中心,复用 NPM 的基础设施是一个理想的选择:

  • 重新造一套组件的成本太高了,Ant Design 一个组件的平均成本在一个月左右
  • NPM 上有大量的高质量组件,可以快速补充组件内容
  • 不同的业务会有自己的一套组件库,无法全部内置到平台,必须要「开放」
  • 当业务出现自定义需求,可以封装 NPM 完成最后一公里
和 NPM 的关系

组件的生产必须与平台解耦,再通过组件规范建立连接,这样才能快速建立组件生态,完成面向组件的拓展。

对组件进行抽象和建模

建立组件规范:NPM 和搭建系统的连接

前面我们提到,对应用的配置可以分解对多个组件的配置。一个典型的组件编辑的场景如下,我们要能找到组件向外暴露的接口,并通过图形化的方式编辑他们。

编辑组件

再以主流的前端框架来看,一个组件是什么?

组件的抽象

UI 就是组件最终的渲染效果,f 是组件的实现,而 props 就是组件向外暴露的接口,以 React 的代码为例,上面的可视化编辑和下面的代码相对应:

<Button type="primary" loading={false} children="我是一个按钮" />

从类型出发找到组件

根据 NPM 的规范,我们可以从 package.json.typings 类型文件出发,通过 AST 解析找到所有的模块导出,并提取出其中符合 UI = F(props) 抽象的组件和属性定义。

比如在 React 技术栈下,React.Component React.FC React.PureComponent 都是符合要求的。

解析组件和 Props

识别/提取组件的元信息

找到组件后,我们可以深入挖掘他的元信息,比如如下的类型声明,我们很容易推测几件事情:

/**
 * @title 尺寸
 */
size: "small" | "middle" | "large";

/**
 * @format icon
 */
beforeIcon: string;

children?: React.ReactNode;
  • size 的中文描述是尺寸
  • size 只有三种取值,语义类似枚举,适合用下拉选择来编辑
  • beforeIcon 的类型是 string,但 formaticon,适合用图标选择器来编辑

将这些对元信息的推测规则沉淀下来,我们可以得到一个渲染引擎驱动的属性面板。在组件无需任何额外定义的情况下,尽可能的提升组件编辑效率。

一些复杂的编辑意图推断

除了上面对基础类型的简单推断,我们还可以做一些更深入的分析推断:

比如 nullable 通常用来表达可关闭的编辑意图。表格的分页属性可以配置为 Object 类型数据表达分页详细信息,也可以将值设为 false 来关闭整个分页功能,我们可以使用下面这种交互:

Table.pagination: false | Pagination
v2-7ddd4832d5f6320ebfdf17c11106ce06_b.jpg
nullable

比如 unionType 通常用来表达分支情况。组件的提示信息有三种类型,每种类型都会一些特定配置项,在不同分支切换时,需要删除前一个分支的值,并为新的分支设置默认值。

tips: Text | Card | Popconfirm
v2-630607aaedc09852beb84d82a3e1834c_b.jpg
unionType

## 组件如何加载、预览

应用如何处理 f 的依赖?

在写代码的模式里,我们把依赖安装到本地,再通过 Webpack 类似的工具对文件进行打包,每当代码修改/依赖发生变化,应用会重新构建,最终发布时,应用会把所有依赖的代码打包到一起。

但对于搭建系统来说,改一行文字等 5s 显然是不能接受的,我们要提供实时预览的方案。

一个简单的依赖关系

常见的玩法是,每个组件提前独立打 umd 包,应用只构建自己的代码,再通过 loader 远程加载所有外部组件依赖,形成一个 distMap,最终做组件渲染,这里 Map 的值就是上面我们提到的 UI = f(props)里的 f。

{
  Button: eval(request(buttonDist)),
  Card: eval(request(cardDist)),
}

React.createElement(map['Button'], ...);

但把所有依赖都打包进去的方法会导致 A 被重复打包多次,如上图,如果 A、B、C 分别打一个 umd 包,应用会有三个 A 的打包体积,并且对于 React.Context 等场景还会带来不同实例数据不通的问题。

我们需要有更细粒度的模块打包方式,能够支持按照版本规则对 A 进行复用。

Bundless 技术方案

NPM 包维度提前打包 & 实时依赖计算

组件在第一次进入到系统时,会按照依赖树递归的做 NPM 级打包,并将结果存储到 Assets CDN 上。

当前端应用的依赖发生变化时,通过请求 Assets CDN,按照版本合并 A & C 的所有依赖,并通过一次网络请求加载回来,再拆分给 loader 装载到 distMap 上。

组件导入 &amp;amp;amp;amp;amp; 依赖加载

TreeShaking

上面的场景中,提前打包的粒度是 NPM 包级,这会导致一个问题,应用只使用了 lodash.get 但却加载了整个 lodash,体积还是很大。如下图,使用了 antdButton Menu Table,最后加载了整个 antd

按需 TreeShaking

我们还需要做一些动态的处理,在应用发布时,根据应用对组件的实际使用情况,自动 TreeShaking 掉未使用的模块,创建一个虚拟的 antd 来降低体积。

这里有一些衍生的问题,如何保证依赖计算的速度、为什么 treeShaking 是安全的,以及为什么不做文件级的 Bundless?后续会有文章专门介绍。

# 组件体系 2.0 - 演进

「偷」来的组件不够好用怎么办?

通过一键导入 NPM 可以帮我们快速补充组件内容,但 NPM 上是相对松散的组件,距离在搭建系统上好用还有很大距离,我们需要对他们做一些封装,并且建立能持续迭代和治理的方案。

## 横向封装

弹窗类组件难以使用

弹窗类组件通常有一个受控属性来控制显示隐藏,如果设为可见,会挡住其他所有组件的编辑;如果设为不可见则无法编辑弹窗本身。

我们尝试抽象弹窗组件的特性,结合编辑态大纲树选中状态这一交互做一些封装:

  • 大纲树上选中 visible: true
  • 取消选择 visible: false

dataEntry 类组件双向绑定成本过高

输入框等组件的值也是受控模式,在输入框输入后需要手动在 onChange 方法里把新的值同步回 value 上,这使得表单类组件在搭建系统下使用效率很低。

我们同样去抽象这类组件的特征,在组件接入时,让组件回答几个问题:

  • 是否为表单类组件
  • 表达值的属性名是什么(比如 value)
  • 同步数据的事件是什么(比如 onChange)

这样我们可以建立一个虚拟的 store,在 onChange 事件触发时,自动完成事件参数到 value 的数据同步。在产品上的体现就是双向绑定,用户只需要为表单类绑定一个变量,数据同步就自动完成了。

面向特征做能力切面的拓展

类似上面两种的封装方式还有很多,这种面向组件特征而不是具体的组件做抽象,有几个非常明显的好处:

  • 横行提升表达力:此类组件都能使用,能力是有限的,但组件是无限的
  • 降低耦合:系统和组件解耦,通过能力描述建立关联
  • 统一心智:通用行为集中在与组件无关的配置区
组件表现力 = 组件数量 x 横向能力

举个例子,一个普通的头像组件,经过大量通用能力的描述可以变为带徽标的头像数组。

## 资产沉淀

除了用导入 NPM 的方式生产组件,我们还可以在搭建系统沉淀一些组件吗?

如果一个应用中有多个地方都使用了相似的布局,我们可以把这部分内容提取为画布组件,并像 React 那样,向外暴露一些属性,实现一处维护、多处使用的目的。

如下,我们把用户信息的展示封装为一个组件,而用户信息作为外部参数传入,这种局部复用的思路和 NPM 是一致的,只是生产组件的方式不仅局限于写代码,还可以通过搭建。

封装画布组件

JSXBox 最后一公里

除了使用已有的组件进行搭建外,有些场景可能更适合用代码,比如根据数据动态嵌套渲染,或是绘制一个复杂的图表。我们可以在页面上挖一个洞,让用户写代码的方式,完成这部分定制化的需求。

它的形态有些类似 CodeSandbox,写一段 React 代码,最终和其他组件混合跑在一个页面上。

JSXBox

在持续发展中,每条业务线都会沉淀自己适用的业务资产,可能是一系列 NPM 包,也可能是我们可以提到的搭建系统内的资产。在产品形态上,我们可以引入资产大包的概念,业务线的开发可以聚合比如 UI 组件、工具函数、服务等,整合到资产包内,再发布给其他应用使用,通过这种方式完成业务资产沉淀和定向的二次效率提升。

并且无论是画布组件、代码组件、JSXBox 都是遵循同样一套组件规范,我们可以直接将搭建系统内沉淀的资产包发布到 NPM。

适合搭建的走搭建、适合代码的写代码。用户可以通过 NPM 完成各自的互相融合研发。

沉淀资产包

## 版本治理

只要有版本,就会有版本碎片

antd 的版本碎片分布 &amp;amp;amp;amp;amp; antd4 changeLog

当搭建系统封装组件给应用使用时,必然会出现版本,也就必然面临版本碎片问题。版本碎片无论对组件的开发者还是使用者都是巨大的成本,对于搭建系统来说,如何解决这个问题?

一次 API 不兼容变更

我们来看一个具体的例子,Menu 早期用 Boolean 表达水平/垂直布局,但新的设计规范引入第三种布局,Boolean 无法承载,需要升级为 String 枚举,从 API 上来看,确实是一次不兼容升级。

Menu1.0 -&amp;amp;amp;amp;gt; Menu2.0

但组件的 API 发生变化,能力并未发生变化,也就是 Menu2.0 仍然支持水平/垂直的布局能力,只是旧的 Boolean 数据不再适用新的组件实现,需要更新。

而搭建平台下,组件的使用是严格受控的,是一份结构化的数据,我们完全可以通过精准的 Codemod 来将所有旧版本的数据升级为新版本,即:

delete $props.vertical
create $props.layout: $before.vertical ? 'vertical' : 'horizontal'

通过这种方式,我们可以将大部分组件不兼容的版本升级上来,组件始终可以保持向前兼容,0 版本碎片。

分享一个数据,去年 antd 从 3 到 4 的升级,使用云凤蝶搭建的几百个应用全程无感知,只需要点一下升级等上一会就行了。

## 实现开放与收敛

有生命力的资产体系

通过上面各种手段,我们实现了一个无序的 NPM 组件到搭建系统的资产的过度,在这个过程:

  • 一键导入:让原来的组件都能用
  • 横向封装:让原来的组件在搭建系统下更好用
  • codemod:让用户始终用最新的组件

同时,面向组件规范、能力切面的抽象,使得在支撑更复杂业务的同时,搭建系统本身的复杂度可以得到收敛。通过底层的各种功能模块支撑,建立起一个有生命力的资产体系。

# one more thing

如何评价程序员 50% 的时间都在写表单表格?

就算搭建系统再好用,资产再有生命力,用户仍然还是要一个表单项一个表单项配置表单、表格、高级搜索,处理双向绑定、字段映射等重复工作,面对有大量规律可循的增删改查中后台应用,我们能不能抽象一些套路,把程序员从千篇一律的工作中解脱出来呢?​

实际上我们观察 API 文档可以发现,接口格式和最终的表单、表格有非常大的关联关系

  • GET /api/use 查询用户列表,大概率会使用表格,表格列和返回值有高度的对应关系
  • POST /api/user 新增用户信息,大概率会使用表单,表单项和入参数有高度的对应关系
  • name: string 大概率在表单中使用输入框,在表格中使用文本

这些推断来源于对 API 元信息的理解、按照能力切面为组件做的封装,以及中后台应用在设计上的最佳实践。

传统 vs 智能向导

有了智能向导的帮助,用户只需要选择接口、勾选他们想要的字端,再做一些简单的映射配置,即可生成带有完整逻辑功能的表单、表格页面,在云凤蝶应用中,有 53% 的等效代码由机器自动生成。

# 写在最后

The dignity of movement of an ice-berg is due to only one-eighth of it being above water. Ernest Hemingway

最后以一张经典冰山图作为结尾,实际上我们看到的很多酷炫功能,为了保障他的正常运行,在看得到的冰山下,有大量看不见的工具链路、渲染引擎、设计规则在悄悄运转。

如果你也对文中提到的这些感兴趣,欢迎加入我们,一起打造更好的属性面板、更智能的表单向导、更有生命力的资产体系。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK