关注我们 文末有福利


七夕到了,还在加班?98后小哥哥教你用 sula 快速配置页面提前下班去约会_自定义插件

曹清达

​ 98年的小哥哥,喜欢对新技术进行探索尝试,来解决业务中的效率,质量和体验问题,影响业务。我们的目标是把自己'​​'。


背景

最近负责重构一个后台系统,发现系统内 90%的页面功能非常类似并且很琐碎。如果通过常规写法会很浪费时间和人力,而且开发人员从中很难获得成长。于是希望有一个后台可复用方案,通过配置 ​​json​​ 即可生成页面。

方案选择

业界基于 ​​antd​​ 存在的四种技术方案:


  1. ​ProComponents​​​:基于 ​​antd​​ 封装用于解决重复样板代码的一系列组件
  2. ​ahooks​​​ 中的 ​​useAntdTable​​​:常用的 ​​React Hooks​​库
  3. ​Formily​​ :面向中后台复杂场景的一个表单框架
  4. ​sula​​​:基于 ​​antd 4.0​​ 封装的一套【产品级】配置框架

前三种技术方案或者是针对 ​​Table​​​ 或者 ​​Form​​​ 单一场景基于 ​​antd​​​ 封装,很难抽象出配置 ​​json​​​,我们需要的是通过配置数据来驱动视图的一个产品级框架,显然 ​​sula​​​ 更合适我们,最终选择 ​​sula​​ 方案并做一次尝试。

基于 sula 配置

先引用官方的话简单介绍下 ​​sula​

​sula​​ 通过插件化的方式实现了配置的高扩展性与高灵活性,并且 sula 通过行为插件的引入,建立了渲染插件与用户行为的连接,实现了场景式的语义化配置。

七夕到了,还在加班?98后小哥哥教你用 sula 快速配置页面提前下班去约会_json_02来源于官网

和 ​​React​​​ 中“一切皆是组件”很相似,在 ​​sula​​​ 中可以理解为一切皆是插件, ​​sula​​​ 配置的实现方式是先通过官方提供的内置插件和自定义插件注册,然后在配置 ​​json​​ 中以约定关键字的形式引入。

在实际项目中基于 ​​sula​​ 的系统架构

七夕到了,还在加班?98后小哥哥教你用 sula 快速配置页面提前下班去约会_自定义插件_03系统架构图

上图为我们部门接入 ​​sula​​​ 的整体架构,我们的理想情况是产品/运营通过低代码平台来管理存放在服务器的配置项,并在需求页面访问时将配置项传给 ​​sula​​​ 组件。大家如果感兴趣想尝试使用 ​​sula​​ 的话,上面很多流程不是必备的,只需要在原有项目中注册插件就可以使用了。

如果大家也想尝试一下的话,现在 ​​sula​​ 还没有发布正式版,并且官方文档有些地方很隐晦,下面我会从实际业务方面详细的介绍一下自定义插件的配置和使用(很多项目已经实践过),对大家非常有帮助。

  • 项目入口文件(非 ​​umi​​ 项目)

在项目入口引入并注册官方提供的插件以及自定义插件

// 引入sula
import { registerFieldPlugins, registerRenderPlugins, registerActionPlugins, registerFilterPlugins, ConfigProvider as SulaConfigProvider } from 'sula';

// 引入本地sula配置

import { registerSpamFieldPlugins } from '@components/Sula/FieldPlugins'; //引入自定义field插件
import { registerSpamRenderPlugins } from '@components/Sula/RenderPlugins'; //引入自定义render插件
import { registerSpamActionPlugins } from '@components/Sula/ActionPlugins'; //引入自定义action插件
import { registerSpamPlugins } from '@components/Sula/SpamPlugins'; //引入自定义插件
import { registerRequestConfig } from '@components/Sula/RegisterRequestConfig'; //引入全局请求配置
import { registerIcon } from '@components/Sula/RegisterIcon'; //引入Icon

// 注册官方插件
registerFieldPlugins();
registerRenderPlugins();
registerActionPlugins();
registerFilterPlugins();

// 注册自定义Field插件 registerFieldPlugin(pluginName)(Component, hasSource, hasCtx)
registerSpamFieldPlugins();

// 注册自定义render插件 registerRenderPlugin(pluginName, extraPropsName)(Component, needCtxAndConfig)
registerSpamRenderPlugins();

