Mr_HJ / form-generator项目文档学习与记录(续)

本文涉及的产品
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Mr_HJ / form-generator项目文档学习与记录(续)

以后主打超融开源社区 (jiangzhicheng88) - Gitee.com

render.js就是对vue的render函数的自己简单定制封装。

render.js实现的功能是将json表单中的__config__.tag解析为具体的vue组件;

正常开发流程我们组件输入的时候会触发组件内的 this.$emit('getValue', val);

引用组件的父组件需要响应子组件上的@getValue方法,调用自身的getValue方法处理里面的逻辑

转换成代码生成器内的流程就是

编辑器组件输入内容=>触发this.$emit('getValue', val)=> 子组件监听getValue事件=>父组件处理getValue事件setEditorValue

require.context  在组件内引入多个组件

我们可以通过 require.context() 函数来创建自己的 context。

可以给这个函数传入三个参数:

要搜索的目录,

标记表示是否还搜索其子目录,

匹配文件的正则表达式。

webpack 会在构建中解析代码中的 require.context()

不熟悉正则的同学,可以看看下面的解析

正则解析:

/^.*\.(jpg|gif|png|bmp)$/i

1

^: 匹配字符串的开始位置

.*: .匹配任意字符,*匹配数量0到正无穷

\.: 斜杠用来转义,\.匹配.

(jpg|gif|png|bmp): 匹配 jpg 或 gif 或 png 或 bmp

$: 匹配字符串的结束位置

i: 不区分大小写。

合起来就是匹配以 .jpg 或 .GIF 或 … 结尾的任意字符串,不区分大小写

const keys = slotsFiles.keys() || []后结果keys如下:
[
    "./el-button.js",
    "./el-checkbox-group.js",
    "./el-input.js",
    "./el-radio-group.js",
    "./el-select.js",
    "./el-upload.js"
]
render key= ./el-button.js
const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')

相当于把前面./和.js都替换掉了

render tag= el-button

import { deepClone } from '@/utils/index'
const componentChild = {}
/**
 * 将./slots中的文件挂载到对象componentChild上
 * 文件名为key,对应JSON配置中的__config__.tag
 * 文件内容为value,解析JSON配置中的__slot__
 */
const slotsFiles = require.context('./slots', false, /\.js$/)
const keys = slotsFiles.keys() || []
keys.forEach(key => {
  const tag = key.replace(/^\.\/(.*)\.\w+$/, '$1')
  const value = slotsFiles(key).default
  componentChild[tag] = value
})
function vModel(dataObject, defaultValue) {
  dataObject.props.value = defaultValue
  dataObject.on.input = val => {
    this.$emit('input', val)
  }
}
function mountSlotFiles(h, confClone, children) {
  const childObjs = componentChild[confClone.__config__.tag]
  if (childObjs) {
    Object.keys(childObjs).forEach(key => {
      const childFunc = childObjs[key]
      if (confClone.__slot__ && confClone.__slot__[key]) {
        children.push(childFunc(h, confClone, key))
      }
    })
  }
}
function emitEvents(confClone) {
  ['on', 'nativeOn'].forEach(attr => {
    const eventKeyList = Object.keys(confClone[attr] || {})
    eventKeyList.forEach(key => {
      const val = confClone[attr][key]
      if (typeof val === 'string') {
        // 代码编辑器自定义事件注册
        // 将getValue的事件指向我们定义的setEditorValue去
        // confClone['on']['getValue'] = event => this.$emit('setEditorValue', event)
        confClone[attr][key] = event => this.$emit(val, event)
      }
    })
  })
}
function buildDataObject(confClone, dataObject) {
  Object.keys(confClone).forEach(key => {
    const val = confClone[key]
    if (key === '__vModel__') {
      vModel.call(this, dataObject, confClone.__config__.defaultValue)
    } else if (dataObject[key] !== undefined) {
      if (dataObject[key] === null
        || dataObject[key] instanceof RegExp
        || ['boolean', 'string', 'number', 'function'].includes(typeof dataObject[key])) {
        dataObject[key] = val
      } else if (Array.isArray(dataObject[key])) {
        dataObject[key] = [...dataObject[key], ...val]
      } else {
        dataObject[key] = { ...dataObject[key], ...val }
      }
    } else {
      dataObject.attrs[key] = val
    }
  })
  // 清理属性
  clearAttrs(dataObject)
}
function clearAttrs(dataObject) {
  delete dataObject.attrs.__config__
  delete dataObject.attrs.__slot__
  delete dataObject.attrs.__methods__
}
function makeDataObject() {
  // 深入数据对象:
  // https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5%E6%95%B0%E6%8D%AE%E5%AF%B9%E8%B1%A1
  return {
    class: {},
    attrs: {},
    props: {},
    domProps: {},
    nativeOn: {},
    on: {},
    style: {},
    directives: [],
    scopedSlots: {},
    slot: null,
    key: null,
    ref: null,
    refInFor: true
  }
}
export default {
  props: {
    conf: {
      type: Object,
      required: true
    }
  },
  render(h) {
    const dataObject = makeDataObject()
    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可以识别的 “数据对象(dataObject)”
    buildDataObject.call(this, confClone, dataObject)
    return h(this.conf.__config__.tag, dataObject, children)
  }
}

