首页 > 其他分享 >终究还是太全面了——Vue二次封装组件和组件库

终究还是太全面了——Vue二次封装组件和组件库

时间:2024-08-31 13:54:19浏览次数:12  
标签:Vue const ref props 组件 封装 modelValue emit

目录

项目亮点技能: Vue 二次封装组件的技巧及要点

在开发 Vue 项目中我们一般使用第三方 UI 组件库进行开发,如 Element-Plus, 但是这些组件库提供的组件并不一定满足我们的需求,这时我们可以通过对组件库的组件进行二次封装,来满足我们特殊的需求。

对于封装组件有一个大原则就是我们应该尽量保持原有组件的接口,除了我们需要封装的功能外,我们不应该改变原有组件的接口,即保持原有组件提供的接口(属性、方法、事件、插槽)不变。

一、保持原有组件的接口

这里我们对 Element-plus 的 input 组件进行简单的二次封装,封装一个 MyInput 组件,代码的结构如下:

// 引入组件进行使用
  <template>
  <MyInput></MyInput>
  </template>


  // MyInput.vue
  <template>
  <div class="my-input">
  <el-input></el-input>
  </div>
  </template>

1. 继承第三方组件的 Attributes 属性

如果我们往 MyInput 组件传入一些属性,并且想要将这些属性传给 el-input,最简单的方式就是在组件中一个个的去定义 props,然后再传给 el-input,但是这种方法非常麻烦,毕竟 el-input 就有二十几个属性(Attributes

这个时候可以使用 $attrs(属性透传)去解决这个问题,先来看下 Vue 官方文档对 $attrs 的解释:包含了父作用域中不作为组件 props 或自定义事件的 attribute 绑定和事件;当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind=“$attrs” 传入内部的 UI 组件中——这在创建高阶的组件时会非常有用。

<MyInput :size="inputSize" :name="userName" :clearable="clearable" ></MyInput>
<template>
    <div class="my-input">
      <el-input v-bind="filteredAttrs"></el-input>

      <!-- 如果不希望过滤掉某些属性 可以直接使用 $attrs -->
      <el-input v-bind="$attrs"></el-input>
    </div>
  </template>

    <script lang="ts" setup>
    import {useAttrs,computed,ref } from 'vue'
    import { ElInput } from 'element-plus'
    defineOptions({
    name: 'MyInput'
    })

    // 接收 name,其余属性都会被透传给 el-input
    defineProps({
    name: String
    });

    // 如果我们不希望透传某些属性比如class, 我们可以通过useAttrs来实现
    const attrs = useAttrs()
    const filteredAttrs = computed(() => {
      return { ...attrs, class: undefined };
    });

对于 props,最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值,此外还可以通过 typevalidator 等方式对输入进行验证

const props = {
  viewport: {
    type: (typeof window !== 'undefined' ? window.HTMLElement : Object) as PropType<HTMLElement>,
    default: () => null,
  },
  threshold: { 
    type: String, 
    default: '0px' 
  },
  direction: {
    type: String,
    default: 'vertical',
    validator: (v) => ['vertical', 'horizontal'].includes(v),
  },
};

这里我们再来聊下 inheritAttrs 属性:默认情况下,父组件传递的,但没有被子组件解析为 props 的 attributes 绑定会被 “透传”。这意味着当我们有一个单根节点的子组件时,这些绑定会被作为一个常规的 HTML attribute 应用在子组件的根节点元素上,当你编写的组件想要在一个目标元素或其他组件外面包一层时,可能并不期望这样的行为。

我们可以通过设置 inheritAttrsfalse 来禁用这个默认行为。这些 attributes 可以通过 $attrs 这个实例属性来访问,并且可以通过 v-bind 来显式绑定在一个非根节点的元素上。 下面来看一个具体的例子:

父组件:

<template>
  <div>
    <TestCom title="父组件给的标题" aa="我是aa" bb="我是bb"></TestCom>
  </div>
</template>
<script setup lang="ts">
  import TestCom from "../../components/TestCom.vue"
</script>

子组件:

<template>
    <div class="root-son">
      <p>我是p标签</p>
      <span>我是span</span>
    </div>
  </template>

因为在默认情况下,父组件的属性会直接渲染在子组件的根节点上,但是有些情况我们希望是渲染在指定的节点上,那怎么处理这问题呢?使用 $attrsinheritAttrs: false 就可以完美的解决这个问题。

template>
    <div class="root-son">
      <p v-bind="$attrs">我是p标签</p>
      <span>我是span</span>
    </div>
  </template>
    <script lang="ts">
    export default {
    inheritAttrs: false,
    }
  </script>

2. 继承第三方组件的 Event 事件

跟上面的属性传递一样,如果我们往 MyInput 组件传入一些事件,并且想要将这些事件传给 el-input,这里需要用到 $listeners

<MyInput @change="change" @focus="focus" @input="input"></MyInput>
// Vue2
  <template>
    <div class="my-input">
    <el-input v-bind="$attrs" v-on="$listeners"></el-input>
    </div>
    </template>

    // Vue3
    <template>
    <div class="my-input"> 
    <!-- 在 Vue3 中,取消了$listeners这个组件实例的属性,将其事件的监听都整合到了$attrs上 -->



    <!-- 因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传 -->




    <el-input v-bind="$attrs"></el-input>    

    </div>    
    </template>    

3. 使用第三方组件的 Slots

插槽也是一样的道理,比如 el-input 就有4个 Slot,我们不应该在组件中一个个的去手动添加 <slot name="prefix">,因此需要使用 $slots

<template>
    <MyInput :placeholder="inputPlaceholder" @input="inputHandle">
      <template #prepend>
        <el-select v-model="select" placeholder="请选择" style="width: 115px">
          <el-option label="HTTPS" value="1" />
          <el-option label="HTTP" value="2" />
        </el-select>
      </template>
      <template #append>
        <el-button :icon="Search" />
      </template>
    </MyInput>
  </template>

在 Vue2 中,需要用到 $slots(插槽) 和 $scopedSlots(作用域插槽):

<template>
    <div class="my-input">
      <el-input
        v-model="childSelectedValue"
        v-bind="attrs"
        v-on="$listeners"
        >
        <!-- 遍历子组件非作用域插槽,并对父组件暴露 -->
        <template v-for="(index, name) in $slots" v-slot:[name]>
          <slot :name="name" />
        </template>
        <!-- 遍历子组件作用域插槽,并对父组件暴露 -->
        <template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
          <slot :name="name" v-bind="data"></slot>
        </template>
      </el-input>
    </div>
  </template>

在 Vue3 中,取消了作用域插槽 $scopedSlots,将所有插槽都统一在 $slots 当中:

<template>
    <div class="my-input">
      <el-input
        v-model="childSelectedValue"
        v-bind="attrs"
        v-on="$listeners"
        >
        <template #[slotName]="slotProps" v-for="(slot, slotName) in $slots" >
          <slot :name="slotName" v-bind="slotProps"></slot>
        </template>
      </el-input>
    </div>
  </template>

4. 使用第三方组件的Methods

有些时候我们想要使用组件的一些方法,比如 el-table 提供9个方法,如何在父组件(也就是封装的组件)中使用这些方法呢?其实可以通过 ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort(),但是这样太麻烦了,代码的可读性差;更好的解决方法:将所有的方法暴露出来,供父组件通过 ref 调用!

在 Vue2 中,可以将 el-table 提供方法提取到实例上:

<template>
  <div class="my-table">
    <el-table ref="el-table"></el-table>
  </div>
</template>

<Script>
export default {
  mounted() {
    this.extendMethod()
  },
  methods: {
    extendMethod() {
      const refMethod = Object.entries(this.$refs['el-table'])
      for (const [key, value] of refMethod) {
        if (!(key.includes('$') || key.includes('_'))) {
          this[key] = value
      }
    }
  },
};
</Script>

<template>
  <MyTable ref="tableRef"></MyTable>
</template>

<Script>
export default {
  mounted() {
    console.log(this.$refs.tableRef.clearSort())
  }
};
</Script>

在 Vue3 中的使用方法如下:

<template>
  <div class="my-table">
    <el-table ref="table"></el-table>
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ElTable } from 'element-plus'

const table = ref();

onMounted(() => { 
    const entries = Object.entries(table.value); 
    for (const [method, fn] of entries) { 
        expose[method] = fn; 
    } 
}); 
defineExpose(expose);
<template>
  <MyTable ref="tableRef"></MyInput>
</template>

<script lang="ts" setup>
import { ref,onMounted } from 'vue'

const tableRef = ref()

onMounted(() => {
  console.log(tableRef.value);
  // 调用子组件中table的方法
  tableRef.value.clearSort()
    
})
</script>

二、v-model 实现双向绑定

我们在封装组件的时候,难免会用到一些表单组件,需要使用 v-model,这个时候可能会遇到一系列的问题,为了更好的解决可能会出现的问题,我们有必要先来了解下关于 v-model 的知识。

1. v-model在Vue2和Vue3中的区别

v-model 本质上是一个绑定属性和事件的语法糖,在 Vue2 和 Vue3 中是有一定的区别的,这里只简单介绍下,想了解更多的内容请查阅相关资料!

在 Vue2 中:

<!-- 子组件 -->
<template>
  <div>
    <input type="text" :value="value" @input="$emit('input', $event.target.value)">
  </div>
</template>

<script>
export default {
  props: {
    value: String,  // 默认接收一个名为 value 的 prop
  }
}
</script>


<!-- 父组件 -->
<my-input v-model="msg"></my-input>
// 等同于
<my-input :value="msg" @input="msg = $event">

在 Vue3 中:

<!-- 父组件 -->
<template>
  <my-input v-model="msg"></my-input>
  <!-- 等同于 -->
  <my-input :modelValue="msg" @update:modelValue="msg = $event"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const msg = ref('hello')
</script>

<!-- 子组件 -->
<template>
  <el-input :modelValue="modelValue" @update:modelValue="handleValueChange"></el-input>
</template>
<script setup lang="ts">
const props = defineProps({
  modelValue: {
    type: String,
    default: '',
  }
});

const emit = defineEmits(['update:modelValue']);

const handleValueChange = (value) => {
    emit('update:modelValue', value)
}
</script>

2. 避免违背Vue的单向数据流

我们来看下面的情况,父组件和子组件中都使用了 v-model,并且绑定的是同一个变量,这个时候就会出问题了,因为子组件直接更改了父组件的数据,违背了单向数据流,这样会导致如果出现数据问题不好调试,无法定位出现问题的根源。

<!-- 父组件 -->
<my-input v-model="msg"></my-input>

<!-- 子组件 -->
<template>
  <el-input v-model="msg"></el-input>
</template>
<script setup lang="ts">
const props = defineProps({
  msg: {
    type: String,
    default: '',
  }
});
</script>

那么有没有方法解决呢?我这里提供了两种解决方法,这里均以 Vue3 的写法为主

第一种是:将 v-model 拆开,通过 emit 让父组件去修改数据

<!-- 父组件 -->
<template>
  <my-input v-model="msg"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const msg = ref('hello')
</script>

<!-- 子组件 -->
<template>
  <el-input :modelValue="modelValue" @update:modelValue="handleValueChange"></el-input>
</template>
<script setup lang="ts">
const props = defineProps({
  modelValue: {
    type: String,
    default: '',
  }
});

const emit = defineEmits(['update:modelValue']);

const handleValueChange = (value) => {
    emit('update:modelValue', value)
}
</script>

第二种方法:使用计算属性的 get set 方法

<!-- 父组件 -->
<template>
  <my-input v-model="msg"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const msg = ref('hello')
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="inputVal"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: String,
    default: '',
  }
});

