Appearance
从零开发H5可视化搭建项目
01.现阶段的可视化搭建的分类
三种分类
第一种:基于dom元素 最终结果给你一个网页
第二种: 基于组件化 最终结果给你组件代码
第三种: 是第一种的升级版 就是把常见的业务做成可调整的组件 最终给你一个网页
存在的问题
- 交互逻辑需要侵入开发,无法自动生成
- 只能在受限、具体的业务场景下发挥作用
自动生成的代码可维护性差 而且有些逻辑需要二开
一般适用于非产品化的页面,开发完不用考虑维护性.
02.如何设计可视化系统
四大问题
需要有丰富的模板、组件玩法满足各种业务场景
需要有易用的可视化编辑器,所见即所得
需要有页面发布能力,支持编辑后页面随时发布上线
最重要的是稳定性,保障线上项目安全稳定运行
解决方案:
问题1
- 编辑系统和组件解偶,组件只需要遵循编辑系统的组织约定, 其具体开发过程和承载的逻辑与编辑系统无关, 支持自由拓展页面组件.
- 编辑系统与模板采用的前端框架解偶, 在遵循编辑系统约定下, 可以选择不同的前端框架.
问题2
用json schema来描述数据结构和类型 展示表单的类型与样式 然后表单对应的值用json展示
问题3
通过 page = fn(view, data)
这样的模式来编译出最终的页面
就把页面当成一个函数 来显示 通过配置对应的view (json schema)和 data(json)来设置页面的样式
问题4
上线后 具体样式如何保存更新也是需要考虑的
整体的架构
一些成熟的产品
教程 | Vue 3.x From Render (muwoo.github.io)
03.前置基础知识准备
可视化搭建的概念:
远程组件
总会出现一些多业务线共用一个组件的场景,如果各业务团队向框架中提供一些私有的展示组件,但是这些组件并不能和框架一起打包,因为框架不能因为某个私有模块的频繁变更而重复构建发布。
lowcode
低代码开发平台减轻了非技术开发人员的压力,帮其免去了代码编写工作,同时也为专业开发人员提供了支持,帮助他们提取应用开发过程中的繁琐底层架构与基础设施任务。
页面模板和全局组件 模板组件是无法进行跨模板共享,只用用于当前页面使用。
全局组件
是远程组件的实现方式,可以用来跨多模板共享。
技术栈:
- egg
- vue3
- antdv
04.模板设计
1.整体架构
既然模板库独立于编辑器且由相关业务线开发同学自己来来发,而开发规范因为团队的不同可能会有所差异,所以我们设计一套具有相关约定且框架无关的模板体系,来满足多业务线同学的开发诉求。
2.模板的例子
所以我们可以按照业务功能的维度划分成2个组件:
- 头部 banner 介绍组件
- 内容填写 form 组件
目录结构
...
├─package.json
├─src
| ├─App.vue
| ├─main.js
| ├─components
| | ├─form
| | ├─banner
...
初始化模板页面
banner
vue
<template>
<a :href="obj.link">
<img
:src="obj.src"
width="100%"
alt="图片"
/>
</a>
</template>
<script>
export default {
name: 'banner',
props: {
obj: {
type: Object,
default: () => {}
}
}
}
</script>
props schema 设计
我们虽然将组件的可编辑的属性通过了 props
来表述出来了,但是外部如何来感知我们具体的数据格式和类型 这时候需要用 JSON Schema
配置数据描述格式
新建一个 package.json
文件
json
{
"type": "object",
"properties": {
"src": {
"title": "图片地址",
"type": "string",
"format": "image"
},
"link": {
"title": "跳转链接",
"type": "string",
"format": "url"
}
},
"required": [
"src"
]
}
同理form的 schema可以描述为
json
{
"type": "object",
"properties": {
"btnText": {
"title": "按钮文案",
"type": "string"
},
"action": {
"title": "提交接口地址",
"type": "string",
"format": "url"
}
},
"required": [
"btnText"
]
}
此时的项目结构
...
├─package.json
├─src
| ├─App.vue
| ├─main.js
| ├─components
| | ├─form
| | | ├─index.vue
| | | └package.json
| | ├─banner
| | | ├─index.vue
| | | └package.json
...
05.模板通信设计
有三个问题
- 有哪些组件:编辑器需要对用户展示当前模板有哪些可用的模板组件
- 组件的基础信息:组件缩略图,用于展示给用户基础的 UI 样式。组件的名称,用于模板的编排。组件的描述,用于提示用户该组件的一些信息。组件的可编辑内容,用于用户对组件信息编写
- 如何告知:毕竟组件和编辑器是解耦的,那应该通过什么方式来通知编辑器?
有哪些组件
所有的组件我们都写到了 components
目录下。那么无非是读取 components
目录中的组件信息,进行展示即可。在前端获取目录的组织结构,我们可以利用 webpack
提供的 require.context
的功能
js
function getComponent() {
const componentConfig = [];
const requireConfig = require.context(
'./components',
// 是否查询其子目录
true,
/package.json$/
);
requireConfig.keys().forEach(fileName => {
const config = requireConfig(fileName);
componentConfig.push(config);
});
return componentConfig;
}
组件的基础信息
拿 banner 组件举例,按照 组件名
、组件描述
、组件缩略图
、schema
的方式设计以下数据结构:
json
{
"name": "coco-banner",
"description": "banner 组件",
"snapshot": "https://cdn.img/banner.png",
"schema": {
"type": "object",
"properties": {
"src": {
"title": "图片地址",
"type": "string",
"format": "image"
},
"link": {
"title": "跳转链接",
"type": "string",
"format": "url"
}
},
"required": [
"src"
]
}
}
再结合之前的获取组件信息的代码,我们执行后,对编辑器展示出的模板所有组件信息,就可以用以下数据结构来表达:
json
[
{
description: "banner 组件",
name: "coco-banner",
schema: {...},
snapshot: "https://cdn.img/banner.png",
},
{
description: "form 组件",
name: "coco-form",
schema: {...},
snapshot: "https://cdn.img/form.png",
}
]
如何告知
通信问题就转化为如何实现 2 个 iframe
之间的通信。所以可以采用 postMessage:
js
export function postMsgToParent (message) {
window.parent.postMessage(
message,
'*'
);
}
// 通知父容器
postMsgToParent({
type: 'returnConfig',
data: {
components: this.componentConfig, // 当前模板信息
// ...
}
});
参考资料
关于工程自动化导入:require.context
工程自动化导入:require.context - 掘金 (juejin.cn)
require.context()让VUE开发更简单,get it~ - 掘金 (juejin.cn)
vue 引入公共组件之 require.context - 掘金 (juejin.cn)
06.模板动态化交互
我们继续介绍模板如何接收编辑器传递过来的对模板编辑后的消息,并消息进行实时响应。
01.动态组件
任何程序设计都可以抽象成 算法 + 数据结构
的模式。要实现模板的动态渲染,我们肯定不能按照之前 Vue 的开发方式写死组件:
vue
<template>
<div>
<coco-banner />
<coco-form />
</div>
</template>
这样写死就无法对模板组件的顺序、格式进行编辑。要对模板的顺序进行编排,就需要我们将展示的数据结构设计出 数组
, 数组中包含了对模板可展示数据的基础描述。所以我们可以按照这样的数据格式来表述我们模板渲染的布局:
json
{
"userSelectComponents": [
{
"name": "co-banner",
"props": {
"src": "",
"link": ""
}
},
{
"name": "co-form",
"props": {
"btnText": "",
"action": ""
}
}
]
}
React实现方案
jsx
const Banner = (props) => {
render() {
return <div />
}
};
const Form = (props) => {
render() {
return <div />
}
}
const Components = {
'co-banner': Banner,
'co-form': Form,
};
const Page = () => {
render() {
return (
<div>
{
userSelectComponents.map(config => {
const Component = Components[config.name];
return <Component {...config.props} />;
});
}
</div>
)
}
}
Vue实现方案
vue
<div
:id="`coco-render-id-_component_${index}`"
:key="index"
v-for="(component, index) in components"
>
<div
:is="component.name"
:key="component + index"
:obj="component.props"
:config="component.config"
/>
</div>
02.接收消息
上篇我们说到了模板如何发消息给编辑器,那么编辑器对数据结构进行编辑后,如何通知模板做对应的改变呢
我们也可以通过 postMessage
来进行通信,但是具体需要调用哪个模板里面哪个功能(编辑、排序、删除),我们可以采用一种取巧的方式来写:
js
export default {
created() {
window.addEventListener('message', (e) => {
// 不接受消息源来自于当前窗口的消息
if (e.source === window || e.data === 'loaded') {
return;
}
this[e.data.type](e.data.data);
});
},
methods: {
addComponent() {
// todo add componet
},
changeProps(payload) {
this.$set(this.components[this.currentIndex], 'props', payload);
},
}
}
这里的消息通信是基于 postMessage
来实现的,要想跨框架,就必须要求我们对编辑器传递过来的消息进行消费,所以我们可以设计一套通用的消息处理 Adapter
来对消息进行处理. coco-component
就包含和编辑器后台的消息通信处理以及基础的模板渲染。
vue
<CocoComponent>
<coco-banner :obj="{
src: require('./assets/banner.jpg'),
link: 'https://coco.com',
}" />
<coco-form />
</CocoComponent>
通过类似高阶组件的方式来实现对模板通用功能的抽象。其中 coco-banner
和 coco-form
是对模板基础功能的初始化布局。最后整体架构大致如下
总结
我们的模板开发完成经过测试后开开心心发布了,但是后面运营说这个表单模板我需要再加个抽奖转盘,我们又重新升级了模板。但是升级后发现跟之前发布过的页面不兼容,那么我们如何来控制模板的影响面呢?