Parser.vue

<script>
import { deepClone } from '@/utils/index'
import render from '@/components/render/render.js'
const ruleTrigger = {
  'el-input': 'blur',
  'el-input-number': 'blur',
  'el-select': 'change',
  'el-radio-group': 'change',
  'el-checkbox-group': 'change',
  'el-cascader': 'change',
  'el-time-picker': 'change',
  'el-date-picker': 'change',
  'el-rate': 'change'
}
const layouts = {
  colFormItem(h, scheme) {
    const config = scheme.__config__
    const listeners = buildListeners.call(this, scheme)
    let labelWidth = config.labelWidth ? `${config.labelWidth}px` : null
    if (config.showLabel === false) labelWidth = '0'
    return (
      <el-col span={config.span}>
        <el-form-item label-width={labelWidth} prop={scheme.__vModel__}
          label={config.showLabel ? config.label : ''}>
          <render conf={scheme} on={listeners} />
        </el-form-item>
      </el-col>
    )
  },
  rowFormItem(h, scheme) {
    let child = renderChildren.apply(this, arguments)
    if (scheme.type === 'flex') {
      child = <el-row type={scheme.type} justify={scheme.justify} align={scheme.align}>
              {child}
            </el-row>
    }
    return (
      <el-col span={scheme.span}>
        <el-row gutter={scheme.gutter}>
          {child}
        </el-row>
      </el-col>
    )
  }
}
function renderFrom(h) {
  const { formConfCopy } = this
  return (
    <el-row gutter={formConfCopy.gutter}>
      <el-form
        size={formConfCopy.size}
        label-position={formConfCopy.labelPosition}
        disabled={formConfCopy.disabled}
        label-width={`${formConfCopy.labelWidth}px`}
        ref={formConfCopy.formRef}
        // model不能直接赋值 https://github.com/vuejs/jsx/issues/49#issuecomment-472013664
        props={{ model: this[formConfCopy.formModel] }}
        rules={this[formConfCopy.formRules]}
      >
        {renderFormItem.call(this, h, formConfCopy.fields)}
        {formConfCopy.formBtns && formBtns.call(this, h)}
      </el-form>
    </el-row>
  )
}
function formBtns(h) {
  return <el-col>
    <el-form-item size="large">
      <el-button type="primary" onClick={this.submitForm}>提交</el-button>
      <el-button onClick={this.resetForm}>重置</el-button>
    </el-form-item>
  </el-col>
}
function renderFormItem(h, elementList) {
  return elementList.map(scheme => {
    const config = scheme.__config__
    const layout = layouts[config.layout]
    if (layout) {
      return layout.call(this, h, scheme)
    }
    throw new Error(`没有与${config.layout}匹配的layout`)
  })
}
function renderChildren(h, scheme) {
  const config = scheme.__config__
  if (!Array.isArray(config.children)) return null
  return renderFormItem.call(this, h, config.children)
}
function setValue(event, config, scheme) {
  this.$set(config, 'defaultValue', event)
  this.$set(this[this.formConf.formModel], scheme.__vModel__, event)
}
function buildListeners(scheme) {
  const config = scheme.__config__
  const methods = this.formConf.__methods__ || {}
  const listeners = {}
  // 给__methods__中的方法绑定this和event
  Object.keys(methods).forEach(key => {
    listeners[key] = event => methods[key].call(this, event)
  })
  // 响应 render.js 中的 vModel $emit('input', val)
  listeners.input = event => setValue.call(this, event, config, scheme)
  return listeners
}
export default {
  components: {
    render
  },
  props: {
    formConf: {
      type: Object,
      required: true
    }
  },
  data() {
    const data = {
      formConfCopy: deepClone(this.formConf),
      [this.formConf.formModel]: {},
      [this.formConf.formRules]: {}
    }
    this.initFormData(data.formConfCopy.fields, data[this.formConf.formModel])
    this.buildRules(data.formConfCopy.fields, data[this.formConf.formRules])
    return data
  },
  methods: {
    initFormData(componentList, formData) {
      componentList.forEach(cur => {
        const config = cur.__config__
        if (cur.__vModel__) formData[cur.__vModel__] = config.defaultValue
        if (config.children) this.initFormData(config.children, formData)
      })
    },
    buildRules(componentList, rules) {
      componentList.forEach(cur => {
        const config = cur.__config__
        if (Array.isArray(config.regList)) {
          if (config.required) {
            const required = { required: config.required, message: cur.placeholder }
            if (Array.isArray(config.defaultValue)) {
              required.type = 'array'
              required.message = `请至少选择一个${config.label}`
            }
            required.message === undefined && (required.message = `${config.label}不能为空`)
            config.regList.push(required)
          }
          rules[cur.__vModel__] = config.regList.map(item => {
            item.pattern && (item.pattern = eval(item.pattern))
            item.trigger = ruleTrigger && ruleTrigger[config.tag]
            return item
          })
        }
        if (config.children) this.buildRules(config.children, rules)
      })
    },
    resetForm() {
      this.formConfCopy = deepClone(this.formConf)
      this.$refs[this.formConf.formRef].resetFields()
    },
    submitForm() {
      this.$refs[this.formConf.formRef].validate(valid => {
        if (!valid) return false
        // 触发sumit事件
        this.$emit('submit', this[this.formConf.formModel])
        return true
      })
    }
  },
  render(h) {
    return renderFrom.call(this, h)
  }
}
</script>