const emit = defineEmits(['update:modelValue']);

const inputVal = computed(() => {
  get() {
    return props.modelValue
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

3. 使用多个v-model绑定对象属性

现在看起来是没有什么问题,但是如果子组件中有多个表单项(如下面的例子所示),不管是上面哪种方法,都要写很多重复的代码,所以我们需要去寻找解决的办法。

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="name"></el-input>
  <el-input v-model="text"></el-input>
  <el-input v-model="password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const name = computed(() => {
  get() {
    return props.modelValue.name
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      name: val
    })
  }
})

const text = computed(() => {
  get() {
    return props.modelValue.text
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      text: val
    })
  }
})

const password = computed(() => {
  get() {
    return props.modelValue.password
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      password: val
    })
  }
})
</script>

上面使用计算属性监听单个属性,所以需要每个属性都写一遍,我们可以考虑在计算属性中监听整个对象:

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = computed(() => {
  get() {
    return props.modelValue
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

这样看起来没什么问题,读取属性的时候能正常调用 get,但是设置属性的时候却无法触发 set,原因是 modelList.value = xxx,才会触发 set,而 modelList.value.name = xxx,无法触发。这个时候,Proxy 代理对象可以完美的解决这个问题:

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = computed(() => {
  get() {
    return new Proxy(props.modelValue, {
      get(target, key) {
        return Reflect.get(target, key)
      },
      set(target, key, value) {
        emit('update:modelValue',{
          ...target,
          [key]: value
        })
        return true
      }
    })
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

现在已经能够把上面的问题都解决了,我们还可以考虑把这段代码进行封装,可以在多处引入进行使用: useVModel.ts,其实 vueuse 里面有提供了这么一个方法,基本的逻辑是一样的

export function useVModel(props, propsName, emit) {
  return computed(() => {
    get() {
      return new Proxy(props[propsName], {
        get(target, key) {
          return Reflect.get(target, key)
        },
        set(target, key, value) {
          emit('update:' + propsName, {
            ...target,
            [key]: value
          })
          return true
        }
      })
    },
    set(val) {
      emit('update:' + propsName, val)
    }
  })
}

在刚刚的例子中引入使用即可:

<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useVModel } from './useVModel.ts'

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = useVModel(props, 'modelValue', emit)
</script>

标签:Vue,const,ref,props,组件,封装,modelValue,emit
From: https://blog.csdn.net/weixin_46714216/article/details/141751704

相关文章

  • Vue3中的自定义事件和状态提升案例
    Vue3中的自定义事件和状态提升案例在现代Web开发中,Vue.js作为一个轻量级且灵活的前端框架,越来越受到开发者的青睐。而Vue3引入的组合式API(setup语法糖)更是让状态管理和事件处理变得更加优雅。在这篇博客中,我们将探讨如何在Vue3中利用自定义事件和状态提升,实现组件间的有效......
  • 在Vue3中实现懒加载功能
    在Vue3中实现懒加载功能在现代前端开发中,懒加载是一种提高应用性能和用户体验的重要技术,尤其是在处理较大图片或长列表数据时。懒加载意味着仅在用户需要时才加载资源,这有助于减少初始加载时间和提升响应速度。本文将使用Vue3和其新推出的setup语法糖来实现懒加载......
  • 039.CI4框架CodeIgniter,封装Model模型绑定数据库的封装
    01、ModelBase.php代码如下:<?phpnamespaceApp\Models;useCodeIgniter\Database\ConnectionInterface;useCodeIgniter\Model;useCodeIgniter\Validation\ValidationInterface;classModelBaseextendsModel{var$Db;function__construct(Conn......
  • 基于springboot+vue+uniapp的使命召唤游戏助手小程序
    开发语言:Java框架:springboot+uniappJDK版本:JDK1.8服务器:tomcat7数据库:mysql5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9系统展示后台登录界面管理员主界面玩家管理游戏分类管理道具种类管理游戏道具管理战绩信息管理......
  • 基于ssm+vue+uniapp的学生毕业管理小程序
    开发语言:Java框架:ssm+uniappJDK版本:JDK1.8服务器:tomcat7数据库:mysql5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9系统展示管理员登录管理员功能界面学生管理开题答辩管理学生过程文档管理系统管理小程序登录小程序首......
  • vue3 jsx响应式渲染变量
    1、JSX渲染变量vue在html代码区渲染变量使用双大括号{{}},jsx在渲染是单大括号{}另外,这里随便记一下一个简单有点绕的业务逻辑2、多个变量影响判断三元表达式根据上图,想要的效果分别是:订单状态是否支付,显示对应状态已支付的订单是否申请开发票,显示对应状态;且已申请的无法......
  • java+vue计算机毕设信阳新型职业农民在线培育平台【源码+开题+论文】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景信阳作为河南省的重要农业区域,其农业发展对于地方经济具有举足轻重的地位。然而,随着现代农业技术的快速发展和市场需求的不断变化,传统农民面临着知识......
  • java+vue计算机毕设学生信息管理系统【源码+开题+论文】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在当今信息化快速发展的时代,学校的管理效率与服务质量对学生及教职工的学习与生活产生着深远影响。传统的学生信息管理方式依赖纸质文档和人工操作,不......
  • 基于Java+SpringMvc+Vue求职招聘系统详细设计实现
    基于Java+SpringMvc+Vue求职招聘系统详细设计实现......
  • 【Java设计模式】组件模式:使用可复用组件简化复杂系统
    文章目录【Java设计模式】组件模式:使用可复用组件简化复杂系统一、概述二、组件设计模式的别名三、组件设计模式的意图四、组件模式的详细解释及实际示例五、Java中组件模式的编程示例六、何时在Java中使用组件模式七、组件模式在Java中的实际应用八、组件模式的优点和权......