1, the preface


For the purpose of deep learning, let’s implement a H5 page visual editor/Visual configuration platform (NOcode) project technology stack together:

  • Front-end Vue + VueRouter + Vuex + Scss + ElementUI
  • Server Egg + MySQL + Sequelize
  • Other VueCli + Eslint + husky

Platform address: Visual configuration platform H5 page: visual configuration platform Output Demo page Front-end code address: Git repository server code address: Git repository

2. Thinking and architecture diagram


3. Effect drawing


4. Function list


  • Component display area drag onto the canvas area to generate page elements
  • Drag and drop inside the canvas to adjust the display order of page components
  • When a component is selected on the canvas, drag the corner to quickly change the width and height of the component
  • Shortcuts to save activities and copy, paste, and delete components
  • Import and export real-time JSON data
  • Canvas can be adjusted to size and scale
  • Component style customization configuration and live preview
  • Component events are custom configured and enable js code behavior to be executed
  • Component entry animation configuration and preview

5. Code directory structure description


| -. Detection of husky - githook code before submitting | - public | -- index. HTML - b side entrance HTML | - view. HTML - h5 entrance HTML | - server node service | - sh | - build. Sh - build script | -- deploy. Sh - deployment script | - SRC | - API - interface management | - components - b side common components | - config Configuration file | | -- the animate. Js - animation -- event. Js - time profile | - json_scheme. | js - the overall data structure -- style. | - directives - js - style configuration files Public instruction | - | plugins - component library - the router routing management | - store - public state management | - style - style | | - views - utils - public js method | | - drag - visual configuration platform - edit - editor area | | - wrapper/edit - canvas - wrapper/Header - | - wrapper/LeftAside - at the top of the operating area Page and component exhibit | - wrapper/RightAside - component and form configuration page list page | | - list - activity - h5 - h5 page | | - auto - visual construction deployment, doc - document | | - App. Vue - entry vue instance -- main. Js - entrance js file | -- permission. | js - routing guards -- vue. Config. Js - vue/cli configurationCopy the code

6. Data structure description

First we need to define all the data structures, such as an activity, a page, a component, a base event, a style, a base approach animation, etc

