Vue Component

130 阅读10分钟

组件

组件允许我们将 UI 划分为独立的、可重用的部分来思考。组件在应用程序中常常被组织成层层嵌套的树状结构

这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件数据模型,使我们可以在每个组件内封装自定义内容与逻辑

组件基础

工作原理

我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC), 它是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中

<!-- 逻辑处理部分, 有JavaScript提供该能力 -->
<script setup>
</script>

<!-- 视图模版, 有HTML和自定义组件或者Web组件提供该能力 -->
<template>
  <p class="greeting">{{ greeting }}</p>
</template>

<!-- 元素样式, 由css提供该能力 -->
<style>
</style>

很显然 该文件并不是个合法的 HTML 文件, 并不能别浏览器直接加载, 它是一种特殊的文件格式, 定义该文件的语法被叫做: SFC 语法定义

该语法需要被转换成标准的 HTML 文件才能被浏览器加载, compiler-sfc 就是用于干这个事儿的, 当然它不是独立工作的, vite 提供了构建功能, Vue 以插件的形式提供 sfc语法转换逻辑(compiler-sfc) , 比如下面的 vite 配置:

import { fileURLToPath, URL } from "url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});

一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块,这也意味着通过适当的构建配置,你可以像导入其他 ES 模块一样导入 SFC

import MyComponent from './MyComponent.vue'

export default {
  components: {
    MyComponent
  }
}

定义组件

我们分别定义:

  • script: JS逻辑
  • template: 视图模板
  • style: 样式
<script setup>
import { ref } from "vue";

const count = ref(0);
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

<style scoped>
</style>

使用组件

通过Import导入后, 通过名称直接使用

<template>
  <div>
    <!-- 在单文件组件中, 推荐为子组件使用 PascalCase 的标签名 -->
    <ButtonCounter style="width: 220px" />
  
    <!-- 也可以使用原生 HTML 标签命名风格(单词-单词), 但是为了和标准HTML元素区分开, 并不推荐这样使用 -->
    <!-- <button-counter></button-counter> -->
  </div>
</template>
<script setup>
// 通过 <script setup>,导入的组件都在模板中直接可用
import ButtonCounter from "@/components/ButtonCounter.vue";
</script>

组件注册

向上面那样直接使用时局部导入

你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入

import TestViewVue from "./views/TestView.vue";
app.component("TestViewVue", TestViewVue);

动态组件

有的需求会想要在两个组件间来回切换:

Vue 提供一个 component 元素, 它有一个属性: is , is 的值可以为:

  • 被注册的组件名
  • 导入的组件对象

比如: 在 components 文件夹中添加三个组件 DemoChengDu.vue 。 其他两个类似

<template>
  <div>chengdu</div>
</template>

<script setup></script>

<style lang="scss" scoped></style>

ViewsAboutView.vue

<template>
  <div>
    <ul v-for="(item, index) in Object.keys(cm)" :key="index">  // 遍历map设置组件按钮
      <li>
        <button @click="currentComp = item">{{ item }}</button>
      </li>
    </ul>
    <component :is="cm[currentComp]"></component> // 名称的切换
  </div>
</template>

<script setup>
import ChengDu from "../components/DemoChengdu.vue";
import ShangHai from "../components/DemoShangHai.vue";
import BeiJing from "../components/DemoBeiJing.vue";
import { ref } from "vue";

let cm = {// 组件的map
  BeiJing,
  ChengDu,
  ShangHai,
};

let currentComp = ref("DemoBeiJingVue");
</script>

通过按键可以切换组件 image.png

内置组件

Vue 内置了一些组件, 可以理解为官方提供的一些标准库, 由于内置组件是全局组册的, 因此我们不需要 Import, 可以在模板中直接使用

  • Transition: 组建过渡动画
  • TransitionGroup: 过渡组, 控制一组组建的过渡动画
  • KeepAlive: 组建缓存, 使我们可以在动态切换多个组件时视情况缓存组件实例

Transition 演示:

  • assets 文件夹下的 main.css 文件里添加
    .bounce-enter-active {
     animation: bounce-in 0.5s;
    }
    .bounce-leave-active {
     animation: bounce-in 0.5s reverse;
    }
    @keyframes bounce-in {
     0% {
       transform: scale(0);
     }
     50% {
       transform: scale(1.25);
     }
     100% {
       transform: scale(1);
     }
    }
    
  • AboutView.vue<component> 组件外套上一层 <Transition> 组件
     <Transition name="bounce">
       <component :is="cm[currentComp]"></component>
     </Transition>
    
    • 每次切换组件时有效果

KeepAlive 演示:

  • 在三个demo里面添加一个 button 和响应式变量 count
<template>
  <div>
    <button @click="count++">点击就送</button>
    beijing:{{ count }}
  </div>
</template>

<script setup>
import { ref } from "vue";

let count = ref(0);
</script>

<style lang="scss" scoped></style>
  • AboutView 中的 component 外面套上一层 KeepAlive 组件
<Transition name="bounce">
      <KeepAlive>
        <component :is="cm[currentComp]"></component>
      </KeepAlive>
    </Transition>

