vue 低代码项目 form-generator 原理解析(超详细)

重新梳理了一下 form-generator 的实现原理,扒了一下源码。虽然项目是 vue2 的,但其实低代码思路是一致的,所以还是有价值的,源码版本使用的 0.2.0,一起来学习一下吧

使用

如上图所示,从左侧组件列表将组件拖入中间,中间可以看到整体表单的效果,同时支持上下拖动、调整顺序;组件的属性可以通过右侧的配置面板修改,例如标签、大小、字段名等

支持出码,点击上方运行后,进入预览页面

左侧为产出的代码,右侧为渲染出的表单效果

这是产出的 JSON DSL,不用细看,贴出来只是为了后面讲源码可以对照

json 复制代码
{
  "fields": [
    {
      "__config__": {
        "layout": "rowFormItem",
        "tagIcon": "row",
        "layoutTree": true,
        "document": "https://element.eleme.cn/#/zh-CN/component/layout#row-attributes",
        "span": 24,
        "formId": 101,
        "renderKey": "1011701267042417",
        "componentName": "row101",
        "children": [
          {
            "__config__": {
              "label": "单行文本",
              "labelWidth": null,
              "showLabel": true,
              "changeTag": true,
              "tag": "el-input",
              "tagIcon": "input",
              "required": true,
              "layout": "colFormItem",
              "span": 24,
              "document": "https://element.eleme.cn/#/zh-CN/component/input",
              "regList": [],
              "formId": 102,
              "renderKey": "1021701267046717"
            },
            "__slot__": {
              "prepend": "",
              "append": ""
            },
            "placeholder": "请输入单行文本",
            "style": {
              "width": "100%"
            },
            "clearable": true,
            "prefix-icon": "",
            "suffix-icon": "",
            "maxlength": null,
            "show-word-limit": false,
            "readonly": false,
            "disabled": false,
            "__vModel__": "field102"
          }
        ]
      },
      "type": "default",
      "justify": "start",
      "align": "top"
    }
  ],
  "formRef": "elForm",
  "formModel": "formData",
  "size": "medium",
  "labelPosition": "right",
  "labelWidth": 100,
  "formRules": "rules",
  "gutter": 15,
  "disabled": false,
  "span": 24,
  "formBtns": true
}

感兴趣地可以自行体验一波 mrhj.gitee.io/form-genera...

思路

低代码的本质无非就是

可视化配置 => JSON DSL => 组件组合 => 生成页面

本次我打算将这个过程拆成两部分来介绍

  1. 出码:JSON DSL => vue 代码 => 生成页面
  2. 配置:可视化配置 => JSON DSL => 生成页面

出码

代码做了简化,仅保留了主流程代码,感兴趣可以自行查阅完整代码

template

js 复制代码
// src\components\generator\html.js
export function makeUpHtml(formConfig, type) {
  const htmlList = [];
  // formConfig 即上面的 json 串
  confGlobal = formConfig;
  /**
   * 每行默认 24 列栅格,可手动修改
   * 这里先判断布局是否都沾满了24个栅格
   * 先将结果缓存起来
   */
  someSpanIsNot24 = formConfig.fields.some(
    (item) => item.__config__.span !== 24
  );
  // 遍历渲染每个组件成html
  formConfig.fields.forEach((el) => {
    // 通过 layout 字段,判断使用哪种布局
    htmlList.push(layouts[el.__config__.layout](el));
  });
  const htmlStr = htmlList.join("\n");
  // 将组件代码放进form标签
  let temp = buildFormTemplate(formConfig, htmlStr, type);
  // dialog 标签包裹代码
  if (type === "dialog") {
    temp = dialogWrapper(temp);
  }
  confGlobal = null;
  return temp;
}

layouts 中声明了两种布局:colFormItem 和 rowFormItem,关系如下:

下面开始扒源码