每个组件都对应一个config配置项,以单行文本框为例

{
    // 1. 组件配置信息
    __config__: {
      label: '单行文本',
      labelWidth: null,
      showLabel: true,
      changeTag: true,
      tag: 'el-input',
      tagIcon: 'input',
      defaultValue: undefined,
      required: true,
      layout: 'colFormItem',
      span: 24,
      document: 'https://element.eleme.cn/#/zh-CN/component/input',
      // 正则校验规则
      regList: []
    },
    // 2. 组件的插槽属性
    __slot__: {
      prepend: '',
      append: ''
    },
    // 3. 直接赋值给组件的属性
    placeholder: '请输入',
    style: { width: '100%' },
    clearable: true,
    'prefix-icon': '',
    'suffix-icon': '',
    maxlength: null,
    'show-word-limit': false,
    readonly: false,
    disabled: false
  },

每个表单配置项有三个部分

  1. 组件配置信息
  2. 组件的插槽属性( 没使用这里不讨论 )
  3. 直接赋值给组件的属性

1和3的区别在于3上面的属性会赋值<el-input :readonly="false" :disabled="false">上而1上的属性不会让我们再看下生成后的表单项(不用细看)

{
  "fields": [{
    "__config__": {
      "label": "单行文本",
      "labelWidth": null,
      "showLabel": true,
      "changeTag": true,
      "tag": "el-input",
      "tagIcon": "input",
      "defaultValue": "你好",
      "required": true,
      "layout": "colFormItem",
      "span": 24,
      "document": "https://element.eleme.cn/#/zh-CN/component/input",
      "regList": [],
      "formId": 101,
      "renderKey": "1011693530948107"
    },
    "__slot__": {
      "prepend": "",
      "append": ""
    },
    "placeholder": "请输入单行文本",
    "style": {
      "width": "100%"
    },
    "clearable": true,
    "prefix-icon": "",
    "suffix-icon": "",
    "maxlength": null,
    "show-word-limit": false,
    "readonly": false,
    "disabled": false,
    "__vModel__": "field101"
  }],
  "formRef": "elForm",
  "formModel": "formData",
  "size": "medium",
  "labelPosition": "right",
  "labelWidth": 100,
  "formRules": "rules",
  "gutter": 15,
  "disabled": false,
  "span": 24,
  "formBtns": true
}

请注意这几个属性

{
  "fields": [{
    "__config__": {
      // 双向绑定的值
      "defaultValue": "你好",
    // 绑定到组件上的key
      "renderKey": "1011693530948107"
    },
    // 字段名
    "__vModel__": "field101"
  }]
}
数据流向

通过上面一进一出我们知道了,form-generator在中间做的是

  1. 批量产生配置项
  2. 修改配置项
    现在让我们看下form-generator是如何处理配置项数据的,从右向左看。看不清请放大

