Skip to content
本页目录

从零开发H5可视化搭建项目

01.现阶段的可视化搭建的分类

三种分类

可视化搭建分类

第一种:基于dom元素 最终结果给你一个网页

VvvebJs

第二种: 基于组件化 最终结果给你组件代码

Iceworks 创建应用_w3cschool

第三种: 是第一种的升级版 就是把常见的业务做成可调整的组件 最终给你一个网页

H5网站制作平台 (dooring.cn)

存在的问题

  1. 交互逻辑需要侵入开发,无法自动生成
  2. 只能在受限、具体的业务场景下发挥作用

自动生成的代码可维护性差 而且有些逻辑需要二开

一般适用于非产品化的页面,开发完不用考虑维护性.

02.如何设计可视化系统

四大问题

  1. 需要有丰富的模板、组件玩法满足各种业务场景

  2. 需要有易用的可视化编辑器,所见即所得

  3. 需要有页面发布能力,支持编辑后页面随时发布上线

  4. 最重要的是稳定性,保障线上项目安全稳定运行

解决方案:

问题1
  1. 编辑系统和组件解偶,组件只需要遵循编辑系统的组织约定, 其具体开发过程和承载的逻辑与编辑系统无关, 支持自由拓展页面组件.
  2. 编辑系统与模板采用的前端框架解偶, 在遵循编辑系统约定下, 可以选择不同的前端框架.
问题2

用json schema来描述数据结构和类型 展示表单的类型与样式 然后表单对应的值用json展示

img

问题3

通过 page = fn(view, data) 这样的模式来编译出最终的页面

就把页面当成一个函数 来显示 通过配置对应的view (json schema)和 data(json)来设置页面的样式

问题4

上线后 具体样式如何保存更新也是需要考虑的

img

整体的架构

img

一些成熟的产品

教程 | Vue 3.x From Render (muwoo.github.io)

Variant Form 3 Pro

Playground (xrender.fun)

03.前置基础知识准备

可视化搭建的概念:

  1. 远程组件

    总会出现一些多业务线共用一个组件的场景,如果各业务团队向框架中提供一些私有的展示组件,但是这些组件并不能和框架一起打包,因为框架不能因为某个私有模块的频繁变更而重复构建发布。

  2. lowcode

    低代码开发平台减轻了非技术开发人员的压力,帮其免去了代码编写工作,同时也为专业开发人员提供了支持,帮助他们提取应用开发过程中的繁琐底层架构与基础设施任务。

  3. 页面模板和全局组件 模板组件是无法进行跨模板共享,只用用于当前页面使用。

    全局组件 是远程组件的实现方式,可以用来跨多模板共享。

技术栈:

  • egg
  • vue3
  • antdv

04.模板设计

1.整体架构

既然模板库独立于编辑器且由相关业务线开发同学自己来来发,而开发规范因为团队的不同可能会有所差异,所以我们设计一套具有相关约定且框架无关的模板体系,来满足多业务线同学的开发诉求。

image-20230620140642770

2.模板的例子

image-20230620143616579

所以我们可以按照业务功能的维度划分成2个组件:

  1. 头部 banner 介绍组件
  2. 内容填写 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.模板通信设计

有三个问题

  1. 有哪些组件:编辑器需要对用户展示当前模板有哪些可用的模板组件
  2. 组件的基础信息:组件缩略图,用于展示给用户基础的 UI 样式。组件的名称,用于模板的编排。组件的描述,用于提示用户该组件的一些信息。组件的可编辑内容,用于用户对组件信息编写
  3. 如何告知:毕竟组件和编辑器是解耦的,那应该通过什么方式来通知编辑器?

有哪些组件

所有的组件我们都写到了 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-bannercoco-form 是对模板基础功能的初始化布局。最后整体架构大致如下

img

总结

我们的模板开发完成经过测试后开开心心发布了,但是后面运营说这个表单模板我需要再加个抽奖转盘,我们又重新升级了模板。但是升级后发现跟之前发布过的页面不兼容,那么我们如何来控制模板的影响面呢?