// 注册自定义action插件
registerSpamActionPlugins();

// 注册 ConvertParamsPlugin
registerSpamPlugins();

// 设置全局请求
registerRequestConfig();

// 注册icon
registerIcon();

简单介绍几种自定义插件及其使用

  • ​ field​​ 插件(用于表单项的配置中)

例如业务中高频用到的远程获取数据并支持模糊搜索 ​​Select​​ 业务组件

// SearchSelect.jsx
import React, { memo } from 'react';
import { Select, Tooltip } from 'antd';

const filterOption = (input, option) => option.children.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;

const SearchSelect = (props) => {
const { source: { list = [] } = {}, ...restProps } = props;
return (
<Select showSearch allowClear filterOption={filterOption} {...restProps} >
{list.map((item) => (
<Select.Option value={item.value} key={item.value}>
<Tooltip title={item.label || item.name}>
{item.label || item.name}
</Tooltip>
</Select.Option>
))}
</Select>
);
};
// FieldPlugins/index.js
import { registerFieldPlugin } from 'sula';
import SearchSelect from './SearchSelect';

function registerSpamFieldPlugins() {
registerFieldPlugin('searchSelect')(SearchSelect, true, true);
}

export {
registerSpamFieldPlugins,
SearchSelect
};

使用

fields: [{
name: 'name',
label: '表单名字',
field: {
type: 'searchSelect', // 自定义 field 插件名字
props: {
placeholder: '请选择',
mode: 'multiple',
}
},
remoteSource: {
url: '', // 请求url
method: 'GET',
}
}]
  • 自定义 ​​render​​ 插件(渲染插件)

例如业务中对于时间日期的展示,接口如果返回时间则需要前端处理成日期,我们可以注册一个 ​​time​​ 插件

// RenderPlugins.jsx

import { registerRenderPlugin } from 'sula';

const Time = (props) => <span>{transTime.format(props.ctx.text)}</span>; // 格式化时间

function registerSpamRenderPlugins() {
registerRenderPlugin('time')(Time, true); // 注册插件
registerRenderPlugin('img')(SpamImg);
registerRenderPlugin('operationgroup')(OperationGroup, true);
}

export {
registerSpamRenderPlugins,
Time,
SpamImg
};

使用

columns: [
{
title: '更新时间',
key: 'updateTime',
render: {
type: 'time' // 自定义 render 插件的名字
}
}
]
  • 自定义 ​​action​​ 插件(行为插件)

例如实现​​导出数据​​按钮

const exportFile = (ctx, config) => {
const { filters = {} } = ctx.table.getPaging();
const { HistoryDownload = '' } = config;
const exportUrl = transToUrl(HistoryDownload, filters);
location.href = exportUrl; //导出按钮
};

function registerSpamActionPlugins() {
registerActionPlugin('export', exportFile);
}

export {
registerSpamActionPlugins,
exportFile,
};

使用

actionsRender: [
{
type: 'button',
props: {
type: 'primary',
children: '导出',
},
action: [{
type: 'export',
HistoryDownload: '', // 请求 Url
}],
}
]
  • 注册其他插件(格式化接口出参、入参、表单依赖、验证)

例如官方期望搜索表格的入参/出参往往和实际业务中会有出入,我们需要对请求参数/出参进行统一格式化处理

返回参数格式化

// 官方期望(格式前)
{
list: [],
total: 10
}

// 业务返回(格式后)
{
dataList: [],
pageInfo: {
total: 10,
...
}
}
// SpamPlugins.js
import { registerPlugin } from 'sula';

// 格式化获取表格类接口返回值
const converter = ({ data: { dataList = [], pageInfo = {} } }) => {
return {
list: dataList.map((item, index) => ({
id: index,
...item,
})),
total: pageInfo.total,
};
};

请求表格类接口参数格式化

//  转换前
{
pageSize: 20
current: 1
filters: {"uid":"123","whiteValue":null,"effectType":null,"operator":"caoqingda"}
sorter: {"order":"ascend","columnKey":"uid"}
}
// 转换后
{
pageNum: 1
uid: 123
operator: caoqingda
ascend: uid
pageSize: 20
}
const queryTableParams = ctx => {
const { params: { current, filters = {}, sorter: { order, columnKey } = {}, ...restParams } = {} } = ctx;
const newfilters = Object.keys(filters).reduce((per, cur) => ((per[cur] = Array.isArray(filters[cur]) ? filters[cur].join(',') : filters[cur]), per), {});
return {
pageNum: current,
...newfilters,
...(order === 'ascend' ? { ascend: columnKey } : {}),
...(order === 'descend' ? { descend: columnKey } : {}),
...restParams
};
};