js 复制代码
// src\components\generator\html.js
const layouts = {
  colFormItem(scheme) {
    /**
     * 从 __config__ 字段获取配置
     * 组装 label、label-width 等属性
     */
    const config = scheme.__config__;
    let labelWidth = "";
    let label = `label="${config.label}"`;
    if (config.labelWidth && config.labelWidth !== confGlobal.labelWidth) {
      labelWidth = `label-width="${config.labelWidth}px"`;
    }
    if (config.showLabel === false) {
      labelWidth = 'label-width="0"';
      label = "";
    }
    const required =
      !ruleTrigger[config.tag] && config.required ? "required" : "";
    /**
     * 这一步就是加载 输入型 或 选择型 组件
     * 组件信息同样存在一个对象中 tags
     */
    const tagDom = tags[config.tag] ? tags[config.tag](scheme) : null;
    /**
     * 这里就能明显看出
     * colFormItem 布局就是直接使用的 el-form-item 组件
     */
    let str = `<el-form-item ${labelWidth} ${label} prop="${scheme.__vModel__}" ${required}>
        ${tagDom}
      </el-form-item>`;
    /**
     * 这里判断当前行是不是占用 24 列栅格
     * 通过 __config__ 下的 span 字段判断
     * 如果不是 24
     * 将在外层再包裹一个 el-col 组件
     */
    str = colWrapper(scheme, str);
    return str;
  },
  rowFormItem(scheme) {
    /**
     * 同样从 __config__ 字段获取配置
     * 组装属性
     */
    const config = scheme.__config__;
    // type 有 default 和 flex 两种布局
    const type = scheme.type === "default" ? "" : `type="${scheme.type}"`;
    const justify =
      scheme.type === "default" ? "" : `justify="${scheme.justify}"`;
    const align = scheme.type === "default" ? "" : `align="${scheme.align}"`;
    const gutter = scheme.gutter ? `:gutter="${scheme.gutter}"` : "";
    /**
     * 与 colFormItem 不同的是
     * rowFormItem 的子组件还是一个布局组件
     * 里面包裹的正是 colFormItem
     */
    const children = config.children.map((el) =>
      layouts[el.__config__.layout](el)
    );
    // rowFormItem 布局使用的是 el-row 组件
    let str = `<el-row ${type} ${justify} ${align} ${gutter}>
      ${children.join("\n")}
    </el-row>`;
    // 同样判断是否需要加 el-col
    str = colWrapper(scheme, str);
    return str;
  },
};

布局的问题就搞定了,接下来就是 输入型组件 和 选择型组件,它们的逻辑在 tags 对象内

源码中定义了 15 种类型的组件,这里我们就只看其中一种就好,原理都大差不差

js 复制代码
// src\components\generator\html.js
const tags = {
    ...
    // 输入框组件
  "el-input": (el) => {
    /**
     * attrBuilder 提前解析并配置好属性值
     * 如:__vModel__ 若存在,则拼接 v-model 字符串
     * `v-model="${confGlobal.formModel}.${el.__vModel__}"`
     * 再赋值给 vModel。
     * 猜测是大部分组件都有这些属性
     * 所以单独提出了个函数
     */
    const { tag, disabled, vModel, clearable, placeholder, width } =
      attrBuilder(el);
    const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : "";
    const showWordLimit = el["show-word-limit"] ? "show-word-limit" : "";
    const readonly = el.readonly ? "readonly" : "";
    const prefixIcon = el["prefix-icon"]
      ? `prefix-icon='${el["prefix-icon"]}'`
      : "";
    const suffixIcon = el["suffix-icon"]
      ? `suffix-icon='${el["suffix-icon"]}'`
      : "";
    const showPassword = el["show-password"] ? "show-password" : "";
    const type = el.type ? `type="${el.type}"` : "";
    const autosize =
      el.autosize && el.autosize.minRows
        ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
        : "";
    /**
     * 这里处理子组件
     * 如 el-select 组件的 el-option
     * el-input 组件的子组件是两个插槽:prepend 与 append
     * `<template slot="append">${slot.append}</template>`
     */
    let child = buildElInputChild(el);
    /**
     * 给子组件换行
     * 因为最终得到的代码不止是用来渲染
     * 还需要展示以及出码,还是要尽量美观(-_-)
     */
    if (child) child = `\n${child}\n`;
    /**
     * 这里 tag 就是 el-input
     * 然后拼接属性、拼接子组件
     */
    return `<${tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${tag}>`;
  },
  ...
};

