从零开发一款可视化搭建框架Dooringx-Lib

开发 架构
去年上线的可视化编辑器 H5-dooring 至今已有一年的时间,期间有很多热心的网友和大佬提出了非常多宝贵的建议,我们也在一步步实现中,以下是几个比较典型的低代码可视化平台需求。

[[423281]]

去年上线的可视化编辑器 H5-dooring 至今已有一年的时间,期间有很多热心的网友和大佬提出了非常多宝贵的建议,我们也在一步步实现中,以下是几个比较典型的低代码可视化平台需求:

  • 出码能力(即源码下载功能)
  • 组件交互(即组件支持业务中常用的链接跳转,弹窗交互,自定义事件等)
  • 数据源管理(即用户创建的不同页面拥有共享数据的能力,不同组件之间也有共享数据的能力)
  • 组件商店(即用户可以自主生产组件,定义组件,接入组件数据的能力)
  • 布局能力(即用户可以选择不同的布局方案来设计页面)
  • 常用功能集成(页面截图,微信分享,debug能力)

上面的这些功需求已经在 H5-dooring 陆续实现了,在我之前的文章中也有对应的技术分享。但是为了让更多的人能低成本的拥有自己的可视化搭建系统,我们团队的大佬花了非常多的时间研究和沉淀,最近也开源了一款可视化搭建框架 dooringx-lib,我们可以基于它轻松制作可视化编辑器,而无需考虑内部的实现细节,接下来我就和大家分享一下这款可视化框架的使用方式和实现思路,同时也非常感谢 dooring可视化团队 各位大佬们的辛勤付出。

可视化搭建框架基本使用和技术实现

为了让大家更好的理解可视化搭建框架,我这里举几个形象的例子:

antd —— antd-pro

我们都知道 antd 是流行的前端组件库,那么基于它上层封装的管理后台 antd-pro 就是它的上层应用。

GrapesJS —— craft.js

GrapesJS 是一款国外的页面编辑器框架(详细介绍可参考我之前的文章 这款国外开源框架, 让你轻松构建自己的页面编辑器) ,那么 craft.js 就是它的上层应用框架。

dooringx-lib —— dooringx

dooringx-lib 是一款可视化搭建框架,同理 dooringx 就是基于 dooringx-lib 的可视化编辑器。

之所以要介绍它们的区别,是因为之前有很多朋友对这块概念理解的不是很清晰,在了解了可视化搭建框架 的 “内涵” 之后,我们开始今天的核心内容。

1.技术栈

在分享框架实现思路之前当然要自报家门,框架实现上我们还是采用熟悉的 React 生态,移动端组件库采用的众安团队的 zarm,编辑器应用层采用的 antd,至于其他的比如拖拽,参考线,状态管理,插件机制等都是我们团队大佬自研的方案。如果你是 vue 或者其他技术栈为主的团队,也可以参考实现思路,相信也会对你有一定的启发。

2.基本使用方式

在开始深入之前我们先看看如何使用这款框架,我们只需要按照如下方式即可安装使用:

  1. npm/yarn install dooringx-lib 

同时我们还提供了基础的使用demo,方便大家在自己的工程中快速上手:

  1. # 克隆项目 
  2. # cnpmjs 
  3. git clone https://github.com.cnpmjs.org/H5-Dooring/dooringx.git 
  4.  
  5. or 
  6. git clone https://github.com/H5-Dooring/dooringx.git 
  7.  
  8.  
  9. # 进入项目目录 
  10. cd dooringx 
  11.  
  12. # 安装依赖 
  13. yarn install 
  14.  
  15. # 启动基础示例 
  16. yarn start:example 
  17.  
  18. # 启动 dooringx-lib 
  19. yarn start 
  20.  
  21. # 启动 dooringx doc 文档 
  22. yarn start:doc 
  23.  
  24. yarn build 

demo 的 github 项目如下:

github地址: https://github.com/H5-Dooring/dooringx

在了解完使用方式之后,我们来看看基本架构和实现思路。

3.dooringx-lib基础架构和工作机制

