以后主打超融开源社区 (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和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在中间做的是
- 批量产生配置项
- 修改配置项
现在让我们看下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操作值
通过理解数据流向我们就知道我们怎样扩展自己的组件了。下面通过一个案例来感受一下