到这里就完成了 vue 组件中 template 部分的代码

js

接下来是 js 代码的生成

js 复制代码
// src\components\generator\js.js
export function makeUpJs(formConfig, type) {
  // 深拷贝一份 json
  confGlobal = formConfig = deepClone(formConfig);
  const dataList = [];
  const ruleList = [];
  const optionsList = [];
  const propsList = [];
  // mixinMethod 混入通用函数
  /**
   * 如果是生成页面
   * 1. 在提交表单时增加表单验证,即:
   *    this.$refs['${confGlobal.formRef}'].validate(valid => {
   *      if(!valid) return
   *      // TODO 提交表单
   *    })
   * 2. 在重置表单时,重置掉表单字段
   *    this.$refs['${confGlobal.formRef}'].resetFields()
   */
  /**
   * 如果是生成弹窗
   * 1. 监听弹窗唤起:onOpen
   * 2. 监听弹窗关闭:onClose
   *    同时在关闭后重置表单字段
   * 3. 添加关闭事件:close
   *    this.$emit('update:visible', false)
   * 4. 添加确认事件:handelConfirm
   *    同时在确认后校验表单
   */
  const methodList = mixinMethod(type);
  const uploadVarList = [];
  const created = [];
  // 核心部分,下面展开细说
  formConfig.fields.forEach((el) => {
    buildAttributes(
      el,
      dataList,
      ruleList,
      optionsList,
      methodList,
      propsList,
      uploadVarList,
      created
    );
  });
  ...
}
js 复制代码
// src\components\generator\js.js
// 构建组件属性
function buildAttributes(...) {
  const config = scheme.__config__
  const slot = scheme.__slot__
  /**
   * 以 __vModel__ 为键名,defaultValue 为值
   * 以键值对的方式 push 进 dataList
   */
  buildData(scheme, dataList)
  /**
   * 仅处理了必填和自定义正则
   * 1. 必填设置
   *  补充 message 等字段,push 到 ruleList
   * 2. 正则列表
   *  遍历正则列表,使用 pattern 字段,补充 message、trigger
   *  push 到 ruleList
   */
  buildRules(scheme, ruleList)
  // 特殊处理options属性
  if (scheme.options || (slot && slot.options && slot.options.length)) {
    /**
     * 处理静态 options
     * el-cascader 组件
     */
    buildOptions(scheme, optionsList)
    // 处理动态 options
    if (config.dataType === 'dynamic') {
      const model = `${scheme.__vModel__}Options`
      const options = titleCase(model)
      const methodName = `get${options}`
      /**
       * 定义动态获取 options 的函数
       * methodName 就是函数名
       * 内部使用 axios 获取
       */
      buildOptionMethod(methodName, model, methodList, scheme)
      /**
       * 将上一步定义的获取函数 push 入 created 列表
       * created 列表在 vue 组件的 creaded 阶段被调用
       */
      callInCreated(methodName, created)
    }
  }
  // 处理props
  if (scheme.props && scheme.props.props) {
    buildProps(scheme, propsList)
  }
  // 处理 el-upload 的action
  if (scheme.action && config.tag === 'el-upload') {
    // 处理 upload 的 aciton 和 fileList 变量
    uploadVarList.push(
      `${scheme.__vModel__}Action: '${scheme.action}',
      ${scheme.__vModel__}fileList: [],`
    )
    /**
     * 增加文件上传相关函数
     * 1. 若设置了fileSize,增加判断文件大小的函数
     * 2. 若设置了 accept,增加判断文件类型的函数
     */
    methodList.push(buildBeforeUpload(scheme))
    // 非自动上传时,生成手动上传的函数
    if (!scheme['auto-upload']) {
      /**
       * 增加 submitUpload 函数
       * this.$refs['${scheme.__vModel__}'].submit()
       */
      methodList.push(buildSubmitUpload(scheme))
    }
  }
  // 构建子级组件属性
  if (config.children) {
    config.children.forEach(item => {
      buildAttributes(...)
    })
  }
}

