中后台管理系统前端可视化低代码方式提效设计一
前言
中后台管理类系统基本都是对数据的增删改查、上传下载等,最多也只是展示形式上的差异, 一般都是由:一块区域用来输入或选择进行调用接口进行查询,一个表格用于对查询出的数据进行展示以及每条数据的操作,一个或两个表单用于数据的添加或者修改,以及一块功能区域用于批量删除、导入、导出等等。这些功能简单且量大编写的再多也不能提升自生境界,纯粹的浪费生命,可谓是苦不堪言。一般可能想到的是cv大法,但是修改也是很痛苦的,因为经常会少改某些变量,在测试的时候又漏掉总是经常偷偷 fixed 也是一脸尴尬。所以我们决定使用可视化的方案来解决这些重复性的问题。
需求分析
如果我们简单的抽象一个CURD的代码大概主要逻辑应该如下(react hook版):
//用于查询
const [searchParams, setSearchParams] = useState({})
//列表数据
const [tableList, setTableList] = useState([])
//查询函数
const loadData = useCallback(() => {
service.search(searchParams).then(list => {
setTableList(list)
})
}, [searchParams])
//loadData 查询函数变化重新调用,因为依赖了 searchParams 所以查询参数变化就会重新查询
useEffect(() => {
loadData()
}, [loadData])
//... 其它
return (<div>
<Search>
{/*
<Input />
...
变化的查询条件
*/}
{/* 将Search中数据set 到 searchParams 以触发查询 */}
<Button>查询</Button>
{/* setSearchParams({}) */}
<Button>重置</Button>
<Search>
<div>
{/* openModal() */}
<Button>新增</Button>
<Button>批量删除</Button>
... 以及其它功能
</div>
{/*
1. 将分页数据设置到 searchParams 中以分页查询 setSearchParams({...searchParams, pageNo, pageSize })
2. 将选择的行保存下来用于如批量删除
*/}
<Table>
{
/*<Column lable="姓名" />
...
变化的列
*/
}
<Column lable="操作">
{/*
是否确定删除? 是:
service.delete(row.id).then(() => loadData())
*/}
<Button>删除</Button>
{/*
打开 modal 将当前行数据 row 传过去
openModal(row)
*/}
<Button>编辑</Button>
... 以及其它功能
</Column >
</Table>
<Modal>
{/*
将数据保存或更新
service.inserOrUpdate(data).then(() => loadData())
*/}
<Form>
{/*
<Input />
<Select />
...
其它输入选择项
*/
}
</Form>
</Modal>
</div>)
抽象至此,可以看到可以看到其逻辑基本都是如此可以是共通的,而变化的只是Search、Table、Form中的子组件,其基本的逻辑并不变。当前如上这些代码只是我一厢情愿的想法罢了,每个系统自然可以有自己的写法,但不论怎么写,总是可以抽象出固定的逻辑的,所以这些看似定的代码自然是要按每个系统要求来自己设计的(这应该是属于低代码的部分了罢,我的理解是)。而其中的变化的组件那就是可视化喽!所以我们准备用 低代码 + 可视化 的方式来解决重复性的问题。找到关键问题那也是应该想一想解决方案了,当然低代码平台也不是什么新鲜事了,能想到应该有很多很多什么百度的 amis、阿里的 LowCodeEngine 等等。这些都是生成一个大的 JSON,然后通过这个JSON来解析生成相应的页面,而且更多都是预定义或穷举了功能,大大的 JSON 也很难维护更难接着开发。而我们的目标是想帮助可发者解决重复性问题,并不是代替开发者。所以我希望其应该像一位开发者以正常的方式切入系统来解决相关问题,输入正常的代码方式来辅助开发者。
设计说明
综上所述,以及开发者开发项目的角度逻辑整理出如下主要功能:
- 项目创建
- 设计抽象代码与视图
- 页面的创建
- 可视化编辑区
- 变量、函数、effect 定义
- 接口定义
- 代码的生成
- 自定义组件
- 在线预览
1. 项目创建
按开发逻辑一般是使用 create-react-app 创建一个脚手架,所以我们创建的时候也通过 create-react-app 在服务端创建一个脚手架,再配置如axios(http库)、全局css、全局数据等,我们将配置的数据写入脚手架的相关文件中。
从开发角度来说这样一个基本项目就算是创建完成了,可以进入开发任务了
设计抽象代码与视图
这一步就如开发者开发共用的组件。当然在我们设计中略有不同,不是用于引用传参数,而是用于复制到相关使用的页面中,更像cv。将上面的抽象代码复制到即将要开发的中再补充变化的组件即可完成功能,所以这一步更是我们用于定制与提效的重要方案。
其与页面开发实际是相同的,比如我们创建一个页面实现如下:
- 放入搜索组件,并放入一个查询按钮
- 放入添加按钮,其可以打开弹窗表单
- 放入表格,加入一个操作列,其中分别有删除、修改按钮,删除:提示是否删除?是,将行数据id用于调用删除接口,修改:打开弹窗表单
- 弹窗表单
可以看到这些功能不关乎具体页面,只有空视图与操作逻辑,那这个页面就是抽象的代码与视图。
但是其中每个页面的调用的接口会是不一样的,所以我们需要在此出创建接口时使用变量,如 ${fileName}/search
创建页面时使用此母版时,用页面的名作前缀等方案来解决。
那么页面一为user 那其查询路径就会为 user/search
那么页面二为code 那其查询路径就会为 code/search
这样就完成了母版的制作,即可以应用于后续的页面了
设计页面图
预览页面图
页面的创建
这一步当然还是以开发者角度,创建文件夹与页面(如用户管理 /user/index.jsx
)、servcie(相关接口)
、index.module.less
(样式文件)以及路由等。所以系统也是如此创建相关文件,多的功能就是可以选择 设计抽象代码与视图 设计的母版来初始化页面
将生成的代码写入对应的文件,来成为完整的项目。
可视化编辑区
还是熟悉的左侧组件列表区、中间设计区、属性配置区。将组件放入设计区后再在属性配置区中配置组件的属性。
组件列表
Form、Table、Dialog、Input、Select 等等组件, 而其对应的就是一条配置项,如:
const components = [
{
title: 'Row',
type: 'row',
//是不是容器、即可不可以在里面再放其它组件
isContainer: true,
//哪些组件可以放入其中,如 FormItem 应该只可以放在 Form 中
canPutGroup: ["element", "container"],
//配置的初始数据
data: {},
props: {
//必填项目提示
mustProps: ['prop'],
/** 将配置分为基本常用配置、不常用配置、样式配置 **/
//基本配置
base: [],
more: [],
style,
/**-----------------------------------------**/
//基本常用配置中可被继承的属性,即当某组件放入其中会附带这里的属性
baseInheritable: [],
//不常用配置中可被继承的属性,即当某组件放入其中会附带这里的属性
moreInheritable: [],
inheritableRules: (children, parent) => {
//处理不同组件组合时可能应该会有不同的属性
}
}
}
]
当然,配置应该是要和生成代码使用的框架息息相关的(如 antd),为了方便联动等功能我们将其轻改造了一下,具体可以见 文档,在此就不再过多介绍了,如Form组件的 baseInheritable 应该就有 load (是否加载组件)、label、rule等等可继承属性,即将 input 组件放在其中那么其配置就会多了load、label、rule 等,而放入table 则只有 load、label,都通过此配置完成。
组件列表图,与上数据一一对应
设计区
将组件放入并且编排结构,如放入一个表单Form、再在表单中放入一个输入Input、一个按键Button,其数据结构 (用于预览与代码生成)
const designList = [
{
title: 'Form',
type: 'Form',
//... 如上的其它配置
children: [
{
title: 'Input',
type: 'input'
//...其它配置
},
{
title: 'Input',
type: 'button'
}
]
}
]
将组件拖入设计器并生成相关数据结构当然不是困难的事,在此我要说一下为什么要使用抽象的结构而不使用组件的原型:不容易摆放、不容易确定边界(如将两个按钮放入到表格的一个列)、组件过大占用空间(因为我们是开发完成的页面,而不是表单,所以如富文本等占空间组件直接显示很不容易开发),当然直接显示的好处就是较直观,见之所得。所以取舍之下,选择了只展示结构 + 按住 ` 键即时预览来弥补不直观问题
设计结构图
结构预览图
属性配置
即对选中的组件的属性进行配置,配置的数据会在上而代码的 designList
中,如:
const designList = [
{
title: 'Form',
type: 'Form',
children: [
{
uuid: '--- uuid ---',
title: 'Input',
type: 'input'
/************ 配置的数据会在此 ************/
data: {
//将label配置为 姓名
label: '姓名',
//值为 abc时组件消失不显示
load: '({value}) => value !== "abc"',
config: { maxLength: 12 }
}
}
]
}
]
其通过唯一键 uuid 来对应组件,data 对应的则是配置表单的数据
配置图
变量、函数、effect 定义
一个完整的页面,一般也不会少的了(React 中的方法) useState、useCallback、useEffect等方法。所以创建这些函数自然也不能少,所以从开发的视角:
//创建一个 useState,保存到服务端则可以是name = 'loading',content = 'true', type = enum.state
const loading = useState(true)
//创建一个 useCallback,保存到服务端则可以是name = 'loadData',content = 'console.log(loading)', dependence = ['loading'], type = enum.callback
const loadData = useCallback(() => {
//使用变量$var.loading用于预览,直接loading是生成的代码
console.log(loading)
}, [loading])
//创建一个 useEffect, 保存到服务端则可以是content = 'loadData()', dependence = ['loadData'], type = enum.effect
useEffect(() => {
//使用函数$fn.loadData()用于预览,直接loadData()是生成的代码
loadData();
}, [loadData])
这些功能都在 设计抽象代码与视图 中设计时使用较多,尽可能在单个页面开发时只关心变化的组件放在哪里,而不关心逻辑
创建变量图
接口定义
一般我们开发的时候与服务端定义接口后会创建接口文件(如services/user.js),然后定义接口:
class UserService {
search(params) {
return Http.post('/user/search', params)
}
}
export default new UserService()
所以我们创建的时候同理,当然我们关注的是不可知的,如方法名,路径,Method, config,而不用关心文件名参数名,那么我们可以通过表单创建name = "search", method="post", url="/user/search", config=null
接口编辑图
在线预览
在线预览则是根据数据结构执行实际生成的代码所运行的功能,如我们页面的配置如下
//接口
const services = [
{ name: 'search', method='post', url='/user/search' }
]
//变量、函数、effect 定义
const preCodes = [
{type: enum.state, name: 'loading', content='true'},
{
type: enum.effect,
content: '$api.search().then(res => $set("loading", false))'
}
]
//设计
const designList = [
{
title: 'Form',
type: 'Form',
children: [
{
type: 'input'
data: {
prop: 'name'
label: '姓名'
}
},
{
type: 'button'
data: {
value: '提交'
prop: '$submit'
config: {
//变量
loading: '$var.loading'
}
}
}
]
}
]
如上数据实现预览,即是将模拟执行react代码
- 定义service对象,将方法挂到 $api = { search(params) { return httpservice.method } }return designList.map(el => {
//将配置以及数据翻译成相关组件
return React.createElement(...)
})
//执行字符串,可以借助 Function
new Function('params1', 'params2', 'functionString')(...params) - 将state变量挂载到对象 previewState上 const previewState, set = useState({ loading: false }),并将其挂到 window.$var
- 定义 $set(name, value) 函数来更新 previewState
- useEffect 中 执行 preCodes 的 effect 代码
- 将 designList 解析成 dom
在线预览图
代码生成
按上(在线预览)中的设计,思路与预览相同,只不过是 return 出字符串,然后通过 parser-babel 插件格式化代码即可
当然要注意将以 $
开头的方法指令去掉(如$var.loading
=> loading
),成为实际的代码
//定义代码数据类
class Icode {
states = [],
effects = [],
callbacks = []
renders = ''
...
}
...
//解析
designList.forEach(list => {
parser(list)
})
return icode.toString()
代码生成图
自定义组件
自定义组件是扩展系统的重要手段,同样开发者角度,如我们需要一个二维码生成,那么一般会找三方库,或自己写:
//安装依赖
npm i qrcode.react
//编写组件
import React from 'react'
import { QRCodeSVG } from 'qrcode.react'
export default function IQrcode({ value, ...config }) {
return <QRCodeSVG
value={value}
{...config}
/>
}
所以我们也同样如此,那么如何让这些代码直接在线运行并使用呢? 我的做法是以 umd 方式先全量编译上传到系统,系统解析成组件即可使用:
const packages = {
'react': React,
'react-dom': ReactDOM,
...
}
//核心解析方法,如果有更好的方法,还请不吝赐教,谢谢
const getParsedModule = (code) => {
const module = { exports: {} }
const require = (name) => {
return packages[name]
}
Function('require, exports, module, React', code)(require, module.exports, module, React)
return module.exports
}
当然,编译后的代码开发者是没法修改与维护的,所以,当前组件文件目录为 /icode
,那么我们 build 的文件为 /icode/dist
,那么我们将这个文件整个上传到系统,/dist
下的编译后的文件用于在线使用,而 /icode
中的实际代码用来生成对应文件到脚手架中如 /customComponents/icode
,再将依赖的库写入到 package.json
中,那么就像正常的开发者在开发了
将组件的配置信息根据相关规则插入到组件列表的 components
中,形成对应组件以及其使用的相关配置即可以融入系统内了
版本管理等再此先不介绍了
总结
此次从主要流程的设计思路入手,简要的介绍了对于管理类系统中重复性工作的解决方案,以及可视化/低代码的设计思路。
后面我会从零开始进行详细的设计,与大家分享交流,不合理的地方也请不吝赐教,谢谢大家
在线体验
下面是较为简陋的初代系统链接,有兴趣的可以随便玩玩尝试一下
网站 UI
(就没UI) 非常难看,希望多多吐槽