切换组件后组件不会销毁,里面的 count 变量依旧存在 image.png

组件通信

组件是 Vue 中用于复用的最小UI单元, 你可以把它理解为后端编程里面的一个函数, 只是这个函数不仅有逻辑还有界面:

func Component() {
   // 定义UI 展示

   // 定义数据处理逻辑
}

现在我们的组件既没有参数,也获取不到组件的返回, 这使得我们的组件很难复用, 比如:

func ComponentA() {
    // 显示 文章A的标签
}

func ComponentB() {
    // 显示 文章B的标签
}

// 使用参数进行统一抽象
func Component(title, callbackFunc) {
    // 根据传人的title参数 进行显示
    title

    // 如果用户修改了title 使用callbackFunc函数回调通知
    callbackFunc(titile)
}

vue 为我们提供组件通讯的机制有:

  • 传递 props: 父组件 --> 子组件
  • 监听事件: 父组件 <-- 子组件
  • v-model: 父组件 <--> 子组件
  • 依赖注入: 多个组件通过共享内存的方式直接通讯

传递 props

传递props, 主要用于 父组件向子组件通讯:

如果向组件传入参数, 首先组件需要定义参数: defineProps 宏就是用于干这个的

<script setup>
// 使用 defineProps 来进行组件属性(参数)的定义。
// defineProps() 是一个宏由编译器负责处理, 无需引入
const props = defineProps({
  title: String,
})

console.log(props.foo)
</script>

最终我们可以通过组建的属性 来进行参数的传递:

<ComponentName title="文章A" likes=10 />

一般情况下都会使用 PascalCase命名法 作为组件标签名,因为这提高了模板的可读性,能很好地区分出 Vue 组件和原生 HTML 元素。 然而这对于传递 prop 来说收效并不高,因此我们选择对其进行转换

示例

//components文件夹内的 DemoChengdu组件
<template>
  <div>
    <li>{{ title }}</li> // 只在这写props会报错
  </div>
</template>

<script setup>
const props = defineProps({
  title: String,
});
console.log(props.title);// 防止报错
</script>

<style lang="scss" scoped></style>

//Views文件夹的 AboutView文件
<template>
  <div>
    // :title 对子组件的内容进行绑定 并将数值传递给子组件
    <component :is="ChengDu" :title="chengduTitle"></component>
  </div>
</template>

<script setup>
import ChengDu from "../components/DemoChengdu.vue";
const chengduTitle = "哈哈"; 
</script>

动态属性

很多时候我们传递的属性都是和我们的响应式数据绑定的, 组件和元素 HTML 在属性绑定上并没有啥区别, 都是用v-bind 执行来进行响应式数据绑定

<!-- 使用v-bind指令 绑定变量name -->
<ComponentName v-bind:greeting-message="name" />

<!-- v-bind可以缩写为: 缩写模式 -->
<ComponentName :greeting-message="name" />

属性校验

定义属性时处理可以指定 type 以外,还可以有:

  • required: bool 是否是必须参数
  • default 属性: 集成类型默认值
  • default 函数: 对象或数组的默认值, 类似于个构造函数
  • validator函数: 属性的校验函数
defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // Number 类型的默认值
  propD: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propE: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  propF: {
    validator(value) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propG: {
    type: Function,
    // 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

注意事项(单数据流)

所有的 prop 都遵循着单向绑定原则,prop 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改了父组件的状态,不然应用的数据流就会变得难以理解了

监听事件

监听事件, 主要用于 子组件向父组件通讯: 结合传递props建立一个双向通讯

  • 通过 defineEmits() 宏来声明要触发的事件, defineProps()定义子组件参数
  • 调用声明 doClick 来进行事件的触发
  • 点击按钮后将 count+1 的消息传递给父组件
  • 父组件通过 v-on 监听事件并通过updateCountEnventHandler 函数处理事件并返给子组件的props.count

定义并触发事件

<script setup>
const props = defineProps({
  count: Number,
});

// 其中事件的名称可以使用数组来进行定义
const emit = defineEmits(["update_count"]);

const doClick = () => {
  // 把修改的值传递给父组件, 由父组件来更新count props
  emit("update_count", props.count + 1);
};
</script>

<template>
  <button @click="doClick">You clicked me {{ count }} times.</button>
</template>

然后我们调用声明来进行事件的触发, 触发时 可以携带任何类型的数据

监听并处理事件

<template>
  <div class="about">
    <cheng-du
      @update_count="updateCountEventHandler"
      :count="count"
    ></cheng-du>
  </div>
</template>

<script setup>
import { ref } from "vue";
import ChengDu from "../components/DemoChengdu.vue";

// 通过 v-on 监听来自于子组件的update_count事件
const count = ref(0);
const updateCountEventHandler = (e) => {
  count.value = e;
};
</script>

双向绑定

v-model实现

很显然上面的写法很累赘, 我们事件处理函数的处理逻辑也很单调, 仅仅是赋值, 像元素的 HTML 元素 都可以直接使用 v-model 来实现双向绑定, 比如:

<input v-model="searchText" >

那能不能把组件也通过这个语法直接变成双向绑定的喃?

<ButtonCounter v-model="count" >

为了使组件能像这样工作,组件必须

  • 定义一个 modelValue 的属性
  • 定义 update:modelValue 的事件, 并且负责更新
<script setup>
const props = defineProps({
  modelValue: Number,
});

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

const doClick = () => {
  // 把修改的值传递给父组件, 由父组件来更新count props
  emit("update:modelValue", props.modelValue + 1);
};
</script>

<template>
  <button @click="doClick">You clicked me {{ modelValue }} times.</button>
</template>

这样我们的父组件就能完成双向通行了 少写了监听事件 v-on

<template>
  <div>
    <ChengDu v-model="count" />
  </div>
</template>

<script setup>
import { ref } from "vue";
import ChengDu from "../components/DemoChengdu.vue";
const count = ref(0);
</script>

由上面可以看出 v-model 作用于自定义组件时, 相当于固定了 modelValue 属性和 update:modelValue 方法

v-model:props实现

由于直接使用 v-model 就等于直接绑定了 modelValue 属性和 update:modelValue 事件, 而且还只针对一个属性, 如果有多个自定义属性怎么办?

v-model 提供了一个参数: v-model:propsName , 比如要绑定的属性名称为 count , 则:

<!-- 如果有多个属性需要绑定就写多个v-model:attr就可以了 -->
<ButtonCounter v-model:count="count" />

因此修改子组件的属性和事件命名:

<script setup>
const props = defineProps({
  count: Number,
});

const emit = defineEmits(["update:count"]);

const doClick = () => {
  emit("update:count", props.count + 1);
};
</script>

<template>
  <button @click="doClick">You clicked me {{ count }} times.</button>
</template>

依赖注入

上面讲到的都是父子组件的通讯, 如果组件并不是父子关系, 而是多层的关系,比如这样, 难道我们需要把 props 一层一层的往下传递?:

注入到父组件的变量, 所有的后面的子组件才能获取到, 就像 Golang 里面的 context

注入依赖

要为组件后代供给数据,需要使用到 provide() 函数, 由于组件本身就是树状结构, 只要我们注入到根组件,那么后续所有组件都能访问到

修改 App.vue

<script setup>
import { RouterLink, RouterView } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue";

import { ref, provide } from "vue";
const count = ref(0);
provide(/* 注入名 */ "count", /* 值 */ count);
</script>

获取依赖

通过 inject 获取父组件注入的变量:(可在APP的任意子组件内使用)

<script setup>
import { inject } from "vue";

// 这里也可以获取默认值: inject(<变量名称>, <变量默认值>), 如果获取不到变量 就使用默认值
const count = inject("count");

const doClick = () => {
  count.value++;
};
</script>

<template>
  <button @click="doClick">You clicked me {{ count }} times.</button>
</template>

依赖注入约等于共享内存,因此灵活度很高

组合式函数

上面的方式 必须像通过上下文 来传递变量, 编程中 有一种更常用的 贡献内存通信的方式: 全局变量

我们可以定义一个全局变量, 在需要的地方直接导入,然后访问该变量的状态

Vue 中, 我们很少直接定义这种全局变量, 它提供了一种 通过定义一个函数来 访问该全局变量的状态的方式: 组合式函数

“组合式函数”是一个利用 Vue 组合式 API 来封装和复用有状态逻辑的函数,

定义组合式函数

鼠标跟踪器示例: 获取当前鼠标的位置坐标:

我们通过编写一个 JS 的模块, 导出一个函数给外部使用(工具函数), 该函数维护这2个响应式变量 (x, y) , 分别代码光标的 x,y位置

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

使用组合式函数

组合式函数就是一个带有响应式数据的普通函数, 并没有特殊之处, 我们按照 JS 模块引入规范,导入使用即可:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

image.png

状态共享

定义一个保护响应式数据的公共模块

// store/global.js
import { reactive } from "vue";

export const store = reactive({
  count: 0,
});

button组件引入并使用:

<script setup>
import { store } from "@/stores/global";

const doClick = () => {
  store.count++;
};
</script>

<template>
  <button @click="doClick">You clicked me {{ store.count }} times.</button>
</template>

其他组件也可以引入并使用:

import { store } from "@/stores/global";
// 访问store
store.count

但是显然我们基于 store 进行数据共享,但是我们 store 并没有持久化, 页面一刷新就没了, 有没有一种能持久化 并且是响应式的数据源工具喃? 这就不得不讲到 vue的 公共库: VueUse

VueUse库

首先我们安装上这个库:

npm i @vueuse/core

然后导入我们需要的函数使用

import { useMouse } from '@vueuse/core'

const { x, y, sourceType } = useMouse()

由于 VueUse 的出现, Vue3 终于可以比肩并 (react Hooks) 且有超越 react 的趋势

VueUse 的中有个这样的库: useStorage, 他可以把浏览器的 Localstorage 包装成一个响应式对象

import { useStorage } from "@vueuse/core";
// 第一个参数是key, 第二个参数是vulue
const count = useStorage("count", 0);