Vue3-组件
我发现很多小伙伴不喜欢写组件 喜欢在一个文件里面写到死 有一些人的项目,一个vue文件一打开,上千行 找个方法都老费劲了 这样其实是不好的 我们要合理使用组件,使用组件简化我们的开发
Vue3-组件
发布时间:2023-12-25 (2023-12-25)

我发现很多小伙伴不喜欢写组件

喜欢在一个文件里面写到死

有一些人的项目,一个vue文件一打开,上千行

找个方法都老费劲了

这样其实是不好的

我们要合理使用组件,使用组件简化我们的开发

组件的好处

  1. 代码重用和可维护性
  2. 提高开发效率
  3. 降低项目复杂度
  4. 提升代码可读性

前端内置组件

就算我们没有还没有学过vue的组件

但是我们也知道前端有些内置的html标签

img video b a

其实我们都可以把他们看成前端的组件

使用图片的时候,我只需要给它一个src值,图片就能显示了,怎么显示的我不需要去关心

a标签也是,我需要跳转就给他一个href值,需要新开一个标签页跳转就加一个target="_blank"

定义组件

当我们需要定义一个自己的组件的时候,我们需要完成以下步骤

例如我要写一个计数按钮++的一个组件

  1. 新建一个 .vue文件

    btnClickNumberAdd.vue

    组件文件的名字 要做到见名知意,建议是小驼峰的命名风格

  2. 编写组件内容

<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>
  1. 注册组件

  2. 使用组件

    我这里没变色是编辑器的问题

好了,这个就是定义组件的最简单流程了,我相信,聪明的你应该学会了

全局注册与局部注册

上面的使用方式就是局部注册

哪个地方需要用这个组件,我就在那个地方引入

如果一个组件,很多地方都要用到,那么可以试试全局引入

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')

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 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传递就会很麻烦了

我们可以使用provideinject 进行依赖注入

父组件

<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/>

要想解决这样的兄弟组件通信,有很多方式

  1. 路由参数

  2. 本地存储

  3. vuex、pinia

  4. 全局事件总线

    vue3的全局事件总线和vue2的不太一样

    需要使用到第三方包 mitt

    可以参考这篇文章

    https://www.jianshu.com/p/9380dd358546

路由参数这种方式,我用的比较多,它有很多很方便的好处

简单说下使用路由参数做兄弟组件之间通信的思路

在标签云组件中,点击某一个标签,给当前路由加上查询参数,带上选择的标签

然后再文章列表组件中,监听路由查询参数的变化,有变化就从里面取值

然后再查一遍文章列表

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>