从上图我们知道,

首先通过点击或者拖拽的方式将config.js中的配置项转化成了唯一的表单配置项,实现了批量生产。

在修改配置项时通过两个不同的表单,渲染表单用来展示组件和修改值,编辑表单用来修改属性

RightPanel.vue 这个组件是用来操配置项的属性的
  • activeData 标识当前选择的 配置项
  • 可以通过v-model绑定例如
<template v-if="['EditTable'].includes(activeData.__config__.tag)">
  <el-divider>表格属性</el-divider>
  <el-form-item label-width="100px" label="表格尺寸">
    <el-radio-group v-model="activeData.size" size="mini">
      <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>
  <el-form-item label-width="100px" label="纵向边框">
    <el-switch
      v-model="activeData.border" size="small"
    />
  </el-form-item>
</template>
render.js  这个组件是用来显示组件操作值的
export default {
  props: {
    conf: {
      type: Object,
      required: true
    }
  },
  components: {
    EditTable
  },
  mounted() {
    // 动态请求数据
    catchData.call(this, this.conf)
  },
  render(h) {
    const dataObject = makeDataObject()
    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可以识别的 “数据对象(dataObject)”
    buildDataObject.call(this, confClone, dataObject)
    return h(this.conf.__config__.tag, dataObject, children)
  }
}

我们可以看到render.js是一个vue组件,不过不是vue文件而是通过render函数和h函数来返回虚拟DOM

h函数的具体可以看渲染函数,简单理解就是h( 标签名,标签属性,子元素 )

使用h函数根据__config__.tag返回特定的组件

标签属性就是绑定了诸如 style、attribute、on、slot等信息的对象。这里我们主要注意on上面会绑定一个input事件我们就是通过它来更新数据的。

数据流向总结

通过config.js设置配置信息

通过defaultValue和@input进行绑定值

通过RightPanel操作值

通过理解数据流向我们就知道我们怎样扩展自己的组件了。下面通过一个案例来感受一下


相关文章
|
11天前
|
机器学习/深度学习 算法 数据挖掘
【博士每天一篇文论文-算法】A small-world topology enhances the echo state property and signal propagationlun
本文研究了小世界拓扑结构在回声状态网络(ESN)中的作用,发现具有层级和模块化组织的神经网络展现出高聚类系数和小世界特性,这有助于提高学习性能和促进信号传播,为理解神经信息处理和构建高效循环神经网络提供了新的视角。
17 0
【博士每天一篇文论文-算法】A small-world topology enhances the echo state property and signal propagationlun
|
3月前
|
移动开发 JavaScript 前端开发
Mr_HJ / form-generator项目文档学习与记录(续1)
Mr_HJ / form-generator项目文档学习与记录(续1)
26 2
|
3月前
|
移动开发 前端开发
Mr_HJ / form-generator项目学习-增加自定义的超融组件(二)
Mr_HJ / form-generator项目学习-增加自定义的超融组件(二)
43 3
|
3月前
|
JSON 移动开发 前端开发
Mr_HJ / form-generator项目学习-增加自定义的超融组件(一)
Mr_HJ / form-generator项目学习-增加自定义的超融组件(一)
32 3
|
3月前
|
移动开发 前端开发
Mr_HJ / form-generator项目文档学习与记录(续2)
Mr_HJ / form-generator项目文档学习与记录(续2)
19 0
|
3月前
|
JSON JavaScript 前端开发
Mr_HJ / form-generator项目文档学习与记录
Mr_HJ / form-generator项目文档学习与记录
30 0
|
3月前
|
XML Java API
poi-tl——Word模板生成器
poi-tl——Word模板生成器
|
3月前
|
存储 分布式计算 Apache
Spark编程范例:Word Count示例解析
Spark编程范例:Word Count示例解析
|
10月前
|
前端开发 JavaScript Android开发
02HUI - 部署及文档结构
02HUI - 部署及文档结构
42 0
|
机器学习/深度学习 算法 PyTorch
【菜菜的CV进阶之路-Pytorch基础-model.eval】同一个模型测试:shuffle=False和shuffle=True 结果差异很大
【菜菜的CV进阶之路-Pytorch基础-model.eval】同一个模型测试:shuffle=False和shuffle=True 结果差异很大
241 0
【菜菜的CV进阶之路-Pytorch基础-model.eval】同一个模型测试:shuffle=False和shuffle=True 结果差异很大