解析完成后就得到了几个列表: dataList、ruleList、optionsList、methodList、propsList、uploadVarList、created

最后一步,使用 join 将列表每一项通过换行符连接,拼接为 vue2 Option API 的形式

js 复制代码
// src\components\generator\js.js
export function makeUpJs(formConfig, type) {
  ...
  /**
   * buildexport 拼接整体js
   * 也就是将上一步得到的 data、methods 等信息
   * 以 vue2 option API 的方式拼接
   * export default {
        components: {},
        props: [],
        data () {
            return {
                ${conf.formModel}: {
                    ${data}
                },
                ${conf.formRules}: {
                    ${rules}
                },
                ${uploadVar}
                ${selectOptions}
                ${props}
            }
        },
        computed: {},
        watch: {},
        created () {
            ${created}
        },
        mounted () {},
        methods: {
            ${methods}
        }
    }
   */
  const script = buildexport(
    formConfig,
    type,
    dataList.join("\n"),
    ruleList.join("\n"),
    optionsList.join("\n"),
    uploadVarList.join("\n"),
    propsList.join("\n"),
    methodList.join("\n"),
    created.join("\n")
  );
  confGlobal = null;
  return script;
}

以上就是 js 部分的解析

css

css 部分就简单很多了,因为 form-generator 根本就没想让你通过自定义设置样式,也可能没空做那部分的功能

js 复制代码
// src\components\generator\css.js
/**
 * 仅新增了 el-rate、el-upload
 * 两个组件的自定义样式
 * 通过类名的方式实现
 */
const styles = {
  "el-rate": ".el-rate{display: inline-block; vertical-align: text-top;}",
  "el-upload": ".el-upload__tip{line-height: 1.2;}",
};
function addCss(cssList, el) {
  const css = styles[el.__config__.tag];
  css && cssList.indexOf(css) === -1 && cssList.push(css);
  if (el.__config__.children) {
    el.__config__.children.forEach((el2) => addCss(cssList, el2));
  }
}
export function makeUpCss(conf) {
  const cssList = [];
  conf.fields.forEach((el) => addCss(cssList, el));
  return cssList.join("\n");
}

上面的代码就是 css.js 完整的代码,没啥特别的东西就不解释了

最终的代码会使用 style 标签包裹

如果要预览,会直接插入到 html 文件中;当然了,会插入到根结点之前,element-ui 样式链接之后

如果要出码,也就是将代码下载下来,直接放在 vue 文件中

展示/渲染/导出

到这里就完成了 json DSL 到可用代码的转换,接下来无论是想出码还是直接渲染,都不是啥大问题了。

展示

form-generator 展示代码时用到了第三方库 monaco-editor,感兴趣的可以自行研究一下这个库。

预览

预览本质就是要将转换后的代码渲染出来;

form-generator 这里加了一个 iframe,iframe 加载的是提前写好的 html

  • head 部分提前加载 vue、vue-router、element-ui 的 CDN 文件
  • body 部分声明一个 id 为 previewApp 的 div 结点

这个 previewApp 的根结点并不直接挂载 vue 实例,实际执行预览时,还会往里加入 style 标签包裹的样式(上一步生成的 css 部分的代码)、一个 id 为 app 的 div 结点

接下来就只要将上面生成的代码搞成 vue 实例挂载到 id 为 app 的结点上

js 复制代码
// src\views\preview\main.js
/**
 * attrs: 仅当渲染的是弹窗时用到,添加 width、visible 等属性
 * main:上一步的 js 部分生成的代码,移除了 `export default`
 * html:上一步的 template 部分生成的代码
 */
function newVue(attrs, main, html) {
  main = eval(`(${main})`);
  main.template = `<div>${html}</div>`;
  /**
   * 新建 vue 实例
   * 生成的表单以子组件的形式引入
   */
  new Vue({
    components: {
      child: main,
    },
    data() {
      return {
        visible: true,
      };
    },
    template: `<div><child ${attrs}/></div>`,
  }).$mount("#app");
}

导出 vue 文件

