最近开发正好要用到若依,然后看到表单构建没有Vue3版本,于是就照着Vue2版本来开发了个Vue3版本的表单构建。正在自己构建Vue3版本的朋友可以看看,避避坑。
三言两语还讲不完,因为要完全改成Vue3,里面有很多东西需要改。所以挑重点讲。
前置条件
- vuedraggable -> [email protected] 这个拖拽包跟vuedraggable使用差不多,看看文档就好了。
- vitejs/[email protected] vue3中使用jsx需要这个插件。然后在vite/plugins/index.js中引入
- vue3中的h方法使用
- vue3中使用jsx语法
tool/build/index.vue
这里基本上就是vue2变成vue3,难度不大。不过也有注意点
- 讲vuedraggable 替换为 vue-draggable-next
- style样式里面没有加scope,千万别加scope不然样式不对(这一点坑了好长时间,因为我写的时候习惯性加scope,然后css复制过来有些样式一直没有生效)。
- element plus有些样式类名发生了变更,需要注意
tool/build/DraggableItem.vue
这里基本上和vue2的差不多,但是也要注意几点
- 里面使用的h是从vue中引入的,并不是vue2中render里面的h
- 比较重要的一点,若依代码里面会在子组件修改props。这个用法没有问题 其他就是vue2改成vue3语法
utils/generator/render.jsx
这里应该就是重中之重了,看到vue2使用js后缀结尾,但是我用js结尾会报js中不能使用jsx语法的错误,所以我干脆就使用了jsx语法。 这里先贴代码
javascript
import { makeMap } from '@/utils/index'
import { h, defineComponent, resolveComponent } from 'vue';
// 参考https://github.com/vuejs/vue/blob/v2.6.10/src/platforms/web/server/util.js
const isAttr = makeMap(
'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,'
+ 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,'
+ 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,'
+ 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,'
+ 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,'
+ 'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,'
+ 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,'
+ 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,'
+ 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,'
+ 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,'
+ 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,'
+ 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,'
+ 'target,title,type,usemap,value,width,wrap'
)
// v-model
const changeEventList = ['el-select', 'el-radio-group',
'el-checkbox-group', 'el-time-picker', 'el-date-picker', 'el-rate', 'el-color-picker'];
function isChangeEvent(tag) {
const isTrue = changeEventList.some(item => item === tag);
return isTrue;
}
function vModel(emit, dataObject, confClone) {
// 这里是因为element plus中有些组件必须要设置model-value或v-model
dataObject.attrs['modelValue'] = confClone.defaultValue;
// 这里事件需要加on
if(isChangeEvent(confClone.tag)) {
dataObject.on.onChange = val => {
emit('input', val)
}
}else {
dataObject.on.onInput = val => {
emit('input', val)
}
}
}
const timeSelectList = ['el-time-picker'];
const componentChild = {
'el-button': {
default(h, conf, key) {
return {
default: conf[key]
}
},
},
'el-input': {
prepend(h, conf, key) {
if(!conf[key]) {
return ''
}else {
return {
prepend: conf[key]
}
}
},
append(h, conf, key) {
if(!conf[key]) {
return ''
}else {
return {
append: conf[key]
}
}
}
},
'el-select': {
options(h, conf, key) {
const list = []
conf.options.forEach(item => {
list.push(<el-option label={item.label} value={item.value} disabled={item.disabled}></el-option>)
})
return {
default: list
}
}
},
'el-radio-group': {
options(h, conf, key) {
const list = []
conf.options.forEach(item => {
if (conf.optionType === 'button') list.push(<el-radio-button value={item.value}>{item.label}</el-radio-button>)
else list.push(<el-radio value={item.value} border={conf.border}>{item.label}</el-radio>)
})
return {
default: list
}
}
},
'el-checkbox-group': {
options(h, conf, key) {
const list = []
conf.options.forEach(item => {
if (conf.optionType === 'button') {
list.push(<el-checkbox-button value={item.value}>{item.label}</el-checkbox-button>)
} else {
list.push(<el-checkbox value={item.value} border={conf.border} label={item.label} />)
}
})
return {
default: list
}
}
},
'el-upload': {
'list-type': (h, conf, key) => {
const list = []
if (conf['list-type'] === 'picture-card') {
list.push(<el-icon class="avatar-uploader-icon"><Plus /></el-icon>)
} else {
list.push(<el-button size="default" type="primary"><el-icon><Upload /></el-icon>{conf.buttonText}</el-button>)
}
if (conf.showTip) {
return {
default: list,
tip: [<div class="el-upload__tip">只能上传不超过 {conf.fileSize}{conf.sizeUnit} 的{conf.accept}文件</div>]
}
// list.push(<div slot="tip" class="el-upload__tip">只能上传不超过 {conf.fileSize}{conf.sizeUnit} 的{conf.accept}文件</div>)
}
return {
default: list
}
}
}
}
const isComponentChildKey = (key) => {
const keys = Object.keys(componentChild);
if(Array.isArray(keys)) {
const isExit = keys.some(item => item === key);
return isExit
}
return false
}
function RenderComponent(props, { slots, emit, attrs }) {
const dataObject = {
attrs: {},
props: {},
on: {},
style: {}
}
const confClone = JSON.parse(JSON.stringify(props.conf))
let childrenSlot = null
// 根据this.conf中tag来返回componentChild对应的对象 tag(标签的名称,例如默认的 el-input)
const childObjs = componentChild[confClone.tag]
if (childObjs) {
// componentChild对应项for循环 如默认的 el-input对象的prepend和append方法
//! 当创建一个组件的 vnode 时,子节点必须以插槽函数进行传递。如果组件只有默认槽,
//! 可以使用单个插槽函数进行传递。否则,必须以插槽函数的对象形式来传递。
//! 这里做只要是插槽,统一用对象。
if(isComponentChildKey(confClone.tag)) {
const childrenObj = {}
// 根据tag里面每一项的返回值来
Object.keys(childObjs).forEach(key => {
const childFuncResult = childObjs[key](h, confClone, key);
Object.keys(childFuncResult).forEach(item => {
childrenObj[item] = () => childFuncResult[item];
})
})
childrenSlot = childrenObj;
}else {
const childrenArr = []
Object.keys(childObjs).forEach(key => {
const childFunc = childObjs[key]
// 默认drawingDefault中是没有prepend和append的
if (confClone[key]) {
childrenArr.push(childFunc(h, confClone, key))
}
});
childrenSlot = () => childrenArr
}
}
// 遍历传过来的所有属性 vue3 中可以自动判断是props还是attrs,所以感觉这一步没啥用
Object.keys(confClone).forEach(key => {
// 每一项属性的值
const val = confClone[key]
if (key === 'vModel') {
vModel(emit, dataObject, confClone)
} else if (dataObject[key]) {
dataObject[key === 'label' ? 'aria-label' : key] = val
} else if (!isAttr(key)) {
dataObject.props[key === 'label' ? 'aria-label' : key] = val
} else {
dataObject.attrs[key === 'label' ? 'aria-label' : key] = val
}
})
delete dataObject.props.defaultValue
// 无语了,这个导致多选框柱不能多选
delete dataObject.props.tag
// 父组件onInput事件会自动继承 设置这个就不会继承inheritAttrs: false,
// return h(resolveComponent(props.conf.tag), {...dataObject.attrs, ...dataObject.props, style: dataObject.style, ...dataObject.on}, childrenSlot)
// props暂时去掉看下有没有什么影响(props中存在令多选框柱不能多选)
// 如果是时间选择器就直接用jsx形式返回,因为用h渲染的话会出现滚动不能选择时间的问题
if(props.conf.tag === 'el-time-picker') {
return <el-time-picker vModel={props.conf.defaultValue} {...dataObject.attrs} {...dataObject.props } style={{...dataObject.style}} {...dataObject.on} ></el-time-picker>
}else if(props.conf.tag === 'el-date-picker') {
return <el-date-picker vModel={props.conf.defaultValue} {...dataObject.attrs} {...dataObject.props } style={{...dataObject.style}} {...dataObject.on} ></el-date-picker>
}else {
return h(resolveComponent(props.conf.tag), { ...dataObject.attrs, ...dataObject.props, style: dataObject.style, ...dataObject.on }, childrenSlot)
}
}
RenderComponent.inheritAttrs = false;
export default RenderComponent
从上往下看
- import { h, resolveComponent } from 'vue';
h:是从vue中引入的,用来渲染成为html的js语法 resolveComponent:当我们使用element plus的组件时,如果不直接引入这个组件然后使用就需要加一个resolveComponent.
- changeEventList 大家看到这个list数组,里面是触发change事件的组件,其余是input事件
- vModel 看到这个方法,注意,element plus很少用value了,基本上都使用modelValue。然后判断是change事件还是input事件。注意,这里需要用小驼峰而且需要加上on事件,属于jsx的一种语法吧。
- componentChild 这里比较重要。因为element plus很多都是使用插槽。如果我们学个tempalte的话jsx是无法识别的,而且我们需要结合h()这个方法。故这里有个文档可以看h方法如果写插槽。粗略讲一下就是h的第三个参数如果是插槽的话,我们需要按照如下方式
scss
// 传递单个默认插槽 h(Foo, () => 'default slot')
// 传递具名插槽
// 注意,需要使用 `null` 来避免
// 插槽对象被当作是 prop
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')] })
挑一个 el-input讲
vbnet
'el-input': {
prepend(h, conf, key) {
if(!conf[key]) {
return ''
}else {
return {
prepend: conf[key]
}
}
},
append(h, conf, key) {
if(!conf[key]) {
return ''
}else {
return {
append: conf[key]
}
}
}
el-input可能会有prepend和append这两个插槽。所以我们针对这两个插槽返回对应的插槽格式对象。如{prepend: conf[key]}。然后我们在RenderComponent方法中处理el-input中prepend和append这两个对象的返回值,然后放到一个对象,最后当做h的最后一个参数(插槽)。即这一段代码,主要需要以剪头函数的形式返回插槽内容。
ini
const childrenObj = {}
// 根据tag里面每一项的返回值来
Object.keys(childObjs).forEach(key => {
const childFuncResult = childObjs[key](h, confClone, key);
Object.keys(childFuncResult).forEach(item => {
childrenObj[item] = () => childFuncResult[item];
})
})
childrenSlot = childrenObj;
- delete dataObject.props.defaultValue dataObject中的属性有些会传给组件,但是有些不需要defaultValue或者需要。未了防止可能得报错,我直接给他删了,不让渲染到组件上
- delete dataObject.props.tag 这里超级坑如果不删掉这个tag的话,因为el-checkbox-group中有一个tag属性,默认是box,但是我们这个tag是为了渲染element组件,所以tag为el-checkbox-group。服了,当时没有删除直接作为这个组件的属性,导致多选无法选中。
- props.conf.tag === 'el-time-picker'和props.conf.tag === 'el-date-picker' 这两个判断是因为,使用h去渲染这两个组件会导致时间无法滚动,无法选择。必须使用jsx这种直接写出组件名。而且vModel还必须单独写出来。服了。所以单独判断。这里具体啥原因还没分析出来,有没有大佬知道的???
- RenderComponent.inheritAttrs = false;这个是为了防止父组件中onInput事件继承到组件上,所以我选择去掉他,不然他继承。
综上,如果这个render.jsx文件吃透了的话基本上就没有难点了。这篇文章既是记录我自己开发过程中的问题,同时如果有也在开发这个需求的朋友一个帮助。
当然还有最后导出生成代码的部分还没写上来,这一部分比较简单,基本上都能做出来,等有空了再加上。当然如果有大佬知道为啥使用h方法渲染时间组件会导致时间选择的时候不能滚动的问题,帮帮小弟,感谢!!!