前言:本文主要讲解项目中的Vue3的组件封装技巧,比较基础,但是也很实用!篇幅可能会有点长,想把能介绍的都介绍,方便更好理解封装的逻辑,数据的处理。希望本文能给掘友们带来帮助。
下面就开始演示(可以根据目录选择相应内容阅读)
注意vue版本!!!
下面代码:js 采用,<script setup> </script> 编译时语法糖,相比普通script具有
更简洁代码,更少样版内容,
而且还可以使用纯ts声明props和自定义事件,
以及更好的运行时性能。
创建一下vue应用
npx create vue@latest //这里版本最新是vue 3.4
简单的组件封装及使用
在src下创建一个xxxComps.vue
<template>
<div>{{_value}}</div>
<input v-model="_value" />
</template>
<script setup>
import{ref} from "vue";
const _value = ref("");
</script>
然后在App.vue中引入一下
<template>
<XxxComps />
<!-- 使用了两次组件,创建了两个实例,实例的数据是互不影响的 -->
<XxxComps />
</template>
<script setup>
import XxxComps from "./xxxComps.vue";
</script>
下面再升级一下难度,传值
组件的值传递
准备
为了方便演示,加入一下antdesign vue
如若有配置,此步骤可以跳过。
npm install ant-design-vue --save //4.2.3版本
简单配置一下按需导入【文档链接1】【文档链接2】 【文档链接3】
npm i less -D //css预处理器
npm i unplugin-vue-components -D //按需加载组件
npm i unplugin-auto-import -D //自动导入例如ref reactive这些
npm i vite-plugin-style-import -D //按需加载样式
在vite.config.ts中配置一下
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import AutoImport from 'unplugin-auto-import/vite'
import { createStyleImportPlugin, AndDesignVueResolve } from 'vite-plugin-style-import'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [AntDesignVueResolver({ importStyle: 'less' })]
}),
createStyleImportPlugin({
resolves: [AndDesignVueResolve()], //按需导入antdVue样式
libs: [
{
libraryName: 'ant-design-vue',
esModule: true,
resolveStyle: (name) => {
return `ant-design-vue/es/${name}/style/index`
}
}
]
}),
AutoImport({
imports: [
'vue', // 自动导入vue的API
'vue-router'
// '@vueuse/core' // 可选,导入 VueUse 库中的 API
],
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
dts: true // 生成 `auto-imports.d.ts` 声明文件
})
],
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
ts问题解决
//解决引入vue文件无法找到模块问题
//无法找到模块“./App.vue”的声明文件。“d:/dataSource/TaoVue/src/App.vue”隐式拥有 "any" 类型。ts(7016)
//在tsconfig.json文件include下添加"shims-vue.d.ts"
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"shims-vue.d.ts", // 确保包含这个文件
"auto-imports.d.ts"
],
//根目录创建shims-vue.d.ts
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
//无法找到文件 Cannot find package 'consola'
npm install consola -D
//是一个用于增强 Node.js 和浏览器环境下日志记录功能的库
配置好后重新启动项目
弹窗类型的组件
这里涉及到3个宏:defineProps、defineEmits、defineExpose
在src下创建api文件夹,然后创建一个message.ts文件模拟请求。
export const getMessageData = () => {
return new Promise((res) => {
setTimeout(() => {
res({
data: {
title: '',
name: 'kk',
age: 25,
phone: '135888999111',
list: ['red', 'blue']
}
})
}, 1500)
})
}
在components文件夹新建一个Modal文件,以及Modal文件里面创建一个baseModal.vue
<template>
<div ref="modalRef"></div>
<a-modal
:open="visible"
:title="title"
@cancel="visible = false"
@ok="okFn"
:getContainer="() => $refs.modalRef"
>
<a-form :mode="formState">
<a-form-item label="年龄" name="age">
<a-input-number v-model:value="formState.age" />
</a-form-item>
<a-form-item label="电话" name="phone">
<a-input v-model:value="formState.phone" />
</a-form-item>
<a-form-item label="喜欢的颜色" name="list">
<a-select v-model:value="formState.list" mode="multiple">
<a-select-option value="red">红色</a-select-option>
<a-select-option value="blue">蓝色</a-select-option>
<a-select-option value="orange">黄色</a-select-option>
<a-select-option value="white">白色</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
const visible = ref(false)
const props = defineProps({
title: {
type: String,
default: '弹窗标题'
}
})
const emits = defineEmits(['formState'])
const formState = reactive({ age: '', phone: '', list: [] })
/**
*
* @param bl bl:boolean 而不是 bl: Boolean,因为Boolean是对象,创建和处理对象会有额外的开销,而boolean原始类型,性能更好
*/
const openModal = (bl: boolean, data: Object) => {
visible.value = bl
Object.assign(formState, data)
}
/**
* 弹窗的提交方法
*/
const okFn = () => {
emits('formState', formState)
visible.value = false
}
defineExpose({ openModal })
</script>
</script>
这里使用了defineProps宏,结合父组件的v-bind,进行父组件向子传递了title这个数据。同时也将弹窗控制open的方法通过defineExport宏暴露出去给父组件调用。 通过defineEmits宏,结合父组件的v-on,进行子组件向父组件传递数据。
在App.vue中导入组件使用
<template>
<div>
<a-form>
<a-form-item label="名称" name="name">
<a-input v-model:value="formState.name"></a-input>
</a-form-item>
<a-form-item label="年龄" name="age">
<a-input-number v-model:value="formState.age"></a-input-number>
</a-form-item>
<a-form-item label="电话" name="phone">
<a-input v-model:value="formState.phone"></a-input>
</a-form-item>
<a-form-item label="喜欢的颜色" name="list">
<a-select v-model:value="formState.list" mode="multiple">
<a-select-option value="red">红色</a-select-option>
<a-select-option value="blue">蓝色</a-select-option>
<a-select-option value="orange">黄色</a-select-option>
<a-select-option value="white">白色</a-select-option>
</a-select>
</a-form-item>
</a-form>
<div>
<a-button @click="() => getMessageDataFn()" :loading="loading">请求数据</a-button>
<a-button @click="openModal">弹窗启动</a-button>
</div>
</div>
<BaseModal ref="baseRef" title="APP弹窗" @formState="getFormState"></BaseModal>
</template>
<script setup>
import { getMessageData } from '@/api/message.ts'
import BaseModal from './components/Modal/baseModal.vue'
const baseRef = ref(null) //获取组件实例,在onMounted之后拿到defineExpose暴露的数据
//请求数据
const loading = ref(false)
const formState = reactive({})
const getMessageDataFn = async () => {
loading.value = true
const res = await getMessageData()
Object.assign(formState, res.data)
loading.value = false
}
//打开弹窗
const openModal = () => {
//通过组件实例获取 defineExpose暴露的数据
baseRef.value.openModal(true, formState)
}
//获取弹窗返回的数据
const getFormState = (value) => {
console.log(value)
}
运行npm run dev,请求数据
此时修改年龄还有电话这些基本类型数据,对父组件基本类型数据是没什么影响(就是影响父组件数据同步改变),因为Object.assign浅拷贝基本类型的是值。但是数组是浅拷贝引用的,为什么改变数组,父组件的数组同样是没影响的呢?我认为是antdesign 的select组件值返回的是新数组,引用发生改变,就不会导致父组件数组的值改变。这边就不去深究了,感兴趣的可以去看看antdesign select的源码。
其实为了更好的代码健壮性,安全性,还有更好的维护,遵循vue的单项数据流原则。在组件封装时,最好就是组件要什么数据就给什么数据,然后组件再深拷贝,使其数据断开关联。
//安装lodash工具
npm i lodash -D
npm i @types/lodash -D
//tsconfig.json配置
"compilerOptions": {
+ "typeRoots": ["./node_modules/@types"],
+ "types": ["lodash"]
}
然后对baseModal.vue代码进行一些修改。
import _ from 'lodash'
const openModal = (bl: boolean, data: Object) => {
visible.value = bl
Object.assign(formState, _.cloneDeep(data))
}
const okFn = () => {
emits('formState', _.cloneDeep(formState))
visible.value = false
}
像是弹窗这类型组件,一般都有删除,更新,新增这些操作在里面,当完成这些操作时,还是需要去更新列表的数据的。列表的数据一般都在父组件发起请求的。所以可以这么操作:
在App.vue中
//打开弹窗
const openModal = () => {
//通过组件实例获取 defineExpose暴露的数据
//getMessageDataFn可以当做回调函数传给子组件。
baseRef.value.openModal(true, formState, getMessageDataFn)
}
在baseModal.vue中
+ let callBack: Function //记录父组件的回调函数
const openModal = (bl: boolean, data: Object,fn: Function) => {
visible.value = bl
+ callBack = fn;
Object.assign(formState, _.cloneDeep(data))
}
const okFn = () => {
emits('formState', _.cloneDeep(formState))
//执行回调函数
+ callBack()
visible.value = false
}
}
深层组件的数据传递
深层组件数据传递,主要有:provide + inject、vuex、pinia
provide + inject
同样,为了更好的代码健壮性,安全性,还有更好的维护,遵循vue的单项数据流原则,建议在父组件的值,就在父组件修改,子组件只读。那怎么实现呢?
为了方便。下面都写创建什么目录和文件,直接用文件路径表示吧
// src/app.vue
<template>
<router-view></router-view>
</template>
<script setup lang="ts"></script>
// src/router/index.ts routes新增
{
path: '/message',
name: 'message',
component: () => import('../views/message/index.vue')
}
// src/views/message/index.vue
<template>
<div style="background-color: skyblue; padding: 10px">
<div>父组件formState: {{ formState }}</div>
<Base />
</div>
</template>
<script setup lang="ts">
import Base from './comps/base.vue'
const formState = ref({ name: 'Mr.k' })
const changeName = (value: string) => {
formState.value.name = value
}
provide('formData', readonly(formState)) //只读,避免数据被污染
provide('changeName', changeName) //处理formState数据的逻辑保留在父组件。
</script>
// src/views/message/comps/base.vue
<template>
<div style="background-color: #ccc; padding: 10px">
<div>子组件</div>
<Base1 />
</div>
</template>
<script setup lang="ts">
import Base1 from './base1.vue'
</script>
// src/views/message/comps/base1.vue
<template>
<div style="background-color: #eceff7; padding: 10px">
孙组件formState:{{ formState }}
<a-button @click="() => callback('123')">改变父组件的name</a-button>
</div>
</template>
<script setup lang="ts">
const formState = inject('formData')
//inject 有三个参数,第一个是key,第二个是默认值,第三个是布尔值,true表示第二个参数被当做一个工厂函数
const callback = inject<Function>('changeName', Function)
</script>
一般在封装组件库提供给别人使用时,推荐symbol作为注入名,避免潜在冲突。 具体步骤请参考:文档
数据双向绑定
数据的双向绑定,依旧是遵循着单向数据流传输原则。 这里主要介绍三种方法的使用
1. defineEmits:update语法糖
// src/views/message/index.vue
<template>
<div style="background-color: skyblue; padding: 10px">
<div>父组件formState: {{ formState }}</div>
<UpdateInput v-model:formData="formState" />
</div>
</template>
<script setup lang="ts">
import UpdateInput from './comps/updateInput.vue'
const formState = ref({ name: 'coder' })
</script>
// src/views/message/comps/updateInput.vue
<template>
<div>
<span>SON:{{ formState }}</span>
<a-input v-model:value="formState.name" @change="changeValue" />
</div>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash'
const props = defineProps({
formData: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits(['update:formData'])
const propsFormData = computed(() => props.formData)
const formState = ref({ name: '' })
watch(
() => propsFormData.value,
(value: { name: string }) => {
formState.value = cloneDeep(value)
},
{ immediate: true }
)
const changeValue = () => {
emits('update:formData', cloneDeep(formState.value))
}
</script>
2. vueUse:useVModels link
npm i @vueuse/core
// src/views/message/index.vue
<template>
<div style="background-color: skyblue; padding: 10px">
<div>父组件name: {{ name }}</div>
<UpdateInput v-model:name="name" />
</div>
</template>
<script setup lang="ts">
import UpdateInput from './comps/updateInput.vue'
const name = ref('coder')
</script>
// src/views/message/comps/updateInput.vue
<template>
<div>
<span>SON:{{ name }}</span>
<a-input v-model:value="name" />
</div>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash'
import { useVModels } from '@vueuse/core'
const props = defineProps({
name: {
type: String,
default: () => ({})
}
})
const emits = defineEmits(['update:name'])
const { name } = useVModels(props, emits)
</script>
3. vue3.4+版本: defineModel link
// src/views/message/index.vue
<template>
<div style="background-color: skyblue; padding: 10px">
<div>父组件name: {{ name }}</div>
<UpdateInput v-model:name="name" />
</div>
</template>
<script setup lang="ts">
import UpdateInput from './comps/updateInput.vue'
const name = ref('coder')
</script>
// src/views/message/comps/updateInput.vue
<template>
<div>
<span>SON:{{ name }}</span>
<a-input v-model:value="name" />
</div>
</template>
<script setup lang="ts">
const name = defineModel('name', { type: String, default: '' })
</script>
懒加载组件
defineAnsyComponent 定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
一般用于弹窗组件,抽屉组件这些(不需要在初始化完成展示的页面) 还有结合Teleport投送标签使用等等。
使用起来也很简单:
const Modal = defineAsynComponent(() => import("./modal.vue"));
插槽的使用
- 默认插槽
- 具名插槽
- 作用域插槽
- 条件插槽
- 动态插槽
// src/views/message/index.vue
<template>
<div style="background-color: skyblue; padding: 10px">
<SlotModule>
<template #default>
<div class="father">父组件使用默认插槽</div>
</template>
<template #one>
<div class="father">父组件使用具名插槽</div>
</template>
<template #two>
<div class="father">父组件使用条件插槽</div>
</template>
<template #three="props">
<div class="father">父组件使用作用域插槽:{{ props.count }}</div>
</template>
</SlotModule>
</div>
</template>
<script setup lang="ts">
import SlotModule from './comps/slotModule.vue'
</script>
<style lang="less" scoped>
.father {
padding: 20px;
margin: 10px;
background-color: skyblue;
}
</style>
// src/views/message/comps/slotModule.vue
<template>
<div class="slot">
<div>
<span>默认插槽</span>
<slot>
<div>默认插槽</div>
</slot>
</div>
<div>
<span>具名插槽</span>
<slot name="one">
<div>具名插槽</div>
</slot>
</div>
<div v-if="$slots.two">
<span>条件插槽</span>
<slot name="two">
<div>条件插槽</div>
</slot>
</div>
<div>
<span>作用域插槽</span>
<slot name="three" :count="111">
<div>作用域插槽</div>
</slot>
</div>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.slot {
display: flex;
flex-direction: row;
gap: 30px;
& > div {
height: 300px;
width: 300px;
background-color: pink;
}
}
</style>
巧妙使用样式透传
比如我们针对于antdesign vue的a-input再次封装。 首先我们看一下它的api
通过透传,将这些attributes传递到a-input上,注意透传是不包含已经声明的props或者emits
// src/views/message/index.vue
<template>
<div style="background-color: skyblue; padding: 10px">
<div>父组件name: {{ name }}</div>
<UpdateInput v-model:name="name" :maxlength="20" showCount @change="inputChange" />
</div>
</template>
<script setup lang="ts">
import UpdateInput from './comps/updateInput.vue'
const inputChange = (e: any) => {
console.log(e)
}
const name = ref('coder')
</script>
// src/views/message/comps/updateInput.vue
<template>
<div>
<span>SON:{{ $attrs }}</span>
<a-input v-model:value="name" v-bind="$attrs" />
</div>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue'
const name = defineModel('name', { type: String, default: '' })
console.log(useAttrs())
onUpdated(() => {
console.log('updated', useAttrs())
})
</script>
总结
每一个组件的封装其实都是基础知识的拼凑,牢固的基础知识,会使你在封装组件的时候得心应手。