以后主打超融开源社区 (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).defaultcomponentChild[tag] = value
})function vModel(dataObject, defaultValue) {dataObject.props.value = defaultValuedataObject.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%A1return {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` : nullif (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 } = thisreturn (<el-row gutter={formConfCopy.gutter}><el-formsize={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-472013664props={{ 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 nullreturn 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和eventObject.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.defaultValueif (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和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在中间做的是
- 批量产生配置项
- 修改配置项
现在让我们看下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-switchv-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操作值
通过理解数据流向我们就知道我们怎样扩展自己的组件了。下面通过一个案例来感受一下