拼接上面得到的代码,组成 vue 文件(由 template、script、style 三部分组成),再自动下载下来。

配置

可以看出来,可视化配置页面主要分为三部分:左侧的组件列表、中间的渲染面板、右侧的配置面板

为了实现可拖拽,引入了第三方库 vuedraggable

组件列表

这一部分没啥好说的,维护一个组件列表数组,通过 draggable 实现可拖拽,值得一说的也就只有 draggable 组件了

html 复制代码
<!-- 组件列表 -->
<!-- src\views\index\Home.vue -->
<div v-for="(item, listIndex) in leftComponents" :key="listIndex">
  <div class="components-title">
    <svg-icon icon-class="component" />
    {{ item.title }}
  </div>
  <!-- 
    这里说一下 group 参数的配置
      name        可跨越拖拽的共同类名
      pull: clone 拖出操作,变成了复制
      put:  false 禁止拖入这里
   -->
  <draggable
    class="components-draggable"
    :list="item.list"
    :group="{ name: 'componentsGroup', pull: 'clone', put: false }"
    :clone="cloneComponent"
    draggable=".components-item"
    :sort="false"
    @end="onEnd"
  >
    <div
      v-for="(element, index) in item.list"
      :key="index"
      class="components-item"
      @click="addComponent(element)"
    >
      <div class="components-body">
        <svg-icon :icon-class="element.__config__.tagIcon" />
        {{ element.__config__.label }}
      </div>
    </div>
  </draggable>
</div>

draggable 组件,同名的 group 之间可以实现拖拽效果。

在 form-generator 中,组件列表的 draggable 的 group 名为 componentsGroup,与中间渲染面板的 draggable 的 group 名称一致;所以,组件列表与渲染面板之间可以进行拖拽操作。

html 复制代码
<!-- 渲染面板使用的 draggable -->
<!-- src\views\index\Home.vue -->
...
<draggable
  class="drawing-board"
  :list="drawingList"
  :animation="340"
  group="componentsGroup"
>
  ...
</draggable>
...

drawingList 是渲染面板用到的组件列表

当组件列表中的组件拖拽到中间渲染面板,渲染面板中绑定的 drawingList 变量将会改变

  1. 从 item.list 中拿到拖拽出来的元素
  2. 经过 cloneComponent 拷贝一个新元素
  3. 将新元素放入 drawingList 中

上述的三个步骤都是 draggable 组件内部完成的,也就是 draggable 实现了跨组件的拖拽,并且自己完成了两个 draggable 组件间的通信(自动更新 drawingList),还直接更新了父组件的 drawingList 变量

跨组件

这里不打算深究,直接讲原理 (懒)

vuedraggable 组件本身是基于 sortablejs 实现的(这俩项目是同一个大佬的手笔)

在 vuedraggable 组件的 mounted 阶段,新建了一个 sortable 实例

js 复制代码
// Vue.Draggable-2.23.2\src\vuedraggable.js
mounted() {
  ...
  this._sortable = new Sortable(this.rootContainer, options);
  this.computeIndexes();
}

让我们把视线再转向 sortablejs

sortablejs 内部维护了一个数组 sortables

js 复制代码
// Sortable\src\Sortable.js
function Sortable(el, options) {
  ...
  /**
   * 创建实例时
   * 将 html 元素存入 sortables
   * 这里的 el 我们可以简单理解为 draggable 组件
   */
  sortables.push(this.el);
  ...
}

每新建一个 Sortable 实例,sortables 中就会多一个实例

在拖拽过程中以及完成时,sortablejs 会去查找可拖入的元素(组件),怎么找呢?

就是通过 sortables

js 复制代码
_detectNearestEmptySortable = function(x, y) {
  let ret;
  // 遍历组件们
  sortables.some((sortable) => {
    ...
    /**
     * 获取 draggable 组件的位置信息
     * 以此确定拖入的地方在不在可拖入的范围内
     */
    const rect = getRect(sortable),
      insideHorizontally =
        x >= rect.left - threshold && x <= rect.right + threshold,
      insideVertically =
        y >= rect.top - threshold && y <= rect.bottom + threshold;
    if (insideHorizontally && insideVertically) {
      // 在范围内,直接返回组件
      return (ret = sortable);
    }
  });
  return ret;
}

