“
一、搭建飞途组件库的背景和遇到的问题
随着高途业务的快速发展,原来的一套大而全的中后台系统,已经不能适应高途的快速发展,根据业务角色、用途等拆分出多个中后台系统;业务的快速发展和市场变化,也需要提炼相同的功能组件适应业务多变和紧急诉求;基于这些原因,搭建了飞途中后台组件库,承接提炼出来UI组件,在各个系统间复用,提升代码开发效率;飞途组件库第一版就是这样诞生了。
但是,飞途组件库经过一年多的发展,共提炼二十多个组件,这些组件累计使用八十多次,复用率比较低;所期望的组件复用提升开发效率的效果完全没有达到,
具体原因是:
1.各个系统分属在不同团队,不同团队之间认知和共识各不相同,UI习惯向不同的方向发展。
2.由于没有UI标准,PM、UI工程师随意借鉴各个大厂的UI设计,让UI组件更个性化的同时,带来了开发量的提升,个性化组件在不同业务系统中很难复用。
3.前端后端接口契约的不同,直接增加了组件的复用难度,增加开发适配工作。
针对上面的问题,我们推进了两个规范(接口规范、UI规范)和组件的融合,情况有所改善,UI组件开始大量使用,样式文件和UI走查大幅减少,这是飞途第二版,但是,实际情况还是不够理想;
具体原因如下:
1.虽然每个组件符合UI标准,但是整个页面像是拼装的,整体效果比较差;
2.UI组件的提炼和高内聚还是不够,每天还是需要投入到大量多变的业务中,不能把有效的人力投入到核心业务中。
飞途组件库经过两个大版本的升级以后,痛定思痛,决定亲自参与到日常迭代中,找到真正的问题是什么?
“
二、飞途组件3.0的成功
飞途3.0最终取得成功。
因为,我们想通了一件事,中后台系统开发中,我们每天都在做什么?CRUD,是的,中后台系统每天都在做这件事;我们调整了飞途组件库以提炼日常项目中UI为主的工作方向,最终把飞途定义为:
feitud 是基于接口规范、UI规范、 Ant Design而开发的模板组件和UI组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面整体。
我们把飞途组件库的目标从UI提炼,改为页面模块级提炼;从UI功能到CRUD整体功能方案的解决;
飞途组件库从原来的被动提炼UI组件,到现在的主动出击解决日常业务开发中遇到的问题。
梳理中后台系统中CRUD每天重复做的工作,决定从五个方面来解决
1.模版化查询页,表单页、详情页三种场景;基于中后台常见的产品模式和功能拆解,对交互形式、接口数据结构进行标准化收敛后,提取出功能高内聚的物料场景、场景控件。
2.抽象模版页面数据处理场景(查询页、表单页、单个组件)。
3.可热插拔、高复用的FormItem组件(FormItem+输入组件)。
4.业务共识类组件(money、电话、日期,上传)等。
5.复杂多变的权限功能按钮,满足权限判断、条件判断、上报、提示、跳转、禁用等一系列工作。
“
三、飞途组件3.0的技术实现
1.场景模版化,查询页、表单页、详情页三种场景覆盖中后台系统80%+的场景
以上是和UI达成的场景标准化规范模版。
根据和UI达成的规范,形成一个主题token插件,其中主题色是高途蓝(#2266ff),代表这个高途人的斗志和奔放,上面的淡红色是测试用;
每个业务系统安装这个主题包插件,保证了每个系统UI规范的统一,大大提高了组件在不同业务系统复用的问题。
2.抽象模版页面数据处理场景组件化
从上图,场景模版中总结数据处理场景,把数据处理代码高内聚在组件内部,减少重复代码,降低开发成本。
上图中数字的含义
1-提交(查询)操作,分页数值默认是第一页;
2-重制操作,搜索条件清空,分页数值默认是第一页;
3-筛选条件查询,分页数值默认是第一页;
4-URL携带搜索条件,每次执行查询或提交,会携带这个条件;
5、6、8-table上分别筛选、排序、分页等查询,分页数值默认是第一页;
7-执行某个功能以后刷新当前页。例如,删除某一条数据,需要刷新当前页面;默认查询条件保持当前,包括分页也是当前分页;
功能分析归类
1)Table触发:5,6,8
2)固定依赖:4
3)form触发:1,2
4)筛选触发:3,
5)刷新:7
数据处理组件根据以上功能分析设计图
数据处理抽象组件(FeituData)解决的三个场景
1)查询页面,feitudata控制子组件
2)表单页面,feitudata控制Form子组件
3)类似ProFormSelect(FormItem+Select),单个需要数据绑定的组件;feitudata集成在子组件内部。
其中,1)2)是通过FeituData控制子组件的形式,进行数据处理高内聚;3)是把数据处理方法集成在组件内部,直接配置就能实现数据处理;
例如:
<ProFormSelect
name="area"
label="地区"
feituDataProps={{
fetch: 'https://api.baijia.com/mock/2545/api/crud/getFilter',
defaultParams: {uid: 4},
dataPath: 'areaList',
}}
multiple= {true}
optionPath={{value: 'key'}}
placeholder="Please select a country"
/>
通过配置数据接口,把ajax数据处理结成在组件内部;
3.可热插拔、高复用的FormItem组件(FormItem+输入组件)
举个通俗的例子 ,110v的电器不能直接接入220v 的电路中,你需要一个变压器,才能接入220v电路中;如果和很多这样的电器,是不是要很多变压器,如果电器支持110v和220v自由转换,是不是就方便很多呢?是的,feitu组件实现了这样的功能,把FormItem数据初始化和结果提交格式化都封装到了组件里面,大大提高了组件的复用,让组件的增加、删除不影响任何其它代码,让formitem组件有了热插拔的特性;
这里主要解决的问题有
1)Form集成footerBar组件、集成处理按钮、集成UI规范;
2)Form->FormItem→输入组件 变为 Form→FromItem+输入组件;把常用的FromItem+输入组件两个组件变成一个组件,简化操作;
3)为每一个FromItem+输入组件集成组件增加输入函数和输出函数;这是飞途组件的最大的创新之一;
feituFormatterInner
feituFormatterOutter
这两个函数集成到组件内部,解决每次使用组件时候的耦合。做到了FormItem组件的可热插拔。
例如:
import React from 'react';
import {Cascader, Form} from 'antd';
import {useModel} from 'umi';
import {ProFormCascader} from 'feitud';
import {isString} from 'lodash';
import {strsToInts} from '@/utils/utils';
const ClazzTags: React.FC<ClazzTagsProps> = (props: ClazzTagsProps) => {
const InnerFormat = (value) => {
if (fieldProps?.multiple) {
if (Array.isArray(value) && value.length > 0 && isString(value[0])) {
return value.map((item) => strsToInts(item));
}
return value;
}
if (isString(value)) {
const val = strsToInts(value);
return val;
}
return value;
}
const OutterFormat = (value) => {
if (fieldProps?.multiple) {
if (Array.isArray(value) && value.length > 0) {
return value.map((item) => item?.join('/'));
}
return [];
}
if (value) {
return value.join('/')
}
return value;
}
return (<ProFormCascader
label={label}
placeholder={placeholder}
fieldProps={{
fieldNames:{
value: 'code',
label: 'name',
children: 'children'
},
showSearch: true,
changeOnSelect: true,
options: getSource(),
...fieldProps
}}
feituFormatterInner= {value => InnerFormat(value)}
feituFormatterOutter= {value => OutterFormat(value)}
{...rest}
/>);
};
export default ClazzTags;
4.业务共识类组件(money、电话、日期,上传)等
这里把和后端约定俗称的都做成了组件
例如,日期范围组件
这些每次要做的功能都封装到了组件里面
1)每次要配置日期格式 YYYY-MM-DD,现在默认进组件,以后不用每次配置。
2)日期常见功能,例如,最大筛选范围是一个月 or 30天,禁用日期,默认值是当天等,直接提供配置能力,不用每次写代码。
3)日期格式处理:timer:[moment,moment], 后端接口常常是这样的 beginTimer:1234453333, endTimer:2343455000; timer:[1234453333, 2343455000],直接组件解决。
4)日期的快捷选择“当天”,“当月”,“当周”等,直接提供这样的能力。
5)选择的时间范围timer:[2022-7-12, 2022-7-23],但是真是需要的是[2022-7-12 00:00:00 000, 2022-7-23 23:59:59 999],直接组件默认解决。
6)rd返回的时间戳,前端需要用moent转换,组件直接转换
以上都是日期范围组件每天重复要写的,现在通过组件化+规范可以解决;
其它类似的组件还有money、tel、upload等组件,这里不展开讲了,感兴趣的伙伴可以看下飞途的官方文档(暂时只能在高途局域网看)。
5.复杂多变的权限功能操作组件
<Access.Href
Authority={tags.UNIONCLAZZLESSON}
hasCondition={startMoreThanHalfHour && relateStatus !== 2}
disabledType={!startMoreThanHalfHour || relateStatus === 2 ? 'display' : 'hide'}
toolTipProps={!startMoreThanHalfHour && unClass
? {
title: '距离开课时间不足30分钟无法操作关联或取消关联'
}
: void 0}
onClick={() => handleOptions(relateStatus !== 1 ? 'related' : 'unrelated', {record})}
>
{relateStatus !== 1 ? '添加关联课节' : '取消关联课节'}
</Access.Href>
在我负责的业务系统中,权限组件是使用最多的,有将近四百次的使用量;
权限组件实现了权限判断、状态条件判断、上报、禁用、跳转、提示等复合功能,大大减轻了功能操作的复杂度;
具体可以看下方飞途文档。
以上5个问题解决以后,看一个业务系统中具体用的demo
import React, {useContext} from 'react';
import {FeituData, FeituTable, QueryFilter} from 'feitud';
import {InputNumber} from 'antd';
import {cloneDeep} from 'lodash';
import {fenToYuan} from '@/utils/utils';
import util from '../../util';
import ORDERCONFIG from '../../orderconfig';
import {ProductInfoContext} from '../ProductInfo/context';
import useProductSearch from '../../hooks/useProductSearch';
import useOrderJson from '../../hooks/useOrderJson';
const {PRODUCT_TYPE, ONSALE, DEFAULT_COLS_NUMBER, MAX_PAGER} = ORDERCONFIG;
const {getFormatValue} = util;
const PhysicalTable = ({rowSelection}) => {
const {countRecord, setCountRecord, selectedRowKeys} = useContext(ProductInfoContext) || {};
const currentFormList = useProductSearch({productType: PRODUCT_TYPE.PHYSICAL});
const ORDER_JSON = useOrderJson();
const {CREATE_ORDER} = ORDER_JSON;
const {AUTOSEARCH, PAGINATION} = CREATE_ORDER || {};
const extraObj = {};
if (!PAGINATION) {
false; =
}
const handleBuyCountChange = ({value, record}) => {
const {productSkuNumber} = record || {};
const tempCountRecord = cloneDeep(countRecord);
value; =
setCountRecord(tempCountRecord);
};
const columns = [
{
title: '商品名称',
align: 'center',
width: 200,
dataIndex: 'productSpuName',
shouldCellUpdate: (record, prevRecord) => record.productSpuName !== prevRecord.productSpuName
},
{
title: '商品编号',
align: 'center',
width: 180,
dataIndex: 'productSkuNumber',
shouldCellUpdate: (record, prevRecord) => record.productSkuNumber !== prevRecord.productSkuNumber
},
{
title: '规格',
align: 'center',
width: 200,
dataIndex: 'productSkuNormValue',
shouldCellUpdate: (record, prevRecord) => record.productSkuNormValue !== prevRecord.productSkuNormValue
},
{
title: '可售卖数量',
align: 'center',
width: 100,
dataIndex: 'remainCount',
shouldCellUpdate: (record, prevRecord) => record.remainCount !== prevRecord.remainCount
},
{
title: '购买数量',
align: 'center',
width: 120,
shouldCellUpdate: record => selectedRowKeys.includes(record.productSkuNumber)
countRecord[record.productSkuNumber] > 1,
record) {
const {remainCount, productSkuNumber} = record || {};
const countValue = countRecord[productSkuNumber] || 1;
const isChecked = selectedRowKeys.includes(productSkuNumber);
return (
<InputNumber
disabled={!remainCount || isChecked}
max={remainCount}
value={countValue}
min={1}
precision={0}
defaultValue={1}
onChange={value => handleBuyCountChange({value, record})}
/>
);
}
},
{
title: '价格',
align: 'center',
width: 120,
dataIndex: 'salePrice',
shouldCellUpdate: (record, prevRecord) => record.salePrice !== prevRecord.salePrice,
{
return `¥${fenToYuan(value)}`;
},
},
];
return (
<FeituData
fetch={CREATE_ORDER[PRODUCT_TYPE.PHYSICAL].SEARCHPATH}
onBefore={v => ({
...getFormatValue(v),
saleStatus: [ONSALE],
pager: PAGINATION ? v.pager : MAX_PAGER,
})}
>
<QueryFilter
defaultColsNumber={DEFAULT_COLS_NUMBER}
style={{width: '100% !important'}}
collapsed={false}
>
{
> component) =
}
</QueryFilter>
<FeituTable
columns={columns}
style={{
position: 'relative',
width: '100%'
}}
scroll={{
y: 400
}}
rowKey={record => record.productSkuNumber}
rowSelection={rowSelection}
autoSearch={AUTOSEARCH}
pagerProps={PAGINATION}
{...extraObj}
/>
</FeituData>
);
};
export default PhysicalTable;
收益:
1.查询页面代码量减少40%+;
2.表单页面代码量减少15%+;
3.常规CSS代码量减少100%;
4.前端接口、UI交互讨论、UI后期走查,时间大幅降低;
飞途组件库核心设计
“
四、飞途速搭
飞途3.0以后,我在想,有什么办法可以更快开发页面呢?怎么才能把前端人力集中在核心复杂的业务当中?怎么才能把重复、低端的开发快速交付呢?
我们想到了配置化,可视化,这才有了飞途速搭;
飞途速搭是低代码平台,用户可以创建自己的空间,拖拽配置快速生成自己想要的页面;飞途组件是飞途速搭低代码的核心物料;低代码编辑器采用的阿里的LowCodeEngine。
为什么弃用imis?
飞途速搭 | imis | |
规范方面 | 公司统一的接口规范和UI规范 | UI没有规范,融入业务困难;接口是imis自定义规范 |
编辑器 | 阿里编辑器,开源,阿里内部有60多个应用; 插件、出码等功能丰富 | 只使用了百度渲染器,没有可视化编辑器,编辑器升级困难;用户拖拽组件困难; |
物料和插件 | 飞途物料、antd物料、ai插件、yapi插件 | antd物料 |
为什么使用阿里低代码编辑器?
网易音乐低代码、华为低代码、阿里低代码都差不多,但是阿里低代码文档和star都更好一些,阿里内部有大量应用,所以选择阿里低代码。
飞途速搭有什么优势
1.飞途组件可以降低代码配置量。主要体现在自动化的数据处理和formItem+输入组件、沉淀的共识类组件等;
2.飞途模版化场景符合UI规范和接口规范,更容易接入到系统当中;
3.YAPI插件配置和function出码能让配置和二次开发更高效,开发中。
飞途速搭适用的场景
1)工具类,用户直接在飞途速搭平台访问和使用。
2)比较简单的业务类,嵌入到业务系统中。
名词解释
飞途物料:飞途组件库的低代码描述文件,低代码引擎能识别的低代码组件库格式;
Sequelize:Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。
Eureka:Eureka是Spring Cloud里面的一个组件,名为注册中心,分为Eureka Server与Eureka Client。Spring Cloud 微服务框架下有众多服务,各个服务都是独立部署运行的,需要一个统一的中心管理并注册各个服务,保存服务的metadata(ip地址,服务名等等),这时Eureka 便诞生出来了。
LowCodeEngine:阿里低代码编辑器;
飞途速搭管理平台:antd pro脚手架搭建的B端管理页面,用于低代码平台的管理;
YAPI插件:通过调用YAPI接口,根据返回的输入和输出参数快速配置出页面;生成schema,可以在低代码编辑器中二次加工;
架构图
YAPI插件图
开发中
通过飞途和飞途速搭开发提效的五种方法
1)项目中直接使用飞途组件,代码量大幅降低
2)低代码引擎出码功能,二次开发
3)低代码配置,项目中使用(微前端接入、iframe接入、渲染插件接入)
4)YAPI vscode(或者Chrome)插件出码功能
5)让更多非前端人员通过拖拽的方式参与到前端开发当中
借用阿里图
低代码的核心是降本提效和角色赋能
1)让更多的非前端人员能参与到前端开发中,让业务不在等待;
2)让有限的前端资源投入到更关键的开发中。
至此,我们通过多维度解决开发过程中遇到的问题,包括接口、UI设计和走查、重复编码、配置生成、二次开发、非前端开发等多种手段,效能比以前有较大提升;
“
五、联系我们
飞途组件库和飞途速搭还没有开源,后续会有这个计划,有需要了解更多细节的伙伴可以通过下面两种方式联系;
1.外部用户,liangxiaohu@baijia.com
2.高途内部用户
——END——