若依表单构建--Vue3避坑版

最近开发正好要用到若依,然后看到表单构建没有Vue3版本,于是就照着Vue2版本来开发了个Vue3版本的表单构建。正在自己构建Vue3版本的朋友可以看看,避避坑

三言两语还讲不完,因为要完全改成Vue3,里面有很多东西需要改。所以挑重点讲。

前置条件

  1. vuedraggable -> [email protected] 这个拖拽包跟vuedraggable使用差不多,看看文档就好了。
  2. vitejs/[email protected] vue3中使用jsx需要这个插件。然后在vite/plugins/index.js中引入
  1. vue3中的h方法使用
  2. vue3中使用jsx语法

tool/build/index.vue

这里基本上就是vue2变成vue3,难度不大。不过也有注意点

  1. 讲vuedraggable 替换为 vue-draggable-next
  2. style样式里面没有加scope,千万别加scope不然样式不对(这一点坑了好长时间,因为我写的时候习惯性加scope,然后css复制过来有些样式一直没有生效)。
  3. element plus有些样式类名发生了变更,需要注意

tool/build/DraggableItem.vue

这里基本上和vue2的差不多,但是也要注意几点

  1. 里面使用的h是从vue中引入的,并不是vue2中render里面的h
  2. 比较重要的一点,若依代码里面会在子组件修改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

从上往下看

  1. import { h, resolveComponent } from 'vue';

h:是从vue中引入的,用来渲染成为html的js语法 resolveComponent:当我们使用element plus的组件时,如果不直接引入这个组件然后使用就需要加一个resolveComponent.

  1. changeEventList 大家看到这个list数组,里面是触发change事件的组件,其余是input事件
  2. vModel 看到这个方法,注意,element plus很少用value了,基本上都使用modelValue。然后判断是change事件还是input事件。注意,这里需要用小驼峰而且需要加上on事件,属于jsx的一种语法吧。
  3. 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;
  1. delete dataObject.props.defaultValue dataObject中的属性有些会传给组件,但是有些不需要defaultValue或者需要。未了防止可能得报错,我直接给他删了,不让渲染到组件上
  2. delete dataObject.props.tag 这里超级坑如果不删掉这个tag的话,因为el-checkbox-group中有一个tag属性,默认是box,但是我们这个tag是为了渲染element组件,所以tag为el-checkbox-group。服了,当时没有删除直接作为这个组件的属性,导致多选无法选中。
  3. props.conf.tag === 'el-time-picker'和props.conf.tag === 'el-date-picker' 这两个判断是因为,使用h去渲染这两个组件会导致时间无法滚动,无法选择。必须使用jsx这种直接写出组件名。而且vModel还必须单独写出来。服了。所以单独判断。这里具体啥原因还没分析出来,有没有大佬知道的???
  4. RenderComponent.inheritAttrs = false;这个是为了防止父组件中onInput事件继承到组件上,所以我选择去掉他,不然他继承。

综上,如果这个render.jsx文件吃透了的话基本上就没有难点了。这篇文章既是记录我自己开发过程中的问题,同时如果有也在开发这个需求的朋友一个帮助。

当然还有最后导出生成代码的部分还没写上来,这一部分比较简单,基本上都能做出来,等有空了再加上。当然如果有大佬知道为啥使用h方法渲染时间组件会导致时间选择的时候不能滚动的问题,帮帮小弟,感谢!!!

相关推荐
江城开朗的豌豆14 分钟前
JavaScript篇:typeof 的魔法:原来你是这样判断类型的!
前端·javascript·面试
江城开朗的豌豆17 分钟前
JavaScript篇:数组扁平化:从‘千层饼’到‘一马平川’的六种神操作 🥞→📜
前端·javascript·面试
当归10242 小时前
Fuse.js:打造极致模糊搜索体验
开发语言·javascript·ecmascript
難釋懷3 小时前
Vue-Todo-list 案例
前端·vue.js·list
前端达人3 小时前
React 播客专栏 Vol.18|React 第二阶段复习 · 样式与 Hooks 全面整合
前端·javascript·react.js·前端框架·ecmascript
GISer_Jing3 小时前
Monorepo 详解:现代前端工程的架构革命
前端·javascript·架构
比特森林探险记4 小时前
Go Gin框架深度解析:高性能Web开发实践
前端·golang·gin
前端百草阁6 小时前
JavaScript 模块系统:CJS/AMD/UMD/ESM
javascript·ecmascript
打小就很皮...6 小时前
简单实现Ajax基础应用
前端·javascript·ajax
wanhengidc7 小时前
服务器租用:高防CDN和加速CDN的区别
运维·服务器·前端