之后的事就简单了,通知 vuedraggable,该更新更新

更新父组件变量

你们以为 vuedraggable 会使用 $emit $ref 吗?

No~ No~ No~

从上面的使用中也能看到,list 属性并没有加 sync

我直接贴段源码

js 复制代码
// Vue.Draggable-2.23.2\src\vuedraggable.js
alterList(onList) {
  if (this.list) {
    onList(this.list);
    return;
  }
  const newList = [...this.value];
  onList(newList);
  /**
   * 用的 list 属性,没有 v-model
   * 忽略这行
   */
  this.$emit("input", newList);
},
updatePosition(oldIndex, newIndex) {
  const updatePosition = list =>
      list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
  this.alterList(updatePosition);
},

看到了吗?就是简单粗暴的 splice,然后?然后就没了

渲染展示

html 复制代码
<!-- src\views\index\Home.vue -->
<!-- 页面滚动,增加 scrollbar -->
<el-scrollbar class="center-scrollbar">
  <!-- 布局组件,美观 -->
  <el-row class="center-board-row" :gutter="formConf.gutter">
    <!-- 直接写好 el-form -->
    <el-form
      :size="formConf.size"
      :label-position="formConf.labelPosition"
      :disabled="formConf.disabled"
      :label-width="formConf.labelWidth + 'px'"
    >
      <!-- 老熟人了,draggable -->
      <draggable
        class="drawing-board"
        :list="drawingList"
        :animation="340"
        group="componentsGroup"
      >
        <!-- 自定义组件,一会儿展开细嗦 -->
        <draggable-item
          v-for="(item, index) in drawingList"
          :key="item.renderKey"
          :drawing-list="drawingList"
          :current-item="item"
          :index="index"
          :active-id="activeId"
          :form-conf="formConf"
          @activeItem="activeFormItem"
          @copyItem="drawingItemCopy"
          @deleteItem="drawingItemDelete"
        />
      </draggable>
      <div v-show="!drawingList.length" class="empty-info">
        从左侧拖入或点选组件进行表单设计
      </div>
    </el-form>
  </el-row>
</el-scrollbar>

这里主要的重点在于 draggable-item 组件的实现

draggable-item 组件仅有 scripts 部分,它没有使用 template,而是使用了 vue 的 render 函数替代 template

html 复制代码
<!-- src\views\index\DraggableItem.vue -->
<script>
  ...
  export default {
    components: {
      render,
      draggable
    },
    ...
    render(h) {
      const layout = layouts[this.currentItem.__config__.layout]
      if (layout) {
        return layout.call(this, h, this.currentItem, this.index, this.drawingList)
      }
      return layoutIsNotFound.call(this)
    }
  }
</script>

布局这里就不细讲了,与前面渲染的区别就是渲染时返回的是字符串,而这里直接返回的是 JSX;同时,在外层多加了几个按钮(复制、删除)

js 复制代码
// src\views\index\DraggableItem.vue
const layouts = {
  colFormItem(h, currentItem, index, list) {
    ...
    return (
      <el-col>
        <el-form-item>
          <render
            key={config.renderKey}
            conf={currentItem}
            onInput={(event) => {
              this.$set(config, "defaultValue", event);
            }}
          >
            {child}
          </render>
        </el-form-item>
        {components.itemBtns.apply(this, arguments)}
      </el-col>
    );
  },
  ...
};

很显然,里面有个很突兀的 render 组件,它是渲染输入型组件、选择型组件的关键

js 复制代码
// src\components\render\render.js
export default {
  props: {
    conf: {
      type: Object,
      required: true,
    },
  },
  render(h) {
    /**
     * 先整个默认属性对象
     * 参考链接 4
     */
    const dataObject = makeDataObject();
    // 拷贝一份 json DSL
    const confClone = deepClone(this.conf);
    const children = this.$slots.default || [];
    /**
     * 如果slots文件夹存在与当前tag同名的文件
     * 则执行文件中的代码
     */
    mountSlotFiles.call(this, h, confClone, children);
    // 将字符串类型的事件,发送为消息
    emitEvents.call(this, confClone);
    /**
     * 将 json 表单配置转化为vue render可以识别的 "数据对象"
     * 同样参考链接 4
     */
    buildDataObject.call(this, confClone, dataObject);
    /**
     * 调用 vue 的 h 函数(render)
     */
    return h(this.conf.__config__.tag, dataObject, children);
  },
};