An active data
Const activity = {title: "", // title: description: "", // author: "", // author: ", // pages: [] //Copy the code
Page data
Const page = {name: "", // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ""," background - image ":" ", "background - size" : "cover"}, config: {}} / / other configuration;Copy the code
A component data
Const element = {name: "", // name: "", // icon: "", // icon animate: "", // [], // Event configuration data, each layer can add multiple events configMap: {}, // component configuration information styleInfo: {} // style information};Copy the code
A configured dynamic form data
{key: "", // configure the key value value: "", // configure the value value valueType: "", // formType: "", // configure the corresponding formType placeholder: When: (configMap) => {return true; when: (configMap) => {// When to display the configuration item. }, label: ""Copy the code
Style data
Const commonStyleConfig = {// All CSS attributes can be added, but not necessary "display": {key: "display", value: "block", valueMap: [{value: {value: "inline-block", desc: "inline block"}, valueType: "enum", formType: "select", placeholder: placeholder "", tip: "Please select element type ", label:" element type "}};Copy the code
The event data
Const trigger = {key: "trigger", value: "click", valueMap: [{value: "click", desc: "click"}, {value: "LongPress ", desc: "longPress", {value: "load", desc: "load"}], valueType: "enum", formType: "select", placeholder: placeholder "Please select event triggering mode ", tip:" Please select event triggering mode ", label: "Triggering mode"}; Const action = {key: "action", value: "toast", valueMap: [{value: "toast", desc: "toast prompt "}, {value: "JumpLink ", desc: "jumpLink", valueType: "enum", formType:" SELECT ", placeholder: "placeholder ", label: "Trigger behavior"}; Const configMap = {text: {key: "text", value: "toast ", valueType: "string", formType: "input", placeholder: "Please enter toast prompt text ", tip:" Please enter toast prompt text ", label: "Prompt text ", when: (actionType) => {return actionType === "toast"; }}};Copy the code
Entry animation data
Const animateMap = [{value: "bounceInLeft", desc: "left"}, {value: "right ", desc:" left "}, {value: "right ", desc:" right "}, {value: "Desc: bounceInDown", "by flying into"}, {value: "bounceInUp desc:" by into "}, {value: "bounceIn desc:" changes from small to big "}, {value: "RubberBand ", desc: "fadeIn", {value: "fadeIn", desc: "fadeIn"}, {value: "fadeInDown", desc: "fadeIn"}, {value:" fadedown ", desc: "fadeIn"}, {value: "fadedown ", desc: "fadeIn"}, {value: "FadeInLeft ", desc: "fadeInLeft", {value: "fadeInRight", desc: "fadeInRight", {value: "fadeInUp", desc: "fadeInUp", {value: "fadeInUp", desc: "fadeInUp", {value: "fadeInUp", desc: "fadeInUp", {value: "fadeInUp", desc: "fadeInUp", {value: "FlipInX ", desc: "flipInY"}, {value: "flipInY", desc: "flipInY"}, {value: "rollIn", desc: "flipInY"}, {value: "rollIn", desc: "flipInY"}, {value: "flipInY", desc: "flipInY"}, {value: "Flip ", desc: "flip admission"}];Copy the code

7. Some important function codes

Page editing area

The editor is implemented as a traversal of vUE dynamic components

<NjElementBox
    v-for="(item, index) in curPageData.elements"
    :id="item.uuid"
    :key="item.uuid"
    :style="item.styleInfo"
    :style-info.sync="item.styleInfo"
    :component-resizing.sync="componentResizing"
    :target-id="item.uuid"
    :class="[
        {'active': item.uuid === editingComponent.uuid},
        item.animate,
        'animated',
        {'move': !componentResizing}
    ]"
    @click.native="setEditingComponent(item)"
    @deleteElement="deleteElement(index)"
    @updateEditingStyleInfo="updateEditingStyleInfo"
>
    <component :is="item.name" class="nj-element" :item="item" />
</NjElementBox>
Copy the code
Quick scaling of component width and height

The main use of Mousedown and Mousemove event monitoring, obtain the real-time mouse position, calculate the movement trajectory to change the width and height properties of the component

<div class="nj-element-box"> <slot></slot> <i class="lt" @mousedown="(e) => { handlerMousedown(e, 'lt')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="lm" @mousedown="(e) => { handlerMousedown(e, 'lm')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="lb" @mousedown="(e) => { handlerMousedown(e, 'lb')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="rt" @mousedown="(e) => { handlerMousedown(e, 'rt')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="rm" @mousedown="(e) => { handlerMousedown(e, 'rm')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="rb" @mousedown="(e) => { handlerMousedown(e, 'rb')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="mt" @mousedown="(e) => { handlerMousedown(e, 'mt')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <i class="mb" @mousedown="(e) => { handlerMousedown(e, 'mb')}" @mouseout="resetFlag" @mouseover="setFlag"></i> <span class="mask"></span> </div> handlerMousedown(ev, type) { const startX = ev.x; const startY = ev.y; const targetEl = document.getElementById(this.targetId); const originW = targetEl.offsetWidth; const originH = targetEl.offsetHeight; const leftToPar = (! this.styleInfo["margin-left"] || this.styleInfo["margin-left"] === "auto") ? 0 : (+(this.styleInfo["margin-left"].replace("px", "").replace("%", "")) || 0); document.onmousemove = (e) => { e.preventDefault(); _throttleHandler(() => { this.resizeFun({ e, type, startX, startY, originW, originH, leftToPar }); }); }; document.onmouseup = () => { document.onmousemove = null; document.onmouseup = null; }; }, resizeFun({ e, type, startX, startY, originW, originH, leftToPar }) { let targetW, targetH; const moveX = e.x - startX; const moveY = e.y - startY; switch (type) { case "lt": targetW = originW - moveX; targetH = originH - moveY; break; case "lb": targetW = originW - moveX; targetH = originH + moveY; break; case "rt": targetW = originW + moveX; targetH = originH - moveY; break; case "rb": targetW = originW + moveX; targetH = originH + moveY; break; case "lm": targetW = originW - moveX; targetH = originH; break; case "rm": targetW = originW + moveX; targetH = originH; break; case "mt": targetW = originW; targetH = originH - moveY; break; case "mb": targetW = originW; targetH = originH + moveY; break; } if (leftToPar + targetW > 375) { targetW = 375; } const styleInfo = this.styleInfo; styleInfo.width = targetW + "px"; styleInfo.height = targetH + "px"; this.$emit("update:styleInfo", styleInfo); this.$emit("updateEditingComponent", styleInfo); }Copy the code
Component list area to page edit area drag function

The h5 native Drag attribute is mainly used. The setData and getData methods of E. datatransfer are used to transfer the component code by value, and then find the corresponding component data from the general configuration to fill in, and finally achieve the effect of component drag

/ / source: Component list display area <ul class=" elder-list "@dragStart ="handleDragStart"> <li v-for="(value, key) in configList" :key="key" class="element-item" draggable :data-index="key" > <p> <i class="iconfont" :class="value.icon"></i> </p> <p class="mini">{{ value.desc }}</p> </li> </ul> handleDragStart(e) { e.dataTransfer.setData("index", e.target.dataset.index); } // target <div id="mobileView" class="mobile-view" :style="pageStyle" :draggable="false" @drop="handleDrop" @dragover="handleDragOver" > </div> handleDragOver(e) { e.preventDefault(); }, handleDrop(e) { const key = e.dataTransfer.getData("index"); if (! key) return; const item = deepClone(this.configList[key]); item.uuid = uuidv4(); const curPageData = this.curPageData; item.configCode = key; curPageData.elements.push(item); },Copy the code
Drag and drop in the page edit area

Using the VueDraggable component

<Draggable
    v-model="curPageData.elements"
    class="dragger-box"
    handle=".move"
    filter=".unmove"
    :animation="400"
>
    <NjElementBox
        v-for="item in curPageData.elements"
        :id="item.uuid"
        :key="item.uuid"
        :style="item.styleInfo"
        :style-info.sync="item.styleInfo"
        :target-id="item.uuid"
        :component-resizing.sync="componentResizing"
        :class="[
            {'active': item.uuid === editingComponent.uuid},
            item.animate,
            'animated',
            {'move': !componentResizing}
        ]"
        @click.native="setEditingComponent(item)"
        @updateEditingComponent="updateEditingComponent"
    >
        <component :is="item.name" class="nj-element" :item="item" />
    </NjElementBox>
</Draggable>
Copy the code
Component configures dynamic forms
<el-form-item v-if="! item.when || item.when(editingComponent.configMap)" class="right-form-item" :label="item.label" > <el-input v-if="item.formType === 'input'" v-model="item.value" v-setDisableKeycode :placeholder="item.placeholder" /> <el-input v-if="item.formType === 'textarea'" v-model="item.value" v-setDisableKeycode :autosize="{ minRows: 2, maxRows: 8}" :placeholder="item.placeholder" type="textarea" /> <el-select v-if="item.formType === 'select'" v-model="item.value"  > <el-option v-for="i in item.valueMap" :key="i.value" :label="i.desc" :value="i.value" /> </el-select> <el-upload v-if="item.formType === 'upload'" action="/" class="avatar-uploader" :show-file-list="false" :on-success="(res, file) => { handleSuccess(res, file, item) }" :before-upload="(file) => { beforeUpload(file, item) }" > <img v-if="item.value" :src="item.value" class="avatar" /> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> <el-tooltip v-if="item.formType ! == 'upload' && item.tip" effect="dark" :content="item.tip" placement="top" > <i class="el-icon-info"></i> </el-tooltip> </el-form-item>Copy the code
Component event registration processing
const handlerEvent = async (eventInfo) => { const el = document.getElementById(eventInfo.uuid); const hammer = new Hammer(el); let actionHandler = () => {}; switch (eventInfo.action) { case "toast": actionHandler = () => { const text = eventInfo.text; const time = eventInfo.time; toastTip({ text, time }); }; break; case "jumpLink": actionHandler = () => { const url = eventInfo.url; location.href = url; }; break; case "jumpPage": actionHandler = () => { const urlParams = getUrlParams(); const pageId = eventInfo.targetUuid; const resParams = { ... urlParams, pageId }; const url = urlWithObj(`${location.origin}/view`, resParams); location.href = url; }; break; case "runCode": actionHandler = () => { const code = eventInfo.jsCode; runJsCode(code); }; break; } switch (eventInfo.trigger) { case "click": el.addEventListener("click", actionHandler); break; case "load": actionHandler(); break; case "longPress": hammer.on("press", () => { actionHandler(); }); break; }}; const handlerEventData = (curPageData) => { const elements = curPageData.elements; for (const { events, uuid } of elements) { if (events && events.length) { for (const { trigger, action, configMap } of events) { const filterInfo = { trigger, action, uuid }; for (const k in configMap) { if (! configMap[k].when || configMap[k].when(action)) { filterInfo[configMap[k].key] = configMap[k].value; } } handlerEvent(filterInfo); }}}}; export default handlerEventData;Copy the code
Component data processing workshop
@param {*} activityData * @returns */ export function setConfigMap(activityData) {if (! activityData || ! activityData.pages || ! activityData.pages.length) return activityData; ActivityData. Pages. ForEach (I = > {appropriate precautions lements && appropriate precautions lements. Length && appropriate precautions lements. ForEach (j = > {/ / component based configuration processing const midMap = deepClone(configList[j.configCode].configMap); for (const k in j.configInfo) { midMap[k].value = j.configInfo[k]; } j.configMap = midMap; j.configInfo && delete j.configInfo; // Component style configuration handles const styleMap = {}; for (const k in j.styleInfo) { const curMap = deepClone(commonStyleConfigMap[k]); styleMap[k] = curMap; styleMap[k].value = j.styleInfo[k]; } j.styleMap = styleMap; // j.styleInfo && delete j.styleInfo; ForEach (x => {const midXMap = deepClone(eventMap); for (const k in x.configInfo) { midXMap[k].value = x.configInfo[k]; } x.configMap = midXMap; x.configInfo && delete x.configInfo; }); }); }); return activityData; } /** * @description; * @param {*} activityData * @returns */ export function removeConfigMap(activityData) {if (! activityData || ! activityData.pages || ! activityData.pages.length) return activityData; ActivityData. Pages. ForEach (I = > {appropriate precautions lements. ForEach (j = > {/ / component based configuration processing const midInfo = {}; for (const k in j.configMap) { midInfo[k] = j.configMap[k].value; } j.configInfo = midInfo; j.configMap && delete j.configMap; // const styleInfo = {}; // for (const k in j.styleMap) { // styleInfo[k] = j.styleMap[k].value; // } // j.styleInfo = styleInfo; j.styleMap && delete j.styleMap; // Component event configuration handles j.vents &&j.vents. ForEach (x => {const midXInfo = {}; for (const k in x.configMap) { midXInfo[k] = x.configMap[k].value; } x.configInfo = midXInfo; x.configMap && delete x.configMap; }); }); }); return activityData; }Copy the code
Shortcut key operation

Global monitor onKeyDown event to achieve the activity save, component delete copy and paste functions encounter a pit is, after global monitor will make the input box itself copy and paste and so on and here occur confusion, solution, custom instructions with status identification processing

document.onkeydown = e => { if (this.disableKeycode) return; const hasCtrl = e.metaKey || e.ctrlKey; switch (e.code) { case "Backspace": e.preventDefault(); this.deleteElement(); break; case "KeyS": e.preventDefault(); hasCtrl && this.$emit("saveActivity"); break; case "KeyC": e.preventDefault(); hasCtrl && this.copyElement(); break; case "KeyV": e.preventDefault(); hasCtrl && this.pasteElement(); break; }}; Vue.directive("setDisableKeycode", { inserted: function(elPar) { const elInput = elPar.getElementsByTagName("input") && elPar.getElementsByTagName("input")[0]; const elTextarea = elPar.getElementsByTagName("textarea") && elPar.getElementsByTagName("textarea")[0]; const el = elInput || elTextarea; el.onfocus = () => { store.dispatch("setDisableKeycode", true); }; el.onblur = () => { store.dispatch("setDisableKeycode", false); }; }});Copy the code

8. Component library design

With reference to elementUI design, on-demand import, full import and CDN import can be supported

| - plugins | - Button | -- config. Js - component configuration | -- index. Js - a single set of pieces of export | -- index. Vue - component instance | - Image | -- index. Js - Index.js >> import Text from "./Text/index.vue"; import Button from "./Button/index.vue"; import Image from "./Image/index.vue"; import Video from "./Video/index.vue"; import Iframe from "./Iframe/index.vue"; const components = [ Text, Button, Image, Video, Iframe ]; const install = function(Vue) { if (install.installed) return; install.installed = true; components.map(component => Vue.component(component.name, component)); }; If (window && window.vue) {// install(window.vue); } export default { install, Text, Button, Image, Video, Iframe };Copy the code

Button component example config.js

Export default {name: "NjButton", // component name desc: "button ", icon: "icon-anniu", animate: "", // configMap: {// configText: {key: "btnText", value: "button ", valueType: "String ", formType: "input", placeholder: "input", tip: "input", label: "input"}}, styleInfo: {// style "display": "block", "position": "relative", "z-index": "0", "text-align": "center", "line-height": "40px", "background-color": "#409eff", "border-top-left-radius": "40px", "border-top-right-radius": "40px", "border-bottom-left-radius": "40px", "border-bottom-right-radius": "40px", "color": "#ffffff", "font-size": "16px", "width": "300px", "height": "40px", "margin-left": "auto", "margin-right": "auto", "margin-top": "0", "margin-bottom": "0", "border-width": "0px", "border-style": "solid", "border-color": "#999" } };Copy the code

index.js

import Component from "./index";
Component.install = Vue => {
    Vue.component(Component.name, Component);
};
export default Component;
Copy the code

index.vue

<template> <div class="nj-btn">{{ btnText }}</div> </template> <script> export default { name: "NjButton", props: { item: { type: Object, default: () => ({}) } }, computed: { btnText() { return this.item.configMap.btnText.value; }}}; </script> <style lang="scss" scoped> .nj-btn{ font-size: inherit; color: inherit; font-weight: inherit; } </style>Copy the code

9. Back-end services

For example, with the increasing number of active pages and page components, THE JSON string will be infinitely larger until the field storage is exhausted. Optimization direction and active page splitting will reduce the field storage pressure in the way of table association

|----app
    |----controller
    |----model
    |----router
    |----service
|----config
    |----config.default.js
    |----plugin.js
        - egg-cors
        - egg-sequelize
|----sh
    |----deploy.sh
Copy the code

Completion of 10,

So far, a basic visual configuration platform has been completed, some simple page can this configuration out, of course, the improvement of the component library expansion is the platform to the fundamental The present era is not necessarily a front end of the era, but this era has created the current front to salute each front-end coder; The new one;