上图就是我根据目前 dooringx-lib 的项目架构梳理的架构图,基本包含了搭建化编辑框架的大部分必备模块。为了保证框架的灵活性,我们还可以按需安装对应的功能组件,开发自定义的组件等。如下是一个基本的导入案例:

  1. import { 
  2.     RightConfig, 
  3.     Container, 
  4.     useStoreState, 
  5.     innerContainerDragUp, 
  6.     LeftConfig, 
  7.     ContainerWrapper, 
  8.     Control, 
  9. from 'dooringx-lib'

我们将整个框架拆分成了不同的模块,这些模块既相互独立又可以相互关联。完整的工作流程如下:

由上图可以看出,我们只需要拥有基础的业务研发能力,就可以借助 dooringx-lib 构建一个属于自己的搭建平台,就好比任何程序的本质: 数据和逻辑。

4.dooringx-lib插件开发

接下来我会和大家分享 dooringx-lib 的插件开发方式和具体实现(如何导入插件,如何编写组件,如何注册函数等),如果大家感兴趣的话也可以跟着下面的方式实践一下。

4.1 如何导入组件

我们在上图可以看到左侧是我们的组件物料区,分为基础组件,媒体组件,可视化组件,它们的添加会统一放在 LeftRegistMap 数组中来管理,其基本结构如下:

  1. const LeftRegistMap: LeftRegistComponentMapItem[] = [ 
  2.   { 
  3.       type: 'basic', // 组件类别 
  4.       component: 'button', // 组件名称 
  5.       img: 'icon-anniu', // 组件icon 
  6.       displayName: '按钮', // 组件中文名 
  7.       urlFn: () => import('./registComponents/button'),  // 注册回调 
  8.   }, 
  9. ]; 

左侧组件支持同步导入或者异步导入。

如果需要异步导入组件,则需要填写 urlFn,需要一个返回 promise 的函数。也可以支持远程载入组件,只要 webpack 配上即可。

如果需要同步导入组件,则需要将组件放入配置项的 initComponentCache 中,这样在载入时便会注册进 componentRegister 里。

  1. initComponentCache: { 
  2.   modalMask: { component: MmodalMask },   
  3. }, 

4.2 如何定制左侧面板

左侧面板传入 leftRenderListCategory 即可。

  1. leftRenderListCategory: [ 
  2.   { 
  3. type: 'basic'
  4. icon: <HighlightOutlined />, 
  5. displayName: '基础组件'
  6.   }, 
  7.   { 
  8. type: 'xxc'
  9. icon: <ContainerOutlined />, 
  10. custom: true
  11. customRender: <div>我是自定义渲染</div>, 
  12.   }, 
  13. ], 

type 是分类,左侧组件显示在哪个分类由该字段决定。icon 则是左侧分类小图标(如上图所示)。当 custom 为 true 时,可以使用 customRender 自定义渲染。

4.3 开发一个自定义的可视化组件

组件需要导出一个由 ComponentItemFactory 生成的对象:

  1. const MButton = new ComponentItemFactory( 
  2.  'button'
  3.  '按钮'
  4.  { 
  5. style: [ 
  6.  createPannelOptions<FormMap, 'input'>('input', { 
  7.   receive: 'text',  
  8.   label: '文字'
  9.  }), 
  10. ], 
  11. animate: [createPannelOptions<FormMap, 'animateControl'>('animateControl', {})], 
  12. actions: [createPannelOptions<FormMap, 'actionButton'>('actionButton', {})], 
  13.  }, 
  14.  { 
  15. props: { 
  16.  ... 
  17.  text:'x.dooring'// input配置项组件接收的初始值 
  18. }, 
  19.  }, 
  20.  (data, context, store, config) => { 
  21. return <ButtonTemp data={data} store={store} context={context} config={config}></ButtonTemp>; 
  22.  }, 
  23.  true 
  24. ); 
  25.  
  26. export default MButton; 

其中第一个参数为组件注册名,第二个参数用来展示使用。

第三个参数用来配置右侧面板的配置项组件。其中键为右侧面板的分类,值为配置项组件数组。

第四个参数会配置组件的初始值,特别注意的是,制作组件必须要有初始宽度高度(非由内容撑开),否则会在适配时全选时产生问题。

这个初始值里有很多有用的属性,比如fixed代表使用固定定位,可以结合配置项更改该值,使得组件可以fixed定位。

还有 canDrag 类似于锁定命令,锁定的元素不可拖拽。

初始值里的 rotate 需要个对象,value 代表旋转角度,canRotate 代表是否可以操作旋转。(0.7.0版本开始支持)

第五个参数是个函数,你将获得配置项中的 receive 属性(暂且都默认该配置为receive)传来的配置,比如上例中 receive 的是 text,则该函数中 data 里会收到该字段。

context 一般只有 preview 和 edit,用来进行环境判断。

config 可以拿到所有数据,用来制作事件时使用。

第六个参数 resize 是为了判断是否能进行缩放,当为 false 时,无法进行缩放。

第七个参数 needPosition,某些组件移入画布后会默认采取拖拽的落点,该配置项默认为 true, 就是需要拖拽的位置,为 false 时将使用组件自身 top 和 left 定位来放置。

4.4 事件注册

注册时机

事件可以细分为 注册时机 和 函数,组件内可以通过 hook 的方式来实现注册时机:

  1. useDynamicAddEventCenter(pr, `${pr.data.id}-init`, '初始渲染时机'); //注册名必须带id 约定! 
  2. useDynamicAddEventCenter(pr, `${pr.data.id}-click`, '点击执行时机'); 

useDynamicAddEventCenter 第一个参数是 render 的四个参数组成的对象。第二个参数是注册的时机名,必须跟 id 相关,这是约定,否则多个组件可能会导致名称冲突,并且方便查找该时机。

注册完时机后,我们需要将时机放入对应的触发位置上,比如这个 button 的点击执行时机就放到 onclick 中:

  1. <Button 
  2.     onClick={() => { 
  3. eventCenter.runEventQueue(`${pr.data.id}-click`, pr.config); 
  4.     }} 
  5.     x.dooring 
  6. </Button>  

 

其中第一个参数则为注册的时机名,第二个为 render 函数中最后一个参数 config

函数注册

函数由组件抛出,可以加载到事件链上。比如,注册个改变文本函数,那么我可以在任意组件的时机中去调用该函数,从而触发该组件改变文本。

函数注册需要放入 useEffect 中,在组件卸载时需要卸载函数!否则会导致函数越来越多。

  1. useEffect(() => { 
  2. const functionCenter = eventCenter.getFunctionCenter(); 
  3. const unregist = functionCenter.register( 
  4.  `${pr.data.id}+改变文本函数`, 
  5.  async (ctx, next, config, args, _eventList, iname) => { 
  6.   const userSelect = iname.data; 
  7.   const ctxVal = changeUserValue( 
  8.    userSelect['改变文本数据源'], 
  9.    args, 
  10.    '_changeval'
  11.    config, 
  12.    ctx 
  13.   ); 
  14.   const text = ctxVal[0]; 
  15.   setText(text); 
  16.   next(); 
  17.  }, 
  18.  [ 
  19.   { 
  20.    name'改变文本数据源'
  21.    data: ['ctx''input''dataSource'], 
  22.    options: { 
  23.     receive: '_changeval'
  24.     multi: false
  25.    }, 
  26.   }, 
  27.  ] 
  28. ); 
  29. return () => { 
  30.  unregist(); 
  31. }; 
  32. }, []); 

函数中参数与配置见后面的函数开发。

4.5 右侧面板开发

为了开发自定义的右侧属性面板,我们只要将开发的组件配成一个对象放入 initFormComponents 即可。为了良好的开发体验,需要定义个 formMap 类型:

  1. export interface FormBaseType { 
  2.     receive?: string; 
  3. export interface FormInputType extends FormBaseType { 
  4.     label: string; 
  5. export interface FormActionButtonType {} 
  6. export interface FormAnimateControlType {} 
  7. export interface FormMap { 
  8.     input: FormInputType; 
  9.     actionButton: FormActionButtonType; 
  10.     animateControl: FormAnimateControlType; 

formMap 的键名就是 initFormComponents 键名,formMap 的值对应组件需要收到的值。

以 input 组件为例,FormInputType 此时有2个属性: label, receive。

那么在开发该组件时,props 会收到:

  1. interface MInputProps { 
  2.     data: CreateOptionsRes<FormMap, 'input'>; 
  3.     current: IBlockType; 
  4.     config: UserConfig; 

也就是 data 是 formMap 类型,而 current 是当前点击的组件,config 就不用说了。

还记得在左侧组件开发中的第三个参数吗?这样就都关联起来了:

  1. style: [ 
  2.     createPannelOptions<FormMap, 'input'>('input', { 
  3.         receive: 'text',   
  4.         label: '文字' 
  5.     }) 
  6. ], 

createPannelOptions 这个函数的泛型里填入对应的组件,将会给收到的配置项良好的提示。

在配置项组件里所要做的就是接收组件传来的配置项,然后去修改 current 的属性:

  1. function MInput(props: MInputProps) { 
  2.  const option = useMemo(() => { 
  3. return props.data?.option || {}; 
  4.  }, [props.data]); 
  5.  return ( 
  6. <Row style={{ padding: '10px 20px' }}> 
  7.  <Col span={6} style={{ lineHeight: '30px' }}> 
  8.   {(option as any)?.label || '文字'}: 
  9.  </Col> 
  10.  <Col span={18}> 
  11.             <Input 
  12.                 value={props.current.props[(option as any).receive] || ''
  13.                 onChange={(e) => { 
  14.                         const receive = (option as any).receive; 
  15.                         const clonedata = deepCopy(store.getData()); 
  16.                         const newblock = clonedata.block.map((v: IBlockType) => { 
  17.                                 if (v.id === props.current.id) { 
  18.                                         v.props[receive] = e.target.value; 
  19.                                 } 
  20.                                 return v; 
  21.                         }); 
  22.                         store.setData({ ...clonedata, block: [...newblock] }); 
  23.                 }} 
  24.             ></Input> 
  25.  </Col> 
  26. </Row> 
  27.  ); 

由于可以很轻松的拿到 store,所以可以在任意地方进行修改数据。

将组件的 value 关联 current 的属性,onChange 去修改 store,这样就完成了个双向绑定。

注意:如果你的右侧组件需要用到 block 以外的属性,可能需要去判断是否处于弹窗模式。

4.6 自定义右键菜单

右键菜单可以进行自定义:

  1. // 自定义右键 
  2. const contextMenuState = config.getContextMenuState(); 
  3. const unmountContextMenu = contextMenuState.unmountContextMenu; 
  4. const commander = config.getCommanderRegister(); 
  5. const ContextMenu = () => { 
  6.  const handleclick = () => { 
  7. unmountContextMenu(); 
  8.  }; 
  9.  const forceUpdate = useState(0)[1]; 
  10.  contextMenuState.forceUpdate = () => { 
  11. forceUpdate((pre) => pre + 1); 
  12.  }; 
  13.  return ( 
  14. <div 
  15.  style={{ 
  16.             left: contextMenuState.left
  17.             top: contextMenuState.top
  18.             position: 'fixed'
  19.             background: 'rgb(24, 23, 23)'
  20.  }} 
  21.  <div 
  22.             style={{ width: '100%' }} 
  23.             onClick={() => { 
  24.                     commander.exec('redo'); 
  25.                     handleclick(); 
  26.             }} 
  27.         > 
  28.             <Button>自定义</Button> 
  29.  </div> 
  30. </div> 
  31.  ); 
  32. }; 
  33. contextMenuState.contextMenu = <ContextMenu></ContextMenu>; 

先拿到 contextMenuState,contextMenuState 上有个 unmountContextMenu 是关闭右键菜单方法。所以在点击后需要调用关闭。同时上面的 left 和 top 是右键的位置。另外,我们还需要在组件内增加强刷,赋值给 forceUpdate,用于在组件移动时进行跟随。

4.7 表单验证提交思路

表单验证提交有非常多的做法,因为数据全部是联通的,或者直接写个表单组件也可以。在不使用表单组件时,简单的做法是为每个输入组件做个验证函数与提交函数。这样是否验证就取决于用户的选取,而抛出的输入可以让用户选择放到哪,并由用户去命名变量。

在点击提交按钮时,调用所有组件的验证函数与提交函数,使其抛给上下文,再通过上下文聚合函数聚合成对象,最后可以通过发送函数发送给对应后端,从而完成整个流程。我们可以在 dooringx 中试下这个demo。

另一种方式是可以专门写个提交按钮,固定了参数,以及部分规则,比如规定在页面中的所有表单都会被收集提交。

 

那么我们可以利用数据源,将所有表单输出内容自动提交给数据源,最后的提交按钮按数据源规定格式的key 提取,发送给后端。

后期规划

后期我们还会在产品功能方面持续迭代优化,如果大家有好的建议, 也可以随时和我们交流, 也欢迎在 github 上积极提 issue。

 

责任编辑:武晓燕 来源: 趣谈前端
相关推荐

2021-07-12 17:23:47

零设计可视化引擎

2023-03-08 07:45:50

可视化编程工具SpringBoot

2021-03-09 08:32:50

开发视化大屏H5-Dooring

2023-09-26 08:01:16

2022-02-28 08:34:42

开发可视化大屏

2019-10-10 08:46:02

Docker可视化技术浏览器

2022-05-17 10:05:55

Karma可视化面板Linux

2018-09-09 23:07:17

物联网可视化编程工具

2022-05-24 15:03:44

开源工具可视化

2020-07-27 09:59:25

Kafka可视化CMAK

2024-03-11 08:32:02

2021-04-12 08:31:53

PC-Dooring项目PC端搭建

2021-08-26 05:15:22

图片编辑器 H5-DooringMitu-Doorin

2022-08-31 08:32:22

数据可视化项目nocode

2016-03-15 12:27:54

WireEdit可视化编辑工具网络数据包编辑器

2023-12-26 15:14:00

2023-02-20 15:09:00

可视化搭建项目开源

2022-07-12 09:35:59

JSON可视化工具

2022-08-15 08:02:09

Go程序函数

2019-11-26 08:43:44

平台桌面软件
点赞
收藏

51CTO技术栈公众号