function registerSpamPlugins() {
registerPlugin('convertParams', 'queryTableParams', queryTableParams);
registerPlugin('converter', 'converter', converter);
}

export {
registerSpamPlugins,
queryTableParams,
converter,
};

使用

remoteDataSource: {
url: '',
method: 'GET',
convertParams: 'queryTableParams',
converter: 'converter',
}
  • 全局配置请求 ​​Api​

基于全局配置实现统一的逻辑处理流程

//例如业务中所有接口使用以下返回值,
{
"respData":{
"dataList":[],
...
},
"respMsg":"", // 报错提示
"respCode":10 // 0正常 非0错误
}
//可以通过统一配置,达到错误信息自动提示,成功信息自定义展示
import { request } from 'sula';

function registerRequestConfig() {
request.use({
bizRequestAdapter(requestConfig) {
requestConfig.headers = {
Accept: 'application/json',
'Content-Type': 'application/json;charset=utf-8',
};
requestConfig.withCredentials = true; //跨域请求携带cookie
return requestConfig;
},
bizDataAdapter(result) {
const { respData } = result;
return respData;
},
bizSuccessMessageAdapter(result, successMessage) {
const { respCode, respMsg } = result;
if (+respCode !== 0) return null;
// successMessage为false时 返回成功不显示提示
if (successMessage === false) return null;

if (successMessage === true) {
// 默认使用后端返回
return respMsg;
} else {
// 前端设置成功提示信息
return successMessage;
}
},
bizErrorMessageAdapter(result) {
const { respCode, errorMsg } = result;
if (+respCode !== 0) return errorMsg;
return null;
}
});
}
export {
registerRequestConfig
};

​sula​​ 的核心特点是插件化,插件的本质是将通用函数逻辑通过提前约定好的关键字实现,并在函数中注入实例,来实现功能更好的内聚。

也正是通过插件化,我们才可以将页面配置转换为 ​​json​​​ 。如果存在自定义函数的配置,可以通过模板字符串 ​​'#{}'​​ 来转换。

我们把各页面配置项、页面的路由菜单放在服务器,从而由后端数据驱动视图,使得每次开发我们只需要让产品或者后端同学配置下数据就可以生成页面。

低代码开发的价值


  1. 开发效率提升:降低重复劳动,相同的功能页面,之前开发一天能缩短到一小时。
  2. 产品质量保障:通过配置生成的页面,查询、翻页等基础功能无需反复测试回归,只需要验证核心功能即可。
  3. 后端持久化:所有配置都能转成​​json​​,实现后端持久化存储,服务端驱动 UI 生成友好。
  4. 业务驱动:我们通过给产品一个可供选择的产品级框架,我们可以变成主动的一方来驱动业务高速发展。

体验感受

​sula​​​本身还在成长,已经满足大部分业务场景,有问题可以给他们提​​issue​​ (处理很快的~)。

相信小伙伴看完,对于 ​​sula​​​ 已经跃跃欲试了。但还是提醒下小伙伴因为 ​​sula​​ 还没有发布正式版,尽量不要投入生产环境。建议先从业务稍简单的页面做起来,如果出问题可以快速替换。

如果觉得 ​​sula​​ 有适用场景的话,需要和后端沟通好,例如参数的统一等等。也一定需要提前和产品沟通好并获得产品的认可,最好让产品出一个模板原型。因为产品不认可的话,会有一些约束之外的需求,那我们投入精力做的配置页面很难带来收益。

结语

通过 ​​sula​​​ 我们实现了后端数据驱动视图,下一步我们将搭建一个低代码可视化开发平台,产品/运营直接可以配置 ​​json​​ 并生成页面,更好的降低开发门槛,转移生产力。

如果小伙伴对于上面 ​​sula​​ 的配置有问题也可以留言给我们,有更好的想法也欢迎分享给大家。





七夕到了,还在加班?98后小哥哥教你用 sula 快速配置页面提前下班去约会_自定义插件_04

                                                                        ​扫二维码|关注我们