
我发现很多小伙伴不喜欢写组件
喜欢在一个文件里面写到死
有一些人的项目,一个vue文件一打开,上千行
找个方法都老费劲了
这样其实是不好的
我们要合理使用组件,使用组件简化我们的开发
组件的好处
- 代码重用和可维护性
- 提高开发效率
- 降低项目复杂度
- 提升代码可读性
前端内置组件
就算我们没有还没有学过vue的组件
但是我们也知道前端有些内置的html标签
img video b a
其实我们都可以把他们看成前端的组件
使用图片的时候,我只需要给它一个src值,图片就能显示了,怎么显示的我不需要去关心
a标签也是,我需要跳转就给他一个href值,需要新开一个标签页跳转就加一个target="_blank" 值
定义组件
当我们需要定义一个自己的组件的时候,我们需要完成以下步骤
例如我要写一个计数按钮++的一个组件
新建一个 .vue文件
btnClickNumberAdd.vue
组件文件的名字 要做到见名知意,建议是小驼峰的命名风格
编写组件内容

<template>
<div class="btn">
<button @click="add">++</button>
<span>当前值:{{ number }}</span>
</div>
</template>
<script lang="ts" setup>
import {ref} from "vue";
const number = ref(0)
function add() {
number.value++
}
</script>
<style scoped>
.btn {
display: flex;
align-items: center;
}
</style>
注册组件
使用组件

我这里没变色是编辑器的问题
好了,这个就是定义组件的最简单流程了,我相信,聪明的你应该学会了
全局注册与局部注册
上面的使用方式就是局部注册
哪个地方需要用这个组件,我就在那个地方引入
如果一个组件,很多地方都要用到,那么可以试试全局引入
import { createApp } from 'vue'
import App from './App.vue'
import btnClickNumberAddProps from "@/components/btnClickNumberAddProps.vue"
const app = createApp(App)
app.component("btnClickNumberAddProps", btnClickNumberAddProps)
app.mount('#app')
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
组件传递
父传子
这个时候,我发现每个组件点一次只能+1,子组件加多少,我希望父组件能够把这个值传递给子组件
怎么做呢?
在子组件中,我们用defineProps去接收
<script lang="ts" setup>
import {ref} from "vue";
const props = defineProps({
plusNumber: {
type: Number,
default: 1,
}
})
const number = ref(0)
function add() {
number.value += props.plusNumber
}
</script>
在父组件中,使用自定义属性传递
<btnClickNumberAddProps></btnClickNumberAddProps>
<btnClickNumberAddProps :plus-number="10"></btnClickNumberAddProps>
如果我们是用ts,那么这样写还不够,我们需要给我们的props定义类型
如下
<script lang="ts" setup>
import {ref} from "vue";
interface Props {
plusNumber?: number // 这里加?可以解决非必填的问题
}
// 这样没法弄默认值了
const props = defineProps<Props>()
// 解决默认值问题
const {plusNumber = 1} = props
const number = ref<number>(0)
function add() {
number.value += plusNumber
}
</script>
也可以用这种
interface Props {
plusNumber?: number
}
// 这样就可以设置默认值了
const props = withDefaults(defineProps<Props>(), {
plusNumber: 1,
})
在子组件里面,不要尝试修改父组件传递进来的props
子通知父
用户有没有点击按钮,作为父组件其实是不知道的
如果我想让父组件知道,谁点击了按钮,那么我们可以给子组件编写一个事件
点击按钮之后,就通知父组件
如下,子组件编写
<script lang="ts" setup>
import {ref} from "vue";
const number = ref(0)
const emits = defineEmits(["plus"])
function add() {
number.value++
emits("plus", "我被点啦", number.value) // 这里传递了两个参数,我们看看父组件怎么接
}
</script>
父组件接收这个事件
<script setup lang="ts">
import btnClickNumberAddEvents from "@/components/btnClickNumberAddEvent.vue"
function plus(name: string, val: number){
console.log(name, val)
}
</script>
<template>
<div>
<btnClickNumberAddEvents @plus="plus"></btnClickNumberAddEvents>
</div>
</template>
如果是ts的话,子组件可以这样写
interface Emits {
(e:"plus", name: string, val: number): void
}
const emits = defineEmits<Emits>()
也可以这样写
const emits = defineEmits<{
plus: [string, number]
}>()
父传孙

如果组件之间离的太远,再用props传递就会很麻烦了
我们可以使用provide 和 inject 进行依赖注入
父组件
<script setup lang="ts">
import btnMsgParent from "@/components/btnMsgParent.vue"
import {provide} from 'vue'
// 注入名 值
provide('message', 'hello!')
</script>
<template>
<div>
<btnMsgParent></btnMsgParent>
</div>
</template>
中间组件
<template>
<div>
我是中间层
<btnMsg></btnMsg>
</div>
</template>
<script setup lang="ts">
import btnMsg from "@/components/btnMsg.vue"
</script>
孙组件
<template>
<div>这是{{ message }}</div>
</template>
<script setup lang="ts">
import {inject} from 'vue'
const message = inject('message')
// const message = inject('message', "默认值") // 设置默认值
</script>
兄弟组件通信

