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


每个组件都对应一个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上面的属性会赋值上而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是如何处理配置项数据的,从右向左看。看不清请放大

Mr_HJ / form-generator项目文档学习与记录(续)_第1张图片

从上图我们知道,
首先通过点击或者拖拽的方式将config.js中的配置项转化成了唯一的表单配置项,实现了批量生产。
在修改配置项时通过两个不同的表单,渲染表单用来展示组件和修改值,编辑表单用来修改属性

RightPanel.vue 这个组件是用来操配置项的属性的
  • activeData 标识当前选择的 配置项
  • 可以通过v-model绑定例如

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操作值
通过理解数据流向我们就知道我们怎样扩展自己的组件了。下面通过一个案例来感受一下
 

你可能感兴趣的:(nbcio-boot,ruoyi-nbcio,前端vue,mr,学习,vue.js)