以后主打超融开源社区 (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和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-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操作值
通过理解数据流向我们就知道我们怎样扩展自己的组件了。下面通过一个案例来感受一下