就是调用 vue 的 render 函数,也即代码中的 h 函数,要做的只是将 json DSL 转换为 render 函数能识别的对象

转换也很简单,甚至没有解析,只有二十几行代码

篇幅有限,可自行查阅 vue2 文档关于渲染函数的部分(参考链接 4)

配置面板

这一块内容就真的没啥好说的了,所有配置项都写死在 Rightpanel.vue 组件中,通过 v-if 决定该展示哪个配置项

html 复制代码
<!-- src\views\index\RightPanel.vue -->
<!-- 截取部分 -->
<el-form-item v-if="activeData.__vModel__!==undefined" label="字段名">
  <el-input
    v-model="activeData.__vModel__"
    placeholder="请输入字段名(v-model)"
  />
</el-form-item>
<el-form-item
  v-if="activeData.__config__.componentName!==undefined"
  label="组件名"
>
  {{ activeData.__config__.componentName }}
</el-form-item>
<el-form-item v-if="activeData.__config__.label!==undefined" label="标题">
  <el-input
    v-model="activeData.__config__.label"
    placeholder="请输入标题"
    @input="changeRenderKey"
  />
</el-form-item>

如何与渲染面板通信呢?以下是可视化配置页面引用的 RightPanel 组件代码

html 复制代码
<!-- src\views\index\Home.vue -->
<right-panel
  :active-data="activeData"
  :form-conf="formConf"
  :show-field="!!drawingList.length"
  @tag-change="tagChange"
  @fetch-data="fetchData"
/>

其中 tag-change 用于监听组件类型的切换(输入型组件与选择型组件间地切换)

fetch-data 用于监听 级联组件、表格组件动态化数据的接口配置更改

其余数据通过 activeDataformConf 两个 prop 传递

将 formConf、activeData 对象中的属性直接作为值绑定

html 复制代码
<!-- src\views\index\RightPanel.vue -->
...
<el-form-item label="表单尺寸">
  <el-radio-group v-model="formConf.size">
    <el-radio-button label="medium"> 中等 </el-radio-button>
    <el-radio-button label="small"> 较小 </el-radio-button>
    <el-radio-button label="mini"> 迷你 </el-radio-button>
  </el-radio-group>
</el-form-item>
...

直接修改属性,父级的 formConf、activeData 也会被修改

生成 JSON

JSON DSL 就是 drawingList 与 formConf 的组合

总结

大概是考虑到要做出码的缘故,在 出码 与 配置 所用的页面渲染方式是不一样的

出码预览时使用 new Vue 实例的方式;可视化配置时,中间面板渲染表单时巧妙地使用了 JSX + render 函数的方式。在执行效率上,明显是后者更胜一筹

But,该项目仅适用于 Vue2 + elementUI 的场景。

题外话

vuedraggable 的源码地址是这个:github.com/SortableJS/...

你上 github 搜,排名前面的都是假的,只是使用了 vuedraggable 的同名 demo 项目,居然还能有几百个 star,属实是羡慕了

参考

  1. jakhuang.github.io/form-genera...
  2. github.com/SortableJS/...
  3. github.com/SortableJS/...
  4. v2.cn.vuejs.org/v2/guide/re...
相关推荐
小白学习日记34 分钟前
【复习】HTML常用标签<table>
前端·html
程序员大金38 分钟前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀1 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081352 小时前
前端之路-了解原型和原型链
前端
永远不打烊2 小时前
librtmp 原生API做直播推流
前端
北极小狐2 小时前
浏览器事件处理机制:从硬件中断到事件驱动
前端
道爷我悟了2 小时前
Vue入门-指令学习-v-html
vue.js·学习·html