Vue进阶 - 终极版二次封装组件(属性 事件 方法)

199 阅读2分钟

1.封装组件

1.属性/事件/插槽的透传

  • 常规做法

   // Comp.vue
   <Comp v-bind="$attrs">
       <template v-for="(_,slot) in $slots" #[slot]="slotProps">
           <slot :name="slot" v-bind="slotProps"></slot>
       </template>
   </Comp>
  • js实现

<Comp/>
// script
import {h,useAttrs,useSlots} from 'vue'
const Comp = ()=>h(Comp,useAttrs(),useSlots())
通过函数使组件透传props和slots,弊端是会缺失双向绑定(没有透传事件)

  • 终极版

<template>
    <component :is="h(Comp,{...$Attrs,...props},$slots)"/>  
    // ===  <el-input :ref="getInstance" v-bind="mergeProps($attrs, props)"></el-input>
    // mergeProps(vue提供,和解构一样效果) el-input这样写无法透传插槽 
</template>
import  {type InputProps } from "element-plus"
const props = defineProps<Partial<InputProps>>({})
// Partial如果不写,父组件编辑器会飘红,因为很多必传参数,Partial将所有参数变为?可选

2. 暴露子组件的方法给父组件

  • 常规做法

  // 使用 ref给子组件绑定,获取实例,再通过defineExpose手动抛出
  比较简单,但是弊端很大,一旦子组件被v-if销毁父组件调用会报错。并且会造成内存泄漏,
  • 借助代理

 defineExpose(new Proxy({
     get(target,key){
         return ()=> inputRef.value?.[key]()
     },
     // proxy的方法外面如果调in会走这里 返回布尔值
     // 调用has是因为Proxy代理的是空对象,如果不加has vue不知道能访问到这个key会报错
     has(target,key){
         return key in inputRef.value
     }
 }))
 <el-input ref="inputRef"/>
  • vue源码思维vm实现

  const vm = getCurrentInstance()
  // ref可以传入一个函数,参数即为解包后的组件实例,给默认值为{}是因为组件可能会v-if卸载
  const changeRef = (value)=>{
      vm.expose = value || {}
  }
  <el-input :ref="changeRef">
  

3.子组件获取父组件传入的插槽中的ref

思路:

  // 父组件
  <Comp>
     <template #default="slotProps">
          <div :ref="slotProps">此处也可以是组件</div>
    </template>
  </Comp>
  
  // 子组件
  <slot :slotProps="slotProps"></slot>
  const slotProps = (instance)=>{
      instance.value 即为插槽的实例
  }

4.插槽的实现原理

子组件负责定义插槽,作用域插槽传值。最终会渲染成函数,调用函数传参数对象,父组件 声明函数接收参数。

 // 子组件 slotComp
 <slot name="header" title="title">最终会被渲染成 slot.header({title})
 useSlots() 或者setup的第二个参数可以获取到三个插槽
 // 函数式组件 (最终编译)
 export const helloSlot:FunctionalComponent<foo:Function> =
 (props,{slots,attrs})=>{
     return h('div',null,[
         props.foo() // 父组件也可以传递props为一个函数返回虚拟节点放入插槽中渲染
         slots.header({title:'title'}),
         slots.default?slots.default():'默认内容',
         slots.footer()
     ])
 }
 
 // 父组件 
 <slotComp>
     <template #header="{title}">
         <h1>{{title}}</h1>
     <template>
 </slotComp>
 
 // 函数式组件
 const Comp = h(slotComp,{
     foo(){
         return h('div',null,'foo')
     }
 },{
     header:({title})=> h('h1',null,title)  //返回一个vnode
 })

2.封装权限组件

指令销毁只销毁了dom,但是js全部会执行,dom虽然销毁了,但是创建的对象还在,引用还在,ref暴露的方法依然会被外部引用执行。 比如

比如列表上加了v-auth,那列表的dom还在, v-if是编译时,自己写的指令是运行时。所以v-auth做不到v-if的效果

  • v-auth指令

const a = document.createElement('div') document.body.appendChild(a) document.body.remoreChild(a) // 虽然把a dom移除了但是创建的a还在 所以创建一个 v-auth函数式组件,使用插槽 控制slot显示。

  • 插槽实现

思路:定义一个组件,父组件使用V-Auth如果有权限就渲染 默认插槽,否则返回空节点
const haveAuth = isAdmin
if(!haveAuth) return ()=>null
 const VAuth = (props,{slots})=>{
     return  ()=> haveAuth ? slots.default() :null
 }