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

javascript 复制代码
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

javascript 复制代码
<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配置项,以单行文本框为例

javascript 复制代码
{
    // 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上的属性不会让我们再看下生成后的表单项(不用细看)

javascript 复制代码
{
  "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
}

请注意这几个属性

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

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

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

从上图我们知道,

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

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

RightPanel.vue 这个组件是用来操配置项的属性的
  • activeData 标识当前选择的 配置项
  • 可以通过v-model绑定例如
javascript 复制代码
<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 这个组件是用来显示组件操作值的
javascript 复制代码
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操作值

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

相关推荐
m0_748256145 分钟前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
IT古董24 分钟前
【机器学习】机器学习的基本分类-半监督学习(Semi-supervised Learning)
学习·机器学习·分类·半监督学习
jbjhzstsl43 分钟前
lv_ffmpeg学习及播放rtsp
学习·ffmpeg
青い月の魔女1 小时前
数据结构初阶---二叉树
c语言·数据结构·笔记·学习·算法
网络安全(king)1 小时前
网络安全攻防学习平台 - 基础关
网络·学习·web安全
苹果醋31 小时前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
王小王和他的小伙伴2 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
虾球xz2 小时前
游戏引擎学习第59天
学习·游戏引擎
枫零NET2 小时前
学习思考:一日三问(学习篇)之匹配VLAN
网络·学习·交换机
好名字08212 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架