兄弟组件之间的通信还是很常见的
例如我的博客里面
文章列表是一个组件,标签云是一个组件
它们都在web/index.vue这个组件里面,页面大致结构如下
<div class="left">
<文章列表组件/>
<div/>
<div class="right">
<标签云组件>
<div/>
要想解决这样的兄弟组件通信,有很多方式
路由参数
本地存储
vuex、pinia
全局事件总线
vue3的全局事件总线和vue2的不太一样
需要使用到第三方包 mitt
可以参考这篇文章
路由参数这种方式,我用的比较多,它有很多很方便的好处
简单说下使用路由参数做兄弟组件之间通信的思路
在标签云组件中,点击某一个标签,给当前路由加上查询参数,带上选择的标签
然后再文章列表组件中,监听路由查询参数的变化,有变化就从里面取值
然后再查一遍文章列表
v-model
我们回忆一下,之前在原生input组件上使用v-model的场景
<input v-model="searchText" />
其实在这段代码的背后,vue在背后给我们做了很多的事情
它会等价转换为以下这段
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
如果我们自己的组件,也想实现v-model这个功能怎么办呢
单个v-model
如果是单个v-model
那么可以这样做
<template>
<div>
<div>
这是输入的内容:{{ modelValue }}
</div>
<div>
<input type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
</div>
</div>
</template>
<script setup lang="ts">
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<script setup lang="ts">
import myInput from "@/components/myInput.vue"
import {ref} from "vue";
const data = ref("")
</script>
<template>
<div>
<myInput v-model="data"></myInput>
</div>
</template>
多个v-model
<template>
<div>
<div>
姓名: {{ modelValue }}
年龄: {{ age }}
</div>
<div>
姓名: <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
年龄: <input type="number" :value="age" @input="$emit('update:age', $event.target.value)">
</div>
</div>
</template>
<script setup lang="ts">
defineProps(['modelValue', "age"])
defineEmits(['update:modelValue', "update:age"])
</script>
<script setup lang="ts">
import myInput from "@/components/myInput.vue"
import {ref} from "vue";
const name = ref("")
const age = ref(18)
</script>
<template>
<div>
<myInput v-model="name" v-model:age="age"></myInput>
</div>
</template>
插槽
我们在使用前端默认标签的时候
闭合标签里面是不是也是可以写内容的,例如
<div>123</div>
<div><div><span></span></div></div>
标签里面可以是文本,图片,链接,也可以是另一个标签
那么vue组件也可以这样做,只不过我们叫它 插槽
默认插槽
<template>
<div>
slot前
<slot></slot>
slot后
</div>
</template>
父组件
<script setup lang="ts">
import slot1 from "@/components/slot1.vue"
</script>
<template>
<div>
<slot1>msg</slot1>
</div>
</template>
插槽里面不仅可以是文本,也可以是变量,标签,甚至是另一个组件
具名插槽
在同一个组件,包含多个可以插槽的内容是非常有必要的
例如 模态框组件
内容部分可以用默认插槽
尾部也可以用一个插槽,方便替换
<template>
<div class="modal">
<div class="modal_title">
<slot name="head">模态框标题</slot>
</div>
<div class="modal_body">
<slot>内容</slot>
</div>
<div class="modal_footer">
<slot name="footer">
<button>取消</button>
<button>确定</button>
</slot>
</div>
</div>
</template>
父组件
<script setup lang="ts">
import modal from "@/components/modal.vue"
</script>
<template>
<div>
<modal>
<template v-slot:head>标题</template>
<div>给我一件三连吧~</div>
<template v-slot:footer>尾部</template>
</modal>
</div>
</template>
v-slot 有对应的简写 #,因此 <template v-slot:head> 可以简写为
<template #head>
动态插槽
<template>
<div>
这里有很多插槽
<div>
<slot name="mo_1">1</slot>
<slot name="mo_2">2</slot>
<slot name="mo_3">3</slot>
<slot name="mo_4">4</slot>
<slot name="mo_5">5</slot>
<slot name="mo_6">6</slot>
<slot name="mo_7">7</slot>
<slot name="mo_8">8</slot>
<slot name="mo_9">9</slot>
</div>
</div>
</template>
<script setup lang="ts">
import syncModal from "@/components/syncModal.vue"
import {computed, ref} from "vue";
const name = ref(1)
function click() {
name.value++
if (name.value === 10) {
name.value = 1
}
}
const slotName = computed(() => {
return "mo_" + name.value
})
</script>
<template>
<div>
<button @click="click">点我</button>
<syncModal>
<template #[slotName]>a</template>
</syncModal>
</div>
</template>
作用域插槽
具名插槽的作用域
<template>
<div>
<slot name="body" msg="这是消息" :age="12"></slot>
</div>
</template>
<msgSlot>
<template #body="data">
这是插槽内部传递的数据: {{ data.msg }} age:{{ data.age }}
</template>
</msgSlot>
默认插槽的作用域
和具名插槽的写法不太一样
<template>
<div>
<slot msg="这是消息" :age="12"></slot>
</div>
</template>
只有默认插槽的情况下
<msgSlot v-slot="data">
这是插槽内部传递的数据: {{ data.msg }} age:{{ data.age }}
</msgSlot>
万能
<template>
<div>
<my-slot>
<template #default="d">
身体 {{ d }}
</template>
<template #header="data">
头部 {{ data}}
</template>
</my-slot>
</div>
</template>