准备阶段

创建Vue3工程

基于vue-cli

官方文档:Home | Vue CLI (vuejs.org)

1
2
Vue CLI 现已处于维护模式!
现在官方推荐使用 create-vue 来创建基于 Vite 的新项目。另外请参考 Vue 3 工具链指南 以了解最新的工具推荐。

基于vite(推荐)

vite是新一代前端构建工具,官网地址:https://vitejs.cn

vite的优势如下:

  • 轻量快速的热重载(HMR),能实现极速的服务启动
  • TypeScriptJSXCSS等支持开箱即用
  • 真正的按需编译,不再等待整个应用编译完成
  • 具体操作如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
npm create vue@latest

Need to install the following packages:
create-vue@3.11.0
Ok to proceed? (y) y


> npx
> create-vue


Vue.js - The Progressive JavaScript Framework

√ 请输入项目名称: ... demo1
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
√ 是否引入 Prettier 用于代码格式化? ... 否 / 是
√ 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) ... 否 / 是

正在初始化项目 C:\Users\Zhao\Desktop\Vue3WithTs\demo1\demo1...

项目初始化完成,可执行以下命令:

cd demo1
npm install
npm run dev

Tips:如果文件爆红,是由于缺少依赖包导致的,在项目根目录下输入以下命令:

1
npm i

输入完后关闭vscode并重新打开即可

目录结构

创建完成后的项目,包括了.vscodenode_modulespublicsrc文件夹,以及其他配置文件

.vscode:不用关注,主要为了第一次使用时,在vscode编译器中安装官方插件

node_modules:不用关注,存放项目的依赖包

public:存放图片、图标等相关资源

src:主要操作的文件夹,需要包含App.vuemain.ts文件

index.html:项目的入口文件

App.vue文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 一个.vue文件中只能存在一个顶层template -->
<template>
<!-- html -->
<div class="title">Hello World</div>
</template>

<script lang="ts">
// TS或JS
export default {
name:'App' // 组件名
}
</script>

<style>
/* css样式 */
.title {
background-color: #ddd;
color: aqua;
text-align: center;
}
</style>

注意点:需要将组件名暴露出去,否则main.js不认为App.vue是一个组件

main.ts文件:

1
2
3
4
5
6
7
// 引入createApp用于创建应用
import { createApp } from 'vue'
// 引入App根组件
import App from './App.vue'

// 创建App且挂载
createApp(App).mount('#app')

自定义组件

在src文件下创建components文件夹,并新建一个Person.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="showTel">点击查看手机号</button>
</div>
</template>


<script lang="ts">
export default {
name:'Person', // 组件名
data() {
return {
name:"张三",
age:18,
tel:138888888
}
},
methods: {
showTel(){
alert(this.tel);
}
}
}
</script>

<style></style>

以上是vue2的写法,证明vue3可以对vue2向下兼容

写完后要注意在App.vue中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="title">Hello World</div>
<Person></Person>
</template>

<script lang="ts">
import Person from './components/Person.vue';
export default {
name:'App', // 组件名
components: {Person} // 注册组件
}
</script>

<style>
/* css样式 */
.title {
background-color: #ddd;
color: aqua;
text-align: center;
}
</style>

Vue2和Vue3的差异

出处:Bilibili-大帅老猿

OptionAPI

Vue2的API设计是Options(配置)风格的

缺点:

Options类型的API,数据、方法、计算属性等,是分散在:datamethodscomputed中的,若想新增或修改一个需求,就需要分别修改datamethodscomputed,不方便维护和复用

Composition Api

Vue3核心语法

setup

setupvue3中一个新的配置项,值是一个函数,它是Composition API“表演的舞台”,组件中所用到的:

  • 数据
  • 方法
  • 计算属性
  • 监视
  • ……

均配置在setup

特点:

  • setup函数返回的对象中的内容,可以直接在模板中使用
  • setup中访问thisundefined
  • setup函数会在beforeCreate之前调用,它是领先所有钩子执行的

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
</div>

<button @click="showTel">点击显示手机号码</button>
<button @click="changeName">点击修改姓名</button>
<button @click="changeAge">点击修改年龄</button>
</template>


<script lang="ts">
export default {
name:'Person', // 组件名
setup() {
// 数据
let name:string = "张三";
let age:number = 18;
let tel:string = "138xxxxxxxx";

// 方法
function showTel():void {
alert(tel);
}

function changeName():void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的
name = "李四";
}

function changeAge():void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的
age++;
}

return {name,age,showTel,changeName,changeAge}
}
}
</script>

set的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
...
</template>


<script lang="ts">
export default {
name:'Person', // 组件名
setup() {
...
...
...


return () => {
return "Hello World";
}
}
}
</script>

最终模板只会显示Hello World

setup、data、method

  1. setupdatamethod是可以同时存在的
  2. 由于setup是早于beforeCreate的,所以data中是可以读取到setup中的数据的,但是setup不能读取data中的数据

setup语法糖

在setup中创建数据、方法后,每次都需要手动return出去,相对繁琐,此时可以用到setup的语法糖

格式:

1
2
3
4
5
<script setup>
// 数据...

// 方法...
</script>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
</div>

<button @click="showTel">点击显示手机号码</button>
<button @click="changeName">点击修改姓名</button>
<button @click="changeAge">点击修改年龄</button>
</template>


<script lang="ts">
export default {
name: 'Person', // 组件名
}
</script>

<script lang="ts" setup> // 这里的语言要一致

// 数据
let name: string = "张三";
let age: number = 18;
let tel: string = "138xxxxxxxx";

// 方法
function showTel(): void {
alert(tel);
}

function changeName(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的
name = "李四";
}

function changeAge(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的
age++;
}

</script>

但是为了控制组件名而单独写一个script标签,又比较奇怪。

此时可以使用到一个插件:

1
npm i vite-plugin-vue-setup-extend -D

安装完插件后,找到根目录下的vite.config.ts引入插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { fileURLToPath, URL } from 'node:url'

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

// 引入的插件
import vueSetupExtend from 'vite-plugin-vue-setup-extend'

// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueSetupExtend() // 调用插件
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

使用方法:

在setup语法糖标签中,加入name="组件名"即可

1
2
3
<script lang="ts" setup name="MyPerson">
...
</script>

响应式数据

基本类型(Ref)

在vue2中,data中的数据就已经是响应式的了

但是在vue3中,要将数据处理成响应式,需要引入响应式方法

1
import ref from 'vue'

引入完成后,要将数据改成响应式数据,只需要将值放入到ref方法内

1
2
let name = ref("张三");
let age = ref(18);

需要注意的是,在模板中的值,并不需要改为name.value,vue会进行自动处理

但是,如果要在setup中对数据进行处理的话,就必须要加上name.value

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="person">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
</div>

<button @click="showTel">点击显示手机号码</button>
<button @click="changeName">点击修改姓名</button>
<button @click="changeAge">点击修改年龄</button>
</template>

<script lang="ts" setup name="MyPerson">
// 导入
import { ref } from 'vue';

// 数据
let name = ref("张三");
let age = ref(18);
let tel: string = "138xxxxxxxx";

console.log(`姓名为:${name.value}`);
console.log(`年龄为:${age.value}`);


// 方法
function showTel(): void {
alert(tel);
}

function changeName(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的
name.value = "李四";
}

function changeAge(): void { // 方法是生效的,但是页面并不会变,这是由于数据是非响应式的
age.value++;
}

</script>

对象类型数据(Reactive)

Ref只能对基本数据类型进行修改,如:string、number、boolean…

Reactive则可以对对象类型进行修改,如:Array、Function….

Ref一样,使用前需要先导入

1
import { reactive } from 'vue';

格式:

1
对象名 = reactive(对象数据)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<!-- 展示修改student对象 -->
<h1>我叫{{ student.name }},性别是{{ student.gender }},今年{{ student.age }}岁了</h1>
<button @click="alterGender">点击修改性别</button>
<button @click="grow">点击增加一岁</button>

<!-- 展示修改hobbies数组 -->
<h2>爱好:</h2>
<ul>
<li v-for="hobby in hobbies" :key="hobby">{{ hobby }}</li>
</ul>

<button @click="deleteHobby">删除爱好</button>
</template>

<script lang="ts" setup name="MyPerson">
import { reactive } from 'vue';

// 定义 student 对象
let student = reactive({
name:"张三",
age:18,
gender:"男"
})

// 定义 hobbies 数组
let hobbies = reactive(["打游戏","敲代码","看电影"]);


// 年龄增长方法
function grow() {
student.age++;
}

// 修改性别方法
function alterGender() {
student.gender = student.gender == "男" ? "女" : "男";
}

// 删除爱好方法
function deleteHobby() {
hobbies.pop();
}

</script>

注意

注意:Reactive只能定义对象类型的响应式数据

1
2
3
4
<script lang="ts" setup name="MyPerson">
import { reactive } from 'vue';
let name = reactive("张三"); // 类型“string”的参数不能赋给类型“object”的参数。
</script>

所以,Reactive是有局限性的

Ref在对象类型上的使用

ref也可以直接对对象进行使用:

1
let 对象名 = ref(对象值);

但是在对对象值进行处理时,需要先.value拿到值,再进行处理

1
2
3
function () {
对象名.value.对象属性 = 值;
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<!-- 展示修改student对象 -->
<h1>我叫{{ student.name }},性别是{{ student.gender }},今年{{ student.age }}岁了</h1>
<button @click="alterGender">点击修改性别</button>
<button @click="grow">点击增加一岁</button>

<!-- 展示修改hobbies数组 -->
<h2>爱好:</h2>
<ul>
<li v-for="hobby in hobbies" :key="hobby">{{ hobby }}</li>
</ul>

<button @click="deleteHobby">删除爱好</button>
</template>

<script lang="ts" setup name="MyPerson">
import { ref } from 'vue';

// 定义 student 对象
let student = ref({
name:"张三",
age:18,
gender:"男"
})

// 定义 hobbies 数组
let hobbies = ref(["打游戏","敲代码","看电影"]);


// 年龄增长方法
function grow() {
student.value.age++;
}

// 修改性别方法
function alterGender() {
student.value.gender = student.value.gender == "男" ? "女" : "男";
}

// 删除爱好方法
function deleteHobby() {
hobbies.value.pop();
}

</script>

Ref vs Reactive

宏观角度看:

  1. ref用来定义:基本类型数据、对象类型数据
  2. reactive用来定义:对象类型数据

区别:

  1. ref创建的变量必须使用.value(可以使用vscode中的插件自动添加.value)
  2. reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)

示例:

1
2
3
4
5
6
7
8
9
10
11
const person = reactive({name:"张三",age:20});

// 假设触发这个函数,页面是不更新的
function changePerson1() {
person = {name:"李四",age:21};
}

// 这个是可以正常使用的
function changePerson2() {
Object.assign(person,{name:"李四",age:21});
}

在利用ref的情况下实现以上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
let student = ref({
name:"张三",
age:18,
gender:"男"
})

function changeStudent() {
student.value = {
name:"李四",
age:21,
gender:"女"
}
}

使用原则:

  1. 若需要一个基本类型的响应式数据,必须使用ref
  2. 若需要一个响应式对象,层级不深,refreactive都可以
  3. 若需要一个响应式对象,且层级较深,推荐使用reactive

toRefs 和 toRef

  • 作用:将响应式对象中的每一个属性,转换为ref对象
  • 备注:toRefstoRef的功能一致,但是toRefs可以批量转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let student = ref({
name:"张三",
age:18,
gender:"男"
})

let name = student.value.name;

function changeStudent() {
student.value.name += '~';
// 点击三次后:
console.log(student.value.name); // 张三~~~
console.log(name); // 张三
}

上面说明了直接将响应式对象的值赋给一个变量,该变量的值并不会成为响应式

而使用toRefs后,效果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ref, toRefs } from 'vue';

let student = ref({
name:"张三",
age:18,
gender:"男"
})

// 将返回的对象结构赋值
let { name,age,gender } = toRefs(student.value);

function changeStudent() {
student.value.name += '~';
// 点击三次后:
console.log(student.value.name); // 张三~~~
console.log(name.value); // 张三~~~
}

并且,如果直接对name修改,也会导致student.value.name的被修改

1
2
3
4
5
6
function changeStudent() {
name.value += '~';
// 点击三次后:
console.log(student.value.name); // 张三~~~
console.log(name.value); // 张三~~~
}

计算属性(Computed)

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护

示例:

在不使用计算属性的情况下,完成如下需求:

  1. 根据输入框的内容,将全名显示出来
  2. 将全名中的首字母大写
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>姓:<input type="text" v-model="firstName"></div>
<div>名:<input type="text" v-model="lastName"></div>
<div>全名:{{ firstName.slice(0,1).toUpperCase() + firstName.slice(1) }} - {{ lastName }}</div>
</template>

<script lang="ts" setup name="MyPerson">
import { computed, ref } from 'vue';

let firstName = ref("zhang");
let lastName = ref("san")

</script>

使用computed后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>姓:<input type="text" v-model="firstName"></div>
<div>名:<input type="text" v-model="lastName"></div>
<div>全名:{{ fullName }}</div>
</template>

<script lang="ts" setup name="MyPerson">
import { computed, ref } from 'vue';

let firstName = ref("zhang");
let lastName = ref("san")

let fullName = computed(() => {
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value;
})

</script>

计算属性缓存

计算属性是带有缓存的

示例:

调用计算属性的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>姓:<input type="text" v-model="firstName"></div>
<div>名:<input type="text" v-model="lastName"></div>
<div>全名:{{ fullName }}</div>
<div>全名:{{ fullName }}</div>
<div>全名:{{ fullName }}</div>
<div>全名:{{ fullName }}</div>
<div>全名:{{ fullName }}</div>
<!-- 调用fullName方法5次,控制台只会输出一次 1 -->
</template>

<script lang="ts" setup name="MyPerson">
import { computed, ref } from 'vue';

// 将全名的首字母大写
let firstName = ref("zhang");
let lastName = ref("san")

let fullName = computed(() => {
console.log(1);
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value;
})

</script>

自定义方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>姓:<input type="text" v-model="firstName"></div>
<div>名:<input type="text" v-model="lastName"></div>
<div>全名:{{ withOutComputed() }}</div>
<div>全名:{{ withOutComputed() }}</div>
<div>全名:{{ withOutComputed() }}</div>
<div>全名:{{ withOutComputed() }}</div>
<div>全名:{{ withOutComputed() }}</div>
<!-- 调用withOutComputed方法5次,控制台输出5次 1 -->
</template>

<script lang="ts" setup name="MyPerson">
import { computed, ref } from 'vue';

// 将全名的首字母大写
let firstName = ref("zhang");
let lastName = ref("san")

let withOutComputed = function() {
console.log(1);
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value;
}

</script>

可写计算属性

计算属性默认是只读的。当尝试修改一个计算属性时,会收到一个运行时警告。

1
2
3
4
5
6
7
8
let fullName = computed(() => {
console.log(1);
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value;
})

function alertComputed() {
fullName.value = 123; // 无法为“value”赋值,因为它是只读属性。
}

只在某些特殊场景中可能才需要用到“可写”的属性,可以通过同时提供 getter 和 setter 来创建:

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div>姓:<input type="text" v-model="firstName"></div>
<div>名:<input type="text" v-model="lastName"></div>
<div>全名:{{ fullName }}</div>

<button @click="alertComputed">修改fullName方法的值</button>
</template>

<script lang="ts" setup name="MyPerson">
import { computed, ref } from 'vue';

// 将全名的首字母大写
let firstName = ref("zhang");
let lastName = ref("san")

let fullName = computed({
get() {
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + "-" + lastName.value;
},
set(val) {
console.log(val); // li-si
let [str1,str2] = val.split("-");
console.log(str1,str2); // li-si
firstName.value = str1;
lastName.value = str2;
},
})


function alertComputed() {
fullName.value = "li-si";
}
</script>

监视(watch)

作用:监视数据的变化(和Vue2中的watch作用一致)

特点:Vue3中的watch只能监视以下四种数据:

  • ref定义的数据
  • reactive定义的数据
  • 函数返回一个值
  • 一个包含上述内容的数组

监视Ref基本类型数据

监视ref定义的基本数据类型,直接写数据名即可,监视的是其value值的改变

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h1>当前求和值为:{{ count }}</h1>
<button @click="addSum">将求和值+1</button>
</template>

<script lang="ts" setup name="MyPerson">
import { ref,watch } from 'vue';


let count = ref(0)

function addSum() {
count.value++;
}

// 监视
let watchCount = watch(count,(newVal,oldVal) => { // count不需要.value,因为监视的是ref
console.log("新的值:" + newVal + ";旧的值:" + oldVal); // 第一次点击,控制台返回:新的值:1;旧的值:0
});

</script>

上述例子中,watchCount这个函数只要被调用,就会把watch方法停止。

1
2
3
4
5
6
7
// 监视
let watchCount = watch(count,(newVal,oldVal) => {
console.log("新的值:" + newVal + ";旧的值:" + oldVal);
if(newVal === 10) {
watchCount(); // 点击到第10次,监视停止
}
});

监视Ref对象类型数据

监视ref定义的对象类型数据:直接写数据名,监视的是对象的地址值,若想监视对象内部的数据,要手动开启深度监视

注意:

  • 若修改的是ref定义的对象中的属性,newValoldVal都是新值,因为它们是同一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<h1>姓名:{{ person.name }}</h1>
<h1>年龄:{{ person.age }}</h1>
<h1>性别:{{ person.gender }}</h1>

<button @click="changeName">修改姓名</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
<button @click="changePerson">整体修改</button>
</template>

<script lang="ts" setup name="MyPerson">
import { ref,watch } from 'vue';

// 定义数据
let person = ref({
name:"张三",
age:19,
gender:"男"
})

// 定义方法
function changeName() {
person.value.name += "~";
}

function changeAge() {
person.value.age++;
}

function changeGender() {
person.value.gender = person.value.gender == "男" ? "女" : "男";
}

function changePerson() {
person.value = {
name:"李四",
age:20,
gender: "女"
}
}

// 监视
watch(person.value,(newVal,oldVal) => {
// 点击第一次修改姓名,返回:newVal:张三~,oldVal:张三~
console.log(`newVal:${newVal.name},oldVal:${oldVal.name}`);
})

</script>

上述例子中,watch监视的是person.value.name,其他值类似

  • 若修改整个ref定义的对象,newVal是新值,oldVal是旧值,因为不是同一个对象了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<h1>姓名:{{ person.name }}</h1>
<h1>年龄:{{ person.age }}</h1>
<h1>性别:{{ person.gender }}</h1>

<button @click="changeName">修改姓名</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
<button @click="changePerson">整体修改</button>
</template>

<script lang="ts" setup name="MyPerson">
import { ref,watch } from 'vue';

// 定义数据
let person = ref({
name:"张三",
age:19,
gender:"男"
})

// 定义方法
function changeName() {
person.value.name += "~";
}

function changeAge() {
person.value.age++;
}

function changeGender() {
person.value.gender = person.value.gender == "男" ? "女" : "男";
}

function changePerson() {
person.value = {
name:"李四",
age:20,
gender: "女"
}
}

// 监视
watch(person,(newVal,oldVal) => {
// 点击整体修改
console.log(`newVal.name:${newVal.name},oldVal.name:${oldVal.name}`);
// newVal.name:李四,oldVal.name:张三
console.log(`newVal.age${newVal.age},oldVal.age${oldVal.age}`);
// newVal.age20,oldVal.age19
console.log(`newVal.gender${newVal.gender},oldVal.gender${oldVal.gender}`);
// newVal.gender女,oldVal.gender男
})

</script>

上述例子中,watch监视的是整个person对象

根据上面两个例子,可以发现:

  • 如果watch的第一个参数是person这个整体,那么只有当整体发生变化时,才会被监视到
  • 而如果watch的第一参数是person.value,那么只有当person内部的值发生变化时,才会被监视,而修改整个person并不会被监视

此时可以给watch添加一个参数,表示深度监视

监视参数(深度监视等)

深度监视(deep)

格式:

1
watch(监视对象,回调函数,{deep:true});

示例:

1
2
3
4
5
6
watch(person,(newVal,oldVal) => {
// 点击整体修改
console.log(`newVal.name:${newVal.name},oldVal.name:${oldVal.name}`);
console.log(`newVal.age${newVal.age},oldVal.age${oldVal.age}`);
console.log(`newVal.gender${newVal.gender},oldVal.gender${oldVal.gender}`);
},{deep:true}) // person的值修改或person被整体修改,都会触发监视

立即监视(immediate)

格式:

1
watch(监视对象,回调函数,{immediate:true});

示例:

1
2
3
4
watch(person,(newVal,oldVal) => {
// 点击整体修改
console.log("newVal.name:" , newVal.name, ",oldVal.name:" , oldVal?.name);
},{immediate:true}) // 当开启immediate配置后,要获取oldVal中的具体值,需要加上 ?,表示可选属性

监视Reactive对象类型数据

和监视ref的区别在于,监视reactive对象类型数据:自动开启深度监视(deep)

即:隐式开启深度监视

就算将watch的深度监视参数手动改为false,也无法关闭

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<h1>姓名:{{ person.name }}</h1>
<h1>年龄:{{ person.age }}</h1>
<h1>性别:{{ person.gender }}</h1>

<button @click="changeName">修改姓名</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
<button @click="changePerson">整体修改</button>
</template>

<script lang="ts" setup name="MyPerson">
import { reactive,watch } from 'vue';

// 定义数据
let person = reactive({
name:"张三",
age:19,
gender:"男"
})

// 定义方法
function changeName() {
person.name += "~";
}

function changeAge() {
person.age++;
}

function changeGender() {
person.gender = person.gender == "男" ? "女" : "男";
}

function changePerson() {
Object.assign(person,{
name:"李四",
age:20,
gender: "女"
});
}

// 监视
// 点击修改姓名、年龄、性别、整体都会监视
watch(person,(newVal,oldVal) => {
console.log("newVal" , newVal, ",oldVal:" , oldVal);
})
</script>

监视响应式数据中的具体属性

基本类型

如果要监视ref或reactive中的某一个具体属性(基本类型),直接写成以下形式是错误的:

1
2
3
watch(person.name,(newVal,oldVal) => {
console.log("newVal" , newVal, ",oldVal:" , oldVal);
})

这是由于watch监视的对象是严格限制的,查看限制类型

解决方法:

写一个函数,并在函数中返回监视的对象

1
2
3
watch(()=>person.name,(newVal,oldVal) => {
console.log("newVal" , newVal, ",oldVal:" , oldVal);
})

对象类型

假设现在的Person为以下形式:

1
2
3
4
5
6
7
8
9
let person = reactive({
name:"张三",
age:19,
gender:"男",
address:{
province:"江苏",
city:"苏州"
}
})

如果要监视address里的属性,写成以下方式也是可以的

1
2
3
watch(person.address,(n,o) => {
console.log(n,o)
})

但是:

  1. 如果只修改address.province,是能被监视到的
  2. 如果修改address就不会被监视

解决方法也是将这个对象以函数的形式返回

1
2
3
watch(()=>person.address,(n,o) => {
console.log(n,o)
})

总结

不管是基本类型还是对象类型,都建议以函数的形式返回

监视多个类型

将多个类型包裹为一个数组传入到watch中就可以

1
2
3
watch([()=>person.address,()=>person.name],(n,o) => {
console.log(n,o)
})

WatchEffect

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数

watch对比watchEffect

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
  2. watch:需要明确指出监视的数据
  3. watchEffect:不用明确的指出监视的数据(函数中用到哪些属性,就监视哪些属性)

假设有多个响应式数据:

1
2
3
let a = ref(0);
let b = ref(0);
...

如果要同时监视以上的所有数据,用watch的方法:

1
2
3
4
5
6
7
8
9
10
11
watch([a,b,...],(val) => {
if (val.[0] > 10) {
console.log("a > 10");
}

if (val.[1] > 10) {
console.log("a > 10");
}

...
})

可以看出需要将被监视的数据逐个填写到形参中

而使用watchEffect方法,则是如下的样子:

1
2
3
4
5
6
7
8
9
10
11
watchEffect(() => {
if (a.value > 10) {
console.log("a > 10")
}

if (b.value > 10) {
console.log("a > 10")
}

...
})

标签的ref属性

作用:用于注册模板引用

  • 用在普通DOM标签上,获取的是DOM节点
  • 用在组件标签上,获取的是组件实例对象

普通DOM标签

假设Person.vue的内容如下:

1
2
3
4
5
6
7
8
9
10
<template>
<h2 id="title">Person.vue</h2>
<button @click="showLog">点击按钮</button>
</template>

<script lang="ts" setup name="Person">
function showLog() {
console.log(document.getElementById("title"));
}
</script>

效果是点击按钮控制台会输出模板中的的h2标签

但是在App.vue中,id的名称也是title

1
2
3
4
5
6
7
8
<template>
<h2 id="title">App.vue</h2>
<Person></Person>
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue';
</script>

那么点击按钮后,控制台输出的结果是

1
<h2 id="title">App.vue</h2>

这是由于id名称的冲突,谁先使用id就输出谁。

解决的办法就是用ref

示例:

Person.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2 ref="title">Person</h2>
<button @click="showLog">点击按钮</button>
</template>

<script lang="ts" setup name="Person">
import { ref } from 'vue';

// 创建一个title,用于存储ref标记的内容
let title = ref();

function showLog() {
console.log(title.value);
}
</script>

App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2 ref="title">App.vue</h2>
<button @click="showLog">点击按钮</button>
<Person></Person>
</template>

<script lang="ts" setup name="App">
import { ref } from 'vue';
import Person from './components/Person.vue';

let title = ref();
function showLog() {
console.log(title.value);
}
</script>

组件标签

如果将ref绑定在组件标签上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h2 ref="title">App.vue</h2>
<button @click="showLog">点击按钮</button>
<Person ref="componentPerson"></Person>
</template>

<script lang="ts" setup name="App">
import { ref } from 'vue';
import Person from './components/Person.vue';

let title = ref();

let componentPerson = ref();

function showLog() {
console.log(componentPerson.value);
}
</script>

那么输出的结果是:

1
Proxy(Object) {__v_skip: true}

但是并不能查看到有用的信息,这是由于vue的安全机制。父级不能随意查看子级的内容,如果子级允许父级查看,可以使用defineExpose(可以手动引入,不过最新的vue3已经自动引入了)

Person.vue中使用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<h2 ref="title">Person</h2>
<button @click="showLog">点击按钮</button>
</template>

<script lang="ts" setup name="Person">
import { ref } from 'vue';

let title = ref();
let a = ref(0);

function showLog() {
console.log(title.value);
}

defineExpose({title,a})
</script>

修改完后,输出的结果为:

1
Proxy(Object) {title: RefImpl, a: RefImpl, __v_skip: true}

Ts中的接口、泛型、自定义类型

接口

假设要将对象的属性类型限制,这时候就可以用到ts语法中的接口

src目录下创建一个types的目录,再创建一个index.ts文件,文件内容如下:

1
2
3
4
5
export interface PersonInter{
id:string;
name:string;
age:number;
}

随后引入到Person.vue

1
2
3
4
5
6
7
8
9
<template>

</template>

<script lang="ts" setup name="Person">
// 引入PersonInter前面一定要加type,说明这是一个规范接口
import {type PersonInter} from "@/types"
const person:PersonInter = {id:"aaaa0001",name:"张三",age:20}
</script>

泛型

假设要将数组中的对象的属性类型限制,这时候就可以用到ts语法中的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>

</template>

<script lang="ts" setup name="Person">
import {type PersonInter} from "@/types"

const personList:Array<PersonInter> = [
{id:"aaaa0001",name:"张三",age:20},
{id:"aaaa0002",name:"李四",age:22},
{id:"aaaa0003",name:"王五",age:18}
]
</script>

自定义类型

personList:Array<PersonInter>这样的写法,可读性较差,所以可以使用自定义类型来优化代码

首先在之前创建的index.ts中创建自定义类型:

1
2
3
4
5
6
7
export interface PersonInter{
id:string;
name:string;
age:number;
}

export type Persons = Array<PersonInter>

随后在Person.vue中导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>

</template>

<script lang="ts" setup name="Person">
import {type PersonInter,type Persons} from "@/types"

const personList:Persons = [
{id:"aaaa0001",name:"张三",age:20},
{id:"aaaa0002",name:"李四",age:22},
{id:"aaaa0003",name:"王五",age:18}
]
</script>

reactive的结合使用

如果在reactive中使用泛型、接口、自定义类型,可以写为以下形式(不推荐):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>

</template>

<script lang="ts" setup name="Person">
import {type PersonInter,type Persons} from "@/types"
import { reactive } from "vue";

const personList:Persons = reactive([
{id:"aaaa0001",name:"张三",age:20},
{id:"aaaa0002",name:"李四",age:22},
{id:"aaaa0003",name:"王五",age:18}
])
</script>

推荐的用法是把泛型应用到方法上:

1
2
3
4
5
6
7
8
9
10
<script lang="ts" setup name="Person">
import {type PersonInter,type Persons} from "@/types"
import { reactive } from "vue";

const personList = reactive<Persons>([
{id:"aaaa0001",name:"张三",age:20},
{id:"aaaa0002",name:"李四",age:22},
{id:"aaaa0003",name:"王五",age:18}
])
</script>

Props的引用

将父组件的内容发送给子组件,可以在子组件中使用defineProps方法用于接收

父组件:

1
2
3
4
5
6
7
<template>
<Person a="你好"/>
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue';
</script>

子组件:

1
2
3
4
5
6
7
<template>
<h2>{{ a }}</h2>
</template>

<script lang="ts" setup name="Person">
defineProps(["a"])
</script>

注意:defineProps中保存的是数组,且数组中的内容类型为字符串,字符串的内容是父组件中子组件标签的属性名称

由于数组的内容是字符串,所以不能在script标签中直接使用,但是在模板中可以直接使用。如果想要用变量保存a,则可以定义一个变量用于接收defineProps的返回值

示例:

1
2
3
4
5
6
7
8
9
10
<template>
<h2>{{ a }}</h2>
</template>

<script lang="ts" setup name="Person">
let x = defineProps(["a"]);

console.log(x);
console.log(x.a);
</script>

输出内容如下:

1
2
Proxy(Object) {a: '你好'}
你好

将对象、变量通过Props传递

示例:

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<Person a="你好" :sayHi :personList/>
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue';

import {type PersonInter,type Persons} from "@/types"
import { reactive } from "vue";

const sayHi = "你好"

const personList = reactive<Persons>([
{id:"aaaa0001",name:"张三",age:20},
{id:"aaaa0002",name:"李四",age:22},
{id:"aaaa0003",name:"王五",age:18}
])
</script>

需要注意的是,和之前直接传输字符串不同,由于这次传输的是变量、对象,此时需要在前面加上冒号表示动态数据

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<h2>{{ a }}</h2>
<h2>{{ sayHi }}</h2>
<h2>{{ personList }}</h2>
</template>

<script lang="ts" setup name="Person">
let x = defineProps(["a","sayHi","personList"]);

console.log(x);
console.log(x.a);
</script>

defineProps限制类型

为了防止父组件传输错误的类型,如<Person :personList="5"/>,这会导致v-for空循环五次,输出空内容

可以使用之前的泛型来约束传输类型,示例如下:

1
2
3
4
5
6
7
8
9
10
11
<template>
<ul>
<li v-for="list in personList" :key="list.id">姓名:{{ list.name }},年龄:{{ list.age }}</li>
</ul>
</template>

<script lang="ts" setup name="Person">
import {type Persons} from "@/types"

defineProps<{personList:Persons}>()
</script>

defineProps<{personList:Persons}>()的详细意思如下:

  1. <>表示是泛型
  2. {}表示接收的对象
  3. personList表示接收的对象是谁
  4. Persons表示约束类型

defineProps更多用法

除了限制类型,还有限制必要性以及指定默认值

限制必要性

当父组件没有传输内容,而子组件又接收了不存的内容,此时代码会报错,这时候可以使用ts中的?。它表示这是一个可选的内容,当父组件存在则接收,不存在则忽略。

示例:

1
2
3
4
5
6
7
8
9
10
11
<template>
<ul>
<li v-for="list in personList" :key="list.id">姓名:{{ list.name }},年龄:{{ list.age }}</li>
</ul>
</template>

<script lang="ts" setup name="Person">
import {type Persons} from "@/types"

defineProps<{personList?:Persons}>()
</script>

默认值

既然父组件没有传输内容,那么子组件就需要一个默认值来顶替父组件本应该传输的内容,这里可以用到vue的withDefault方法,该方法需要引入:

1
import { withDefaults } from "vue";

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<ul>
<li v-for="list in personList" :key="list.id">姓名:{{ list.name }},年龄:{{ list.age }}</li>
</ul>
</template>

<script lang="ts" setup name="Person">
import {type Persons} from "@/types"
import { withDefaults } from "vue";

withDefaults(defineProps<{personList?:Persons}>(),{
personList: () => [{id:"aaaa0000",name:"匿名",age:0}]
})
</script>

withDefaults说明:

  1. 第一个参数:表示接收的数据
  2. 第二个参数:当没有接收到数据时,使用第二个参数中的值,注意的是对象的值必须为函数的返回值

生命周期

vue2中,生命周期有四个阶段(创建、挂载、更新、销毁),每个阶段分前后两种,共以下8种:

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted
  5. beforeUpdate
  6. updated
  7. beforeDestroy
  8. destroyed

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
data() {
return {
message: 'Hello Vue!'
};
},
beforeCreate() {
console.log('beforeCreate: 组件实例被创建之前');
},
created() {
console.log('created: 组件实例创建完成');
},
beforeMount() {
console.log('beforeMount: 组件挂载到DOM之前');
},
mounted() {
console.log('mounted: 组件挂载到DOM之后');
},
beforeUpdate() {
console.log('beforeUpdate: 组件数据更新之前');
},
updated() {
console.log('updated: 组件数据更新之后');
},
beforeDestroy() {
console.log('beforeDestroy: 组件实例销毁之前');
},
destroyed() {
console.log('destroyed: 组件实例销毁之后');
}
};
</script>

vue3中,与vue2类似,依然保留了创建、挂载、更新和销毁四个阶段,但在细节上有所调整和优化。

下面是 Vue 3 的生命周期钩子函数列表:

  • setup:创建
  • onBeforeMount: 在挂载开始之前被调用,相关的渲染函数首次被调用。
  • onMounted: 实例挂载完成后被调用,此时 DOM 元素已经插入文档中。
  • onBeforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • onUpdated: 组件更新完成后被调用,此时 DOM 已经更新。
  • onBeforeUnmount: 在卸载组件之前调用。
  • onUnmounted: 组件卸载后调用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
// 创建
console.log('创建')
// 挂载前
onBeforeMount(()=>{
console.log('挂载前')
})
// 挂载完毕
onMounted(()=>{
console.log('挂载完毕')
})
// 更新前
onBeforeUpdate(()=>{
console.log('更新前')
})
// 更新完毕
onUpdated(()=>{
console.log('更新完毕')
})
// 卸载前
onBeforeUnmount(()=>{
console.log('卸载前')
})
// 卸载完毕
onUnmounted(()=>{
console.log('卸载完毕')
})
</script>

自定义Hooks

类似于封装函数(自己的理解来说的话是这样的)

示例:

不使用自定义Hooks案例

在不使用hook的情况下实现如下要求:

  1. 页面展示数字,默认值为0,点击按钮后+1
  2. 页面展示图片,无默认图片,点击按钮后添加一张图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<h2 id="showNum">{{ num }}</h2>
<button @click="sum">点击按钮数字+1</button>

<br>

<img v-for="(dog,index) in dogList" :src="dog" :key="index">
<br>
<button @click="getPicture">点击按钮获取图片</button>
</template>

<script lang="ts" setup name="Person">
import { ref } from 'vue';
import axios from 'axios';

// 定义数字
let num = ref(0)

// 定义图片数组
let dogList:any = ref([]);

// 定义数字增加函数
function sum() {
num.value++;
}

// 定义获取图片函数
async function getPicture() {
try {
let src = await axios.get('https://dog.ceo/api/breeds/image/random');
dogList.value.push(src.data.message);
} catch (error) {
alert(error)
}
}
</script>

<style scoped>
h2 {
color: aqua;
}

img {
width: 100px;
margin: 20px;
}
</style>

使用自定义Hooks案例

将以上的内容改为使用自定义hooks的方法如下:

首先在src目录下(与components目录同级),创建一个hooks文件夹,将内容拆分,涉及到图片的就创建一个名为useImg.ts的文件将相关内容剪切到该文件中,涉及到数字的就创建一个useNum.ts的文件将相关内容剪切到该文件中。

useImg.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ref } from 'vue';
import axios from 'axios';

export default function() {
// 定义图片数组
let dogList:any = ref([]);
// 定义获取图片函数
async function getPicture() {
try {
let src = await axios.get('https://dog.ceo/api/breeds/image/random');
dogList.value.push(src.data.message);
} catch (error) {
alert(error)
}
}

return {dogList,getPicture}
}

useNum.ts:

1
2
3
4
5
6
7
8
9
10
11
12
import { ref } from 'vue';

export default function() {
// 定义数字
let num = ref(0)
// 定义数字增加函数
function sum() {
num.value++;
}

return {num,sum}
}

注意:

  • 相关内容必须要包含在函数内,且函数需要暴露出去,由于是匿名函数,所以必须使用export default
  • 函数中将相关的东西要return出去

在原来的模板中使用自定义hooks的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<h2 id="showNum">{{ num }}</h2>
<button @click="sum">点击按钮数字+1</button>

<br>

<img v-for="(dog,index) in dogList" :src="dog" :key="index">
<br>
<button @click="getPicture">点击按钮获取图片</button>
</template>

<script lang="ts" setup name="Person">
import useImg from '@/hooks/useDog';
import useNum from '@/hooks/useNum';

const {dogList,getPicture} = useImg();
const {num,sum} = useNum();

</script>

<style scoped>
h2 {
color: aqua;
}

img {
width: 100px;
margin: 20px;
}
</style>

路由

路由是 URL 与视图组件之间的映射。程序员可以定义路由规则,让特定的 URL 映射到特定的视图组件。

以下是基础的路由实现实例:

  1. 首先在App.vue中创建基本的样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<template>
<div id="test">
<h2>路由测试页面</h2>
<!-- 导航栏 -->
<div id="navigate">
<a src="/home" >首页</a>
<a src="/news" >新闻</a>
<a src="/about">关于</a>
</div>

<!-- 展示区 -->
<div id="content">
之后组件展示的区域
</div>
</div>
</template>

<script lang="ts" setup name="App">

</script>

<style>
/* 页面总体样式 */
#test {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
color: #2c3e50;
background: linear-gradient(to bottom, #ece9e6, #ffffff);
padding: 30px;
border-radius: 10px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
max-width: 900px;
margin: 50px auto;
}

/* 标题样式 */
#test h2 {
font-size: 28px;
color: #2980b9;
margin-bottom: 25px;
}

/* 导航栏样式 */
#navigate {
margin-bottom: 25px;
display: flex;
justify-content: center;
gap: 15px;
}

#navigate a {
text-decoration: none;
color: #fff;
background-color: #3498db;
padding: 12px 25px;
border-radius: 20px;
transition: background-color 0.3s, transform 0.3s;
}

#navigate a:hover {
background-color: #1abc9c;
transform: scale(1.05);
}

#navigate a.active {
background-color: #1abc9c;
}

/* 展示区样式 */
#content {
font-size: 18px;
line-height: 1.8;
background-color: #ffffff;
padding: 25px;
border-radius: 10px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
</style>

以上代码主要包含了基本的框架,如页面的标题、导航栏以及展示区,并不涉及路由

  1. 创建相对应的模板,Home.vueNews.vueAbout.vue
1
2
3
4
5
<template>
<h1>Home组件页面</h1>
</template>

<script lang="ts" setup name="Home"></script>
  1. 创建路由文件夹,用于存放路由器。路径为src/router,在文件夹内创建路由器文件index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 引入createRouter
import { createRouter,createWebHistory } from 'vue-router'

// 引入需要展示的组件
import Home from '@/components/Home.vue'
import About from '@/components/About.vue'
import News from '@/components/News.vue'

// 创建路由器
const router = createRouter({
history:createWebHistory(), // 路由器的工作模式
routes:[
// 当路径为空时默认跳转到Home组件页面
{
path:'/',
component:Home
},
{
path:'/home',
component:Home
},
{
path:'/news',
component:News
},
{
path:'/about',
component:About
}
]
})

// 将路由器暴露出去
export default router

如果显示缺少路由模块,可以手动下载:

1
npm i vue-router

然后引入createRouter,createWebHistory,这两个的作用分别是创建路由器和设置路由器工作模式。

注意:如果不设置路由器工作模式代码会报错

  1. 最重要是要在main.ts中修改相关内容,确保正确的使用到了路由器
1
2
3
4
5
6
7
8
9
10
// 引入createApp用于创建应用
import { createApp } from 'vue'
// 引入App根组件
import App from './App.vue'

// 引入路由器
import router from './router'

// 创建App,并且使用路由器,再挂载
createApp(App).use(router).mount('#app')
  1. 此时只需要在App.vue文件中稍加修改就可以正确的引用路由了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<template>
<div id="test">
<h2>路由测试页面</h2>
<!-- 导航栏 -->
<div id="navigate">
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterLink to="/news" active-class="active">新闻</RouterLink>
<RouterLink to="/about" active-class="active">关于</RouterLink>
</div>

<!-- 展示区 -->
<div id="content">
<RouterView></RouterView>
</div>
</div>
</template>

<script lang="ts" setup name="App">
import { RouterView, RouterLink } from 'vue-router';
</script>

<style>
/* 页面总体样式 */
#test {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
color: #2c3e50;
background: linear-gradient(to bottom, #ece9e6, #ffffff);
padding: 30px;
border-radius: 10px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
max-width: 900px;
margin: 50px auto;
}

/* 标题样式 */
#test h2 {
font-size: 28px;
color: #2980b9;
margin-bottom: 25px;
}

/* 导航栏样式 */
#navigate {
margin-bottom: 25px;
display: flex;
justify-content: center;
gap: 15px;
}

#navigate a {
text-decoration: none;
color: #fff;
background-color: #3498db;
padding: 12px 25px;
border-radius: 20px;
transition: background-color 0.3s, transform 0.3s;
}

#navigate a:hover {
background-color: #1abc9c;
transform: scale(1.05);
}

#navigate a.active {
background-color: #1abc9c;
}

/* 展示区样式 */
#content {
font-size: 18px;
line-height: 1.8;
background-color: #ffffff;
padding: 25px;
border-radius: 10px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
}
</style>

<RouterLink>的作用

<RouterLink>组件用于创建导航链接,它允许用户在不同的路由之间进行切换。它的作用类似于 HTML 中的 <a>标签,但是具有更多的 Vue Router 特性。

主要功能:

  1. 导航链接:可以使用 to 属性指定目标路由的路径。当用户点击链接时,会导航到指定的路由。
1
<RouterLink to="/home">首页</RouterLink>
  1. 动态路由:支持动态生成路由链接,根据数据或条件生成不同的目标路径。
1
<RouterLink :to="{ name: 'user', params: { userId: 123 }}">用户123</RouterLink>
  1. 激活类名:可以通过 active-class 属性设置当前路由激活时应用的类名,方便样式定制。
1
<RouterLink to="/home" active-class="active">首页</RouterLink>
  1. 替换模式:使用 replace 属性进行导航时,不会向历史记录中添加新记录。
1
<RouterLink to="/home" replace>首页</RouterLink>

<RouterView>的作用

<RouterView>组件用于显示匹配的视图组件。它是 Vue Router 中的一个占位符,当路由匹配时,会在这个位置渲染对应的组件

主要功能:

  1. 视图渲染:根据当前路由,渲染相应的组件。当用户导航到不同路由时,<RouterView>会动态地替换显示的组件。
1
<RouterView></RouterView>
  1. 嵌套路由:支持嵌套使用<RouterView>,用于渲染子路由的组件。父级<RouterView>渲染父路由的组件,子级<RouterView>渲染子路由的组件。
1
2
<RouterView></RouterView>
<RouterView name="child"></RouterView>
  1. 路由过渡:可以与 Vue 的<transition>组件结合使用,实现视图切换的过渡动画。
1
2
3
<transition name="fade">
<RouterView></RouterView>
</transition>

关于路由的注意点

  1. 路由组件通常存放在pagesviews文件夹,一般组件通常存放在components文件夹
  2. 通过点击导航,视觉效果上“消失”了的路由组件,默认是被卸载掉的,需要的时候再去挂载

路由工作模式

历史模式(History Mode)

工作原理:利用浏览器的 history.pushState 和 history.replaceState 方法来管理路由。URL 中没有哈希(#)符号,路径看起来像正常的 URL。

优点:URL 更加美观且符合 SEO(搜索引擎优化)要求。

缺点:需要服务端配置支持,否则刷新页面时会出现 404 错误。

哈希模式(Hash Mode)

工作原理:利用 URL 的哈希(#)符号来模拟一个完整的 URL。当 URL 改变时,页面不会重新加载。

优点:不需要服务端配置,适用于所有浏览器,特别是在开发环境中很方便。

缺点:URL 中包含哈希符号,不太美观。

命名路由

在 Vue 3 中,路由命名是一种为路由定义名称的方式,以便在导航和动态路由匹配时更容易引用。这对于大型应用程序尤为有用,因为它可以让代码更加简洁和易读。

使用方法

  1. 定义命名路由: 在定义路由时,通过 name 属性为路由指定一个名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';
import News from './views/News.vue';
import About from './views/About.vue';

const routes = [
{ path: '/', component: Home, name: 'home' },
{ path: '/news', component: News, name: 'news' },
{ path: '/about', component: About, name: 'about' }
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;
  1. 使用命名路由进行导航: 在使用<RouterLink>组件进行导航时,可以通过 :to 属性指定目标路由的名称,而不是路径。
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<nav>
<RouterLink :to="{ name: 'home' }">首页</RouterLink>
<RouterLink :to="{ name: 'news' }">新闻</RouterLink>
<RouterLink :to="{ name: 'about' }">关于</RouterLink>
</nav>
<RouterView></RouterView>
</template>

<script lang="ts" setup>
import { RouterLink, RouterView } from 'vue-router';
</script>

路由嵌套

在 Vue 3 中,嵌套路由允许在父路由的基础上定义子路由,从而实现复杂的页面结构。这种设计使得开发多层次、多视图的单页应用变得更加容易和直观。

实例:

如果要求在之前代码的基础上,增加以下需求:

  1. 在新闻页面添加导航栏
  2. 导航栏下面添加不同的需要呈现的模板
  3. 模板内展示图片,且只用一个模板就能展示不同的图片

解决需求如下:

在News.vue中修改相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<template>
<h1>News组件页面</h1>
<div id="news_navigate">
<div>
<div v-for="(img, index) in imgList" :key="index">
<RouterLink to="/news/details" active-class="active">{{ img.title }}</RouterLink>
<br>
</div>
</div>
<div id="imgBorder"><RouterView></RouterView></div>
</div>


</template>

<script lang="ts" setup name="News">
import { reactive } from 'vue';

let imgList = reactive([
{ id: "aaaa001", url: 'https://images.dog.ceo/breeds/shiba/shiba-1.jpg', title: '狗狗图01' },
{ id: "aaaa002", url: 'https://images.dog.ceo/breeds/shiba/shiba-2.jpg', title: '狗狗图02' },
{ id: "aaaa003", url: 'https://images.dog.ceo/breeds/shiba/shiba-3.jpg', title: '狗狗图03' },
{ id: "aaaa004", url: 'https://images.dog.ceo/breeds/shiba/shiba-4.jpg', title: '狗狗图04' },
{ id: "aaaa005", url: 'https://images.dog.ceo/breeds/shiba/shiba-5.jpg', title: '狗狗图05' },
{ id: "aaaa006", url: 'https://images.dog.ceo/breeds/shiba/shiba-6.jpg', title: '狗狗图06' }
])
</script>

<style scoped>
#news_navigate {
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
}

a {
text-decoration: none;
color: #fff;
background-color: #3498db;
padding: 6px 9px;
border-radius: 7px;
transition: background-color 0.3s, transform 0.3s;
font-size: 12px;
}

a:hover {
background-color: #1abc9c;
transform: scale(1.05);
}

a.active {
background-color: #1abc9c;
}

#imgBorder {
width: 100px;
border: 1px black;
}
</style>

将相关的数据存放在一个列表中,并用v-for循环遍历展示出内容,由于内容中需要包含另外的路由模板,所以创建一个Details.vue的路由模板:

1
2
3
4
5
6
7
<template>
123123123
</template>

<script lang="ts" setup name="Details">

</script>

并在index.ts路由器文件中修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 引入createRouter
import { createRouter,createWebHistory } from 'vue-router'

// 引入需要展示的组件
import Home from '@/components/Home.vue'
import About from '@/components/About.vue'
import News from '@/components/News.vue'
import Details from '@/components/Details.vue'

// 创建路由器
const router = createRouter({
history:createWebHistory(), // 路由器的工作模式
routes:[
// 当路径为空时默认跳转到Home组件页面
{
path:'/',
component:Home
},
{
path:'/home',
component:Home
},
{
path:'/news',
component:News,
children:[
{
path:"details", // 子路由不需要加/
component:Details
}
]
},
{
path:'/about',
component:About
}
]
})

// 将路由器暴露出去
export default router

此时,路由嵌套就实现了,但是Details.vue中的内容只是固定死的,点击哪个按钮展示哪张图片的功能需要之后的路由传参

路由传参

query

发送query参数

只需要将路径改为query的格式即可:

query参数的格式一般为abc.com/test?a=1&b=2&c=3

1
<RouterLink :to="{path:'/xxx/xxx',query:{a:123,b:123}}">{{ img.title }}</RouterLink>

示例:

News.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<h1>News组件页面</h1>
<div id="news_navigate">
<div>
<div v-for="(img, index) in imgList" :key="index">
<RouterLink :to="{path:'/news/details',query:{url:img.url}}">{{ img.title }}</RouterLink>
<br>
</div>
</div>
<div id="imgBorder"><RouterView></RouterView></div>
</div>


</template>

<script lang="ts" setup name="News">
import { reactive } from 'vue';

let imgList = reactive([
{ id: "aaaa001", url: 'https://images.dog.ceo/breeds/shiba/shiba-1.jpg', title: '狗狗图01' },
{ id: "aaaa002", url: 'https://images.dog.ceo/breeds/shiba/shiba-2.jpg', title: '狗狗图02' },
{ id: "aaaa003", url: 'https://images.dog.ceo/breeds/shiba/shiba-3.jpg', title: '狗狗图03' },
{ id: "aaaa004", url: 'https://images.dog.ceo/breeds/shiba/shiba-4.jpg', title: '狗狗图04' },
{ id: "aaaa005", url: 'https://images.dog.ceo/breeds/shiba/shiba-5.jpg', title: '狗狗图05' },
{ id: "aaaa006", url: 'https://images.dog.ceo/breeds/shiba/shiba-6.jpg', title: '狗狗图06' }
])
</script>

<style scoped>
#news_navigate {
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
}

a {
text-decoration: none;
color: #fff;
background-color: #3498db;
padding: 6px 9px;
border-radius: 7px;
transition: background-color 0.3s, transform 0.3s;
font-size: 12px;
}

a:hover {
background-color: #1abc9c;
transform: scale(1.05);
}

#imgBorder {
width: 100px;
border: 1px black;
}
</style>

这里只修改了RouterLink的路径,因为它是发送query参数的

接收query参数

利用useRoute函数接收即可

示例:

而接收query则需要在Details.vue中修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div><img :src="url" alt=""></div>
</template>

<script lang="ts" setup name="Details">
import { useRoute } from 'vue-router';
import { computed } from 'vue'

// hooks
let route = useRoute();

const url = computed(() => route.query.url as string);
console.log(route.query); // 可以在控制台中看打印的内容,由于是传输的是query参数,所以接收到的也是query
</script>

<style scoped>
img {
width: 100px;
}
</style>

以上代码添加了一个hooks函数useRoute,这个函数可以接收query参数

params

params参数格式一般为abc.com/test/a/b/c,其中a、b、c为传输的参数而不是路径

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<h1>News组件页面</h1>
<div id="news_navigate">
<div>
<div v-for="(img, index) in imgList" :key="index">
<RouterLink :to="{name:'xiangxi',params:{url:img.url}}">{{ img.title }}</RouterLink>
<br>
</div>
</div>
<div id="imgBorder"><RouterView></RouterView></div>
</div>


</template>

<script lang="ts" setup name="News">
import { reactive } from 'vue';

let imgList = reactive([
{ id: "aaaa001", url: 'https://images.dog.ceo/breeds/shiba/shiba-1.jpg', title: '狗狗图01' },
{ id: "aaaa002", url: 'https://images.dog.ceo/breeds/shiba/shiba-2.jpg', title: '狗狗图02' },
{ id: "aaaa003", url: 'https://images.dog.ceo/breeds/shiba/shiba-3.jpg', title: '狗狗图03' },
{ id: "aaaa004", url: 'https://images.dog.ceo/breeds/shiba/shiba-4.jpg', title: '狗狗图04' },
{ id: "aaaa005", url: 'https://images.dog.ceo/breeds/shiba/shiba-5.jpg', title: '狗狗图05' },
{ id: "aaaa006", url: 'https://images.dog.ceo/breeds/shiba/shiba-6.jpg', title: '狗狗图06' }
])
</script>

<style scoped>
#news_navigate {
display: flex;
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
}

a {
text-decoration: none;
color: #fff;
background-color: #3498db;
padding: 6px 9px;
border-radius: 7px;
transition: background-color 0.3s, transform 0.3s;
font-size: 12px;
}

a:hover {
background-color: #1abc9c;
transform: scale(1.05);
}

#imgBorder {
width: 100px;
border: 1px black;
}
</style>

注意:

  1. 为了不让vue误认为路径中的是子路由路径,所以要到路由文件index.ts中修改
  2. 和query传参不一样,:to内不能使用path而是要使用name

index.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 引入createRouter
import { createRouter,createWebHistory } from 'vue-router'

// 引入需要展示的组件
import Home from '@/components/Home.vue'
import About from '@/components/About.vue'
import News from '@/components/News.vue'
import Details from '@/components/Details.vue'

// 创建路由器
const router = createRouter({
history:createWebHistory(), // 路由器的工作模式
routes:[
// 当路径为空时默认跳转到Home组件页面
{
path:'/',
component:Home
},
{
path:'/home',
component:Home
},
{
path:'/news',
component:News,
children:[
{
name:"xiangxi",
path:"details/:url", // 子路由不需要加/
component:Details
}
]
},
{
path:'/about',
component:About
}
]
})

// 将路由器暴露出去
export default router

在子路由中的路径中加上:xxx表示路径后的为参数而不是子路由路径,并且要给details路由命名

接收params也和接收query类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div><img :src="url" alt=""></div>
</template>

<script lang="ts" setup name="Details">
import { useRoute } from 'vue-router';
import { computed } from 'vue'

// hooks
let route = useRoute();

const url = computed(() => route.params.url as string);
console.log(route.query); // 可以在控制台中看打印的内容,由于是传输的是query参数,所以接收到的也是query
</script>

<style scoped>
img {
width: 100px;
}
</style>

注意:

  1. 传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path
  2. 传递params参数时,需要提前在规则中占位。

路由props配置

作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件)

写法一

第一种写法只适用于params参数。格式:props:true

在路由文件index.ts中修改子路由的参数:

1
2
3
4
5
6
{   
name:"xiangxi",
path:"details/:url",
component:Details,
props:true
}

然后路由对应的模板组件就可以简化成以下方式:

1
2
3
4
5
6
7
<template>
<div><img :src="url" alt=""></div>
</template>

<script lang="ts" setup name="Details">
defineProps(['url'])
</script>

对比之前没有使用props的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div><img :src="url" alt=""></div>
</template>

<script lang="ts" setup name="Details">
import { useRoute } from 'vue-router';
import { computed } from 'vue'

// hooks
let route = useRoute();

const url = computed(() => route.params.url as string);
</script>

写法二

第二种写法既可以是query也可以params,不过既然存在第一种更加简洁的写法,第二种则可以默认针对query参数

格式:

1
2
3
props(route) {
return route.query
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
path:'/news',
component:News,
children:[
{
name:"xiangxi",
path:"details",
component:Details,
props(route) {
return route.query
}
}
]
},

replace属性

作用:控制路由跳转时操作浏览器历史记录的模式

浏览器的历史记录有两种写入方式:分别为pushreplace

  • push是追加历史记录(默认值)
  • replace是替换当前记录

开启replace模式:

1
<RouterLink replace .....>XXX</RouterLink>

编程式导航

之前的案例,是通过点击链接,既<RouterLink>实现跳转。

编程式导航是一种在代码中通过编程方式进行路由跳转的方法,而不是依赖用户的操作来触发路由变化。它在许多场景下非常有用,以下是几个常见的使用场景:

  1. 表单提交后跳转
    • 用户提交表单后,开发者可能希望在表单验证和处理完成后,将用户重定向到一个新的页面。例如:用户注册成功后,跳转到欢迎页面。
  2. 登录验证
    • 检查用户是否登录,如果未登录,重定向到登录页面。例如:访问一个需要登录的页面时,如果用户未登录,则编程式导航到登录页面。
  3. 多步骤表单
    • 在多步骤表单中,根据用户的操作和输入,动态跳转到下一个或上一个步骤。例如:用户填写完第一步后,自动跳转到第二步。
  4. 错误处理
    • 在处理请求或操作时,如果发生错误,可以编程式导航到错误页面或显示错误信息。例如:用户访问一个不存在的页面时,跳转到404页面。
  5. 权限控制
    • 根据用户的权限,决定是否允许访问某个页面,如果没有权限,可以跳转到无权限提示页面或首页。例如:管理员权限页面的访问控制。
  6. 动态内容加载
    • 根据用户的选择动态加载内容并跳转到相应的页面。例如:选择一个分类后,跳转到该分类的详情页面。

在使用编程式导航之前,需要先引入useRouter

1
import { useRouter } from 'vue-router';

然后通过调用 useRouter() 这个方法,将当前应用的路由实例赋值给常量 router

1
const router = useRouter();

假设现在有一个按钮,按钮上绑定了一个showDogImg的方法,此时编程式导航的书写如下:

1
2
3
4
5
function showDogImg(img:ImgInter) {  // 这里如果是TS会报类型错误,可以用img:any或者接口规范类型
router.push( // 两种模式:一种是push,另一种就是replace
{path:'/news/details',query:{url:img.url}} // 括号内的内容和RouterLink中的to写法一样
)
}

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<h1>News组件页面</h1>
<div id="news_navigate">
<div>
<div v-for="(img, index) in imgList" :key="index">
<button @click="showDogImg(img)">点击按钮查看狗狗图</button>
<RouterLink :to="{path:'/news/details',query:{url:img.url}}">{{ img.title }}</RouterLink>
<br>
</div>
</div>
<div id="imgBorder"><RouterView></RouterView></div>
</div>


</template>

<script lang="ts" setup name="News">
import { reactive } from 'vue';
import { useRouter } from 'vue-router';

const router = useRouter();

let imgList = reactive([
...
])

interface ImgInter{
id:string,
url:string,
title:string
}

function showDogImg(img:ImgInter) { // 这里如果是TS会报类型错误,可以用img:any或者接口规范类型
router.push(
{path:'/news/details',query:{url:img.url}}
)
}
</script>

路由重定向

将指定的路径跳转到另一个路径

在之前编写路由器的时候,为了防止第一次输入网页不会自动跳转到首页,进行了如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const router = createRouter({
history:createWebHistory(), // 路由器的工作模式
routes:[
// 当路径为空时默认跳转到Home组件页面
{
path:'/',
component:Home
},
{
path:'/home',
component:Home
},
{
path:'/news',
component:News,
children:[
{
name:"xiangxi",
path:"details",
component:Details,
props(route) {
return route.query
}
}
]
},
{
path:'/about',
component:About
}
]
})

现在有了更好的解决方案,既路由重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 创建路由器
const router = createRouter({
history:createWebHistory(), // 路由器的工作模式
routes:[
// 当路径为空时默认跳转到Home组件页面
{
path:'/home',
component:Home
},
{
path:'/news',
component:News,
children:[
{
name:"xiangxi",
path:"details",
component:Details,
props(route) {
return route.query
}
}
]
},
{
path:'/about',
component:About
},
{
path:'/',
redirect:'/home'
}
]
})

Meta

在 Vue Router 中,meta 属性用于存储与路由相关的自定义数据。这些数据不会影响路由的实际行为,但可以在导航守卫、组件中访问和使用。这使得 meta 属性非常适合用于存储如权限信息、页面标题、布局选项等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: loginPage,
meta: { centered: false } // 决定是否采用居中样式
},
{
path: '/login',
component: loginPage,
meta: { centered: false }
},
{
path: '/home',
component: homePage,
meta: { centered: true },
children: [
{
path: 'camList',
component: camList,
},
{
path: '',
component: atHome,
},
{
path: 'atHome',
component: atHome,
},
{
path: 'monitorRealTime',
component: monitorRealTime,
}
],
}
]
});

Pinia

Pinia 是 Vue 的一个状态管理库,用于管理和共享应用程序中的全局状态。它是 Vuex 的替代方案,提供了更简单、更直观的 API 和更好的性能。Pinia 可以帮助开发人员在多个组件之间共享数据,同时保持代码的清晰和可维护性。

以下是 Pinia 的一些主要功能和特点:

  1. 简单的 API:Pinia 提供了直观易懂的 API,简化了状态管理的过程。
  2. 模块化:可以将状态分成不同的模块,每个模块独立管理自己的状态、动作和变更。
  3. TypeScript 支持:Pinia 对 TypeScript 有很好的支持,能够提供类型推断和类型安全。
  4. 插件支持:Pinia 支持插件,允许开发人员扩展其功能,例如持久化状态、日志记录等。
  5. SSR 支持:Pinia 支持服务端渲染,可以在服务端和客户端之间共享状态。

测试框架

在使用之前,先搭建好测试的框架:

App.vue:

1
2
3
4
5
6
7
8
9
<template>
<Count/>
<Talk/>
</template>

<script lang="ts" setup name="" >
import Count from './components/Count.vue';
import Talk from './components/Talk.vue';
</script>

以上代码表示还要创建两个组件CountTalk

Talk.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<ul v-for="(talk, index) in talkList" :key="index">
<li>{{ talk.content }}</li>
</ul>
</div>
<button @click="addPoem">添加诗句</button>
</template>

<script lang="ts" setup name="Talk">
import axios from 'axios';
import { nanoid } from 'nanoid';
import { reactive } from 'vue';

const url = 'https://v1.jinrishici.com/rensheng.txt';

let talkList = reactive([
{ id: "asdafdasfxz", content: '归志宁无五亩园,读书本意在元元。' },
{ id: "asdgituwsad", content: '人老去西风白发,蝶愁来明日黄花。' },
{ id: "sjiasdfjasa", content: '能令暂开霁,过是吾无求。' }
])

async function addPoem() {
let result = await axios.get('https://v1.jinrishici.com/rensheng.txt');
console.log(result);
let obj = {id:nanoid(),content:result.data};
talkList.push(obj);
}


</script>

Count.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
<h2>当前数字累计为:{{ sum }}</h2>
<select v-model.number="num">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add">点击增加数字</button>
<button @click="sub">点击减小数字</button>
</div>
</template>

<script lang="ts" setup name="Count" >
import { ref } from 'vue';
let sum = ref(0); // 累计数
let num = ref(1); // 选择数

// 增加数字方法
function add() {
sum.value += num.value;
}

// 减小数字方法
function sub() {
sum.value -= num.value;
}
</script>

样式可以自行设计

Pinia教程

第一步需要安装Pinia

1
npm i pinia

第二步在main.ts中修改代码

1
2
3
4
5
6
7
8
9
10
11
12
// 引入createApp用于创建应用
import { createApp } from 'vue'
// 引入App根组件
import App from './App.vue'
// 引入Pinia
import { createPinia } from 'pinia'

// 创建Pinia
const pinia = createPinia();

// 创建App,安装pinia,挂载
createApp(App).use(pinia).mount('#app')

验证是否安装成功需要在浏览器开发者模式中的vue devtools查看,如果有一个菠萝的logo就表明安装并且引入成功。

数据存储

将之前测试框架中,talk.vuetalkListcount.vuesum存储到pinia中。

首先在src目录下创建一个store的文件夹,并且在该文件夹下创建对应的talk.tscount.ts

talk.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineStore } from "pinia";

export const useTalkStore = defineStore('talkList',{
state() {
return {
talkList:[
{ id: "asdafdasfxz", content: '归志宁无五亩园,读书本意在元元。' },
{ id: "asdgituwsad", content: '人老去西风白发,蝶愁来明日黄花。' },
{ id: "sjiasdfjasa", content: '能令暂开霁,过是吾无求。' }
]
}
}
})

count.ts:

1
2
3
4
5
6
7
8
9
10
import { defineStore } from "pinia";

export const useCountStore = defineStore('count',{
// 真正存储数据的地方
state() {
return {
sum:0
}
}
})

根据以上案例,总结出以下步骤:

  1. 引入defineStore
  2. 使用defineStore,参数第一个表示名称,参数第二个表示配置
  3. 向外暴露存储的数据

使用数据

talk.vuecount.vue中引入暴露的数据

1
import { useTalkStore } from '@/store/talk';
1
import { useCountStore } from '@/store/count';

然后用变量接收数据:

1
const CountStore = useCountStore();
1
const TalkStore = useTalkStore();

使用数据:

1
CountStore.sum
1
TalkStore.talkList

完整案例如下:

Count.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>
<h2>当前数字累计为:{{ CountStore.sum }}</h2>
<select v-model.number="num">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="add">点击增加数字</button>
<button @click="sub">点击减小数字</button>
</div>
</template>

<script lang="ts" setup name="Count" >
import { ref } from 'vue';
import { useCountStore } from '@/store/count';

const CountStore = useCountStore();

let num = ref(1); // 选择数

// 增加数字方法
function add() {
CountStore.sum += num.value;
}

// 减小数字方法
function sub() {
CountStore.sum -= num.value;
}
</script>

Talk.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
<ul v-for="(talk, index) in TalkStore.talkList" :key="index">
<li>{{ talk.content }}</li>
</ul>
</div>
<button @click="addPoem">添加诗句</button>
</template>

<script lang="ts" setup name="Talk">
import axios from 'axios';
import { nanoid } from 'nanoid';
import { reactive } from 'vue';
import { useTalkStore } from '@/store/talk';

const url = 'https://v1.jinrishici.com/rensheng.txt';

const TalkStore = useTalkStore();

async function addPoem() {
let result = await axios.get('https://v1.jinrishici.com/rensheng.txt');
console.log(result);
let obj = {id:nanoid(),content:result.data};
TalkStore.talkList.push(obj);
}


</script>

修改数据

第一种方式

在之前的案例中,已经呈现了修改数据的第一种方式——直接调用修改:

1
2
3
4
5
6
7
8
9
10
const CountStore = useCountStore();
// 增加数字方法
function add() {
CountStore.sum += num.value;
}

// 减小数字方法
function sub() {
CountStore.sum -= num.value;
}
1
2
3
4
5
6
7
const TalkStore = useTalkStore();
async function addPoem() {
let result = await axios.get('https://v1.jinrishici.com/rensheng.txt');
console.log(result);
let obj = {id:nanoid(),content:result.data};
TalkStore.talkList.push(obj);
}

第二种方式

第二种方式通常用于批量修改数据:

1
XXX.$patch({})

为了展示后面修改数据的特殊,将store文件夹中的count.ts进行稍微的修改:

增加了两个数据nameage

1
2
3
4
5
6
7
8
9
10
11
12
import { defineStore } from "pinia";

export const useCountStore = defineStore('count',{
// 真正存储数据的地方
state() {
return {
sum:0,
name:'张三',
age:20
}
}
})

触发Count.vueadd()方法,修改存储的数据值

  • namezhangsan改为lisi
  • age改为15
  • sum改为99999

使用第二种方法:

1
2
3
4
5
6
7
8
9
const CountStore = useCountStore();
// 增加数字方法
function add() {
CountStore.$patch({
name:'lisi',
age:15,
sum:99999
})
}

第三种方式

第三种方式修改数据的位置在pinia的文件中,使用到了actions方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineStore } from "pinia";

export const useCountStore = defineStore('count',{
actions:{
change(value:number){
this.sum += value;
this.name = 'lisi';
this.age = 15
}
},
// 真正存储数据的地方
state() {
return {
sum:0,
name:'张三',
age:20
}
}
})

相当于存储数据的地方又存储了一个修改数据的方法

而使用该函数的方法就是和第一种方式直接调用一样:

1
2
3
4
5
const CountStore = useCountStore();
// 增加数字方法
function add() {
CountStore.change(num.value)
}

小结

根据以上的所有示例,可以总结出:

  1. pinia的功能就是存储全局变量,全局方法。(但全局变量是响应式的)
  2. 修改存储的数据,如果只是简单的修改,直接使用第一种方式,如果相对复杂,推荐使用第三种方式

小小的优化

在之前的案例中,每次想要获得数据,都需要用XXX.xxx,这样写逻辑上虽然没问题,但是观感相对较差。

在之前,解决的方法是利用解构赋值:

1
2
const CountStore = useCountStore();
const {sum,name,age} = CountStore;

但是问题就出现了,数据丢失了响应性,此前也有解决该问题的办法,就是使用toRefs方法

1
const {sum,name,age} = toRefs(CountStore);

这么修改,虽然问题解决了,数据又恢复了响应式,但是在控制台中查看toRefs(CountStore),发现CountStore中的所有内容全被添加上了响应式,即使不需要的内容也会被添加上响应式。

针对以上的问题,pinia有对应的办法:

引入storeToRefs

1
import { storeToRefs } from 'pinia';

使用storeToRefs

1
const {sum,name,age} = storeToRefs(CountStore);

总结:解构赋值时,对于pinia的数据要用storeToRefs保证不会丢失响应性

getters

piniagetters类似于vue中的computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { defineStore } from "pinia";

export const useCountStore = defineStore('count',{
actions:{
change(value:number){
this.sum += value;
this.name = 'lisi';
this.age = 15
}
},
// 真正存储数据的地方
state() {
return {
sum:0,
name:'张三',
age:20
}
},
getters:{
// 将数据放大10倍
bigSum():number {
return this.sum * 10;
}
}
})

使用:

1
<h3>数字放大10倍为:{{ CountStore.bigSum }}</h3>

subscribe

可以看作是vue中的watch

具体使用方法如下:

1
2
3
4
5
6
CountStore.$subscribe((mutate,state) => {
console.log("CountStore被修改了");
console.log(mutate);
console.log(state);
console.log("-------------------");
})

store的组合式写法

原来的选项式写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineStore } from "pinia";

export const useCountStore = defineStore('count',{
actions:{
change(value:number){
this.sum += value;
this.name = 'lisi';
this.age = 15
}
},
// 真正存储数据的地方
state() {
return {
sum:0,
name:'张三',
age:20
}
},
getters:{
bigSum():number {
return this.sum * 10;
}
}
})

修改成组合式写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineStore } from "pinia";
import { ref } from "vue";

export const useCountStore = defineStore('count',() => {
// 相当于state
let sum = ref(0);
let name = ref('张三');
let age = ref(20)

// 相当于actions
function change(value:number){
sum.value += value;
name.value = 'lisi';
age.value = 15
}

return {sum,name,age,change}
})

组件通信

在Vue3中,有多种方式可以用于组件之间的通信,每种方式都有其适用的场景和优缺点。以下是一些主要的组件通信方式:

  1. Props 和 Emit
    • Props:父组件通过props向子组件传递数据。
    • Emit:子组件通过emit向父组件发送事件或数据。
  2. Provide 和 Inject
    • Provide:祖先组件提供数据或方法。
    • Inject:后代组件注入并使用提供的数据或方法。适用于跨越多层组件的场景。
  3. 全局事件总线
    • 使用一个空的Vue实例作为中央事件总线,通过$emit$on在非父子关系的组件之间传递消息。
  4. Vuex
    • Vuex是Vue的状态管理库,用于管理全局状态。通过store,任何组件都可以访问和修改状态。
  5. Composition API
    • 使用组合式API中的refreactivecomputed等,可以在多个组件中共享状态和逻辑。例如,自定义hook(composables)。
  6. Pinia
    • Pinia是Vue3的状态管理库,作为Vuex的替代方案,提供了更简单和更高性能的状态管理。
  7. EventEmitter
    • 可以使用Node.js的EventEmitter或者其他第三方库来实现组件之间的事件通信。
  8. 通过浏览器的LocalStorage、SessionStorage或Cookies
    • 用于持久化和共享数据,不过这种方式主要用于在不同组件之间保留状态,而不是实时通信。
  9. URL查询参数或路由参数
    • 在路由中传递数据,适用于页面之间的通信。

Props 和 Emit

在Vue3中,父组件和子组件之间的通信主要通过Props和Emit来实现。以下是一个简单的示例,展示了如何使用Props和Emit在父子组件之间传递数据和事件

父组件示例(ParentComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<ChildComponent :message="parentMessage" @updateMessage="updateMessage"></ChildComponent>
<p>来自子组件的消息: {{ childMessage }}</p>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

const parentMessage = '这是来自父组件的消息';
const childMessage = ref('');

function updateMessage(newMessage: string) {
childMessage.value = newMessage;
}
</script>

子组件示例(ChildComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<p>父组件消息: {{ message }}</p>
<input v-model="inputMessage" placeholder="输入新的消息">
<button @click="sendMessage">发送消息给父组件</button>
</div>
</template>

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

// 定义Props
const props = defineProps<{
message: string;
}>();

// 定义Emits
const emit = defineEmits<{
(e: 'updateMessage', newMessage: string): void;
}>();

const inputMessage = ref('');

function sendMessage() {
emit('updateMessage', inputMessage.value);
}
</script>

解释

  1. 父组件(ParentComponent.vue)
    • 通过parentMessage传递数据到子组件的message属性。
    • 监听子组件的updateMessage事件,并使用updateMessage方法更新childMessage
  2. 子组件(ChildComponent.vue)
    • 使用defineProps定义message属性,接收来自父组件的消息。
    • 使用defineEmits定义updateMessage事件,以便将新的消息发送给父组件。
    • 用户在输入框输入新消息并点击按钮时,调用sendMessage方法,触发updateMessage事件并将新消息传递给父组件。

Mitt

Mitt 是一个轻量级的事件发射器,用于在 Vue 3 中实现组件之间的通信,特别适用于兄弟组件之间的通信

首先,安装 Mitt:

1
npm install mitt
  1. 创建事件总线
1
2
3
4
5
import mitt from 'mitt';

const emitter = mitt();

export default emitter;
  1. 在需要通信的组件中使用事件总线

发送事件的组件(SenderComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
<template>
<button @click="sendMessage">发送消息</button>
</template>

<script lang="ts" setup>
import emitter from '../eventBus';

function sendMessage() {
emitter.emit('customEvent', 'Hello from SenderComponent');
}
</script>

接收事件的组件(ReceiverComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<p>{{ message }}</p>
</template>

<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import emitter from '../eventBus';

const message = ref('');

function handleMessage(payload: string) {
message.value = payload;
}

onMounted(() => {
emitter.on('customEvent', handleMessage);
});

onUnmounted(() => {
emitter.off('customEvent', handleMessage);
});
</script>

解释

  1. 创建事件总线
    • 使用 Mitt 创建一个事件总线实例 emitter,并将其导出以供其他组件使用。
  2. 发送事件的组件
    • SenderComponent.vue 中,使用 emitter.emit 方法发送一个自定义事件 customEvent,并附带消息数据。
  3. 接收事件的组件
    • ReceiverComponent.vue 中,定义一个响应事件的处理函数 handleMessage,并在组件挂载时通过 emitter.on 监听 customEvent 事件。
    • 在组件卸载时,通过 emitter.off 移除事件监听,以避免内存泄漏。

v-model

v-model 是 Vue 的双向数据绑定指令,用于将数据在组件之间进行同步。

在Vue3中,v-model 可以用在父组件和子组件之间传递和同步数据。以下是一个简单的示例,展示了如何使用 v-model 进行父子组件通信。

父组件示例(ParentComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<ChildComponent v-model:message="parentMessage"></ChildComponent>
<p>来自子组件的消息: {{ parentMessage }}</p>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

const parentMessage = ref('这是来自父组件的消息');
</script>

子组件示例(ChildComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
<p>父组件消息: {{ modelValue }}</p>
<input v-model="inputMessage" placeholder="输入新的消息">
<button @click="updateMessage">发送消息给父组件</button>
</div>
</template>

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

// 定义Props
const props = defineProps<{
modelValue: string;
}>();

// 定义Emits
const emit = defineEmits<{
(e: 'update:modelValue', newMessage: string): void;
}>();

const inputMessage = ref(props.modelValue);

function updateMessage() {
emit('update:modelValue', inputMessage.value);
}
</script>

解释

  1. 父组件(ParentComponent.vue)
    • 使用 v-model:message 将父组件的 parentMessage 绑定到子组件的 modelValue 属性。
    • parentMessage 的变化将自动反映在子组件中,反之亦然。
  2. 子组件(ChildComponent.vue)
    • 使用 defineProps 定义 modelValue 属性,接收来自父组件的消息。
    • 使用 defineEmits 定义 update:modelValue 事件,以便将新的消息发送给父组件。
    • 用户在输入框中输入新消息并点击按钮时,调用 updateMessage 方法,触发 update:modelValue 事件并将新消息传递给父组件。

$attrs

$attrs 经常用于祖孙组件之间的通信,特别是当祖先组件需要将一些属性直接传递给孙组件,而中间的子组件不需要处理这些属性时。这样可以避免逐级传递 props,使代码更简洁。

祖先组件(GrandParentComponent.vue):

1
2
3
4
5
6
7
8
9
10
<template>
<div>
<ParentComponent :title="grandParentTitle"></ParentComponent>
</div>
</template>

<script lang="ts" setup>
import ParentComponent from './ParentComponent.vue';
const grandParentTitle = '这是来自祖先组件的标题';
</script>

父组件(ParentComponent.vue):

1
2
3
4
5
6
7
8
9
<template>
<div>
<ChildComponent v-bind="$attrs"></ChildComponent>
</div>
</template>

<script lang="ts" setup>
import ChildComponent from './ChildComponent.vue';
</script>

孙组件(ChildComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<p>{{ title }}</p>
</div>
</template>

<script lang="ts" setup>
import { defineProps } from 'vue';

const props = defineProps<{
title: string;
}>();
</script>

解释

  1. 祖先组件(GrandParentComponent.vue)
    • 定义一个属性 grandParentTitle 并将其传递给 ParentComponent
  2. 父组件(ParentComponent.vue)
    • 使用 v-bind="$attrs" 将所有未声明的属性传递给 ChildComponent
  3. 孙组件(ChildComponent.vue)
    • 使用 defineProps 接收 title 属性,并在模板中显示。

provide-inject

provideinject 是一种用于组件间传递依赖的机制,特别适用于跨越多层级组件的通信。这种机制允许祖先组件为其后代组件提供数据或方法,而不需要逐级传递props

provideinject 的作用

  1. provide
    • 祖先组件使用 provide 方法提供数据或方法,供后代组件使用。
    • 通常在组件的 setup 函数中调用 provide
  2. inject
    • 后代组件使用 inject 方法注入并使用祖先组件提供的数据或方法。
    • 通常在组件的 setup 函数中调用 inject

示例:

GrandParent.vue:

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<ParentComponent></ParentComponent>
</div>
</template>

<script lang="ts" setup>
import { provide } from 'vue';

const providedData = '这是来自祖先组件的数据';
provide('sharedData', providedData);
</script>

Parent.vue:

1
2
3
4
5
6
7
8
9
<template>
<div>
<ChildComponent></ChildComponent>
</div>
</template>

<script lang="ts" setup>
import ChildComponent from './ChildComponent.vue';
</script>

Child.vue:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<p>注入的数据: {{ sharedData }}</p>
</div>
</template>

<script lang="ts" setup>
import { inject } from 'vue';

const sharedData = inject<string>('sharedData', '默认数据');
</script>

插槽

默认插槽

默认插槽(Default Slot)是指在子组件中未指定名称的插槽内容。它允许父组件将内容插入到子组件的预定义位置,从而实现更灵活的组件内容配置。默认插槽是最基础也是最常用的插槽类型。

子组件(ChildComponent.vue):

1
2
3
4
5
6
7
8
9
<template>
<div>
<h2>我是子组件</h2>
<slot>默认插槽内容</slot>
</div>
</template>

<script lang="ts" setup>
</script>

父组件(ParentComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
<ChildComponent>
<p>这是插入到子组件中的内容1</p>
</ChildComponent>
<ChildComponent>
<p>这是插入到子组件中的内容2</p>
</ChildComponent>
</div>
</template>

<script lang="ts" setup>
import ChildComponent from './ChildComponent.vue';
</script>

解释

  1. 子组件
    • 使用 <slot> 标签定义插槽,表示这个位置的内容可以由父组件提供。
    • 如果父组件没有提供内容,则会显示 slot 标签内的默认内容(例如 “默认插槽内容”)。
  2. 父组件
    • 使用子组件 <ChildComponent> 时,在其标签内放置需要插入的内容(例如 <p>这是插入到子组件中的内容</p>)。
    • 这些内容将被插入到子组件的 <slot> 标签位置,替换默认内容。

具名插槽

具名插槽(Named Slots)是Vue中一种更高级的插槽类型,它允许为插槽指定一个名称,从而在父组件中可以更精确地将内容插入到子组件的特定位置。

子组件(ChildComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<header>
<slot name="header">默认头部内容</slot>
</header>
<main>
<slot>默认主体内容</slot>
</main>
<footer>
<slot name="footer">默认尾部内容</slot>
</footer>
</div>
</template>

<script lang="ts" setup>
</script>

父组件(ParentComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<ChildComponent>
<template #header>
<h1>这是插入到头部的内容</h1>
</template>
<template #default>
<p>这是插入到主体的内容</p>
</template>
<template #footer>
<p>这是插入到尾部的内容</p>
</template>
</ChildComponent>
</div>
</template>

<script lang="ts" setup>
import ChildComponent from './ChildComponent.vue';
</script>

解释

  1. 子组件(ChildComponent.vue)
    • 使用 <slot name="header"><slot name="footer"> 定义具名插槽,允许父组件插入特定内容到这些位置。
    • 使用 <slot> 定义默认插槽,允许父组件插入默认主体内容。
  2. 父组件(ParentComponent.vue)
    • 使用 <template #header> 插入内容到子组件的头部插槽。
    • 使用 <template #default> 插入内容到子组件的默认插槽。
    • 使用 <template #footer> 插入内容到子组件的尾部插槽。

作用域插槽

作用域插槽(Scoped Slots)是Vue中的一种强大功能,允许子组件将数据传递回父组件,从而使父组件能够在插槽内容中使用这些数据。与普通插槽不同,作用域插槽不仅仅是内容插入,还能实现数据共享和逻辑解耦。

示例

假设有一个列表组件(ListComponent),它接收一个数组并通过插槽渲染每个项。

子组件(ListComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<slot :items="items"></slot>
</div>
</template>

<script lang="ts" setup>
import { defineProps } from 'vue';

const props = defineProps<{
items: Array<{ id: number; name: string; }>;
}>();
</script>

父组件(ParentComponent.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<ListComponent :items="items">
<template #default="{ items }">
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
</ListComponent>
</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import ListComponent from './ListComponent.vue';

const items = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橘子' }
]);
</script>

解释

  1. 子组件(ListComponent.vue)
    • 接收一个 items 数组作为 props
    • 使用 <slot :items="items"></slot> 传递 items 数据给插槽内容。这里的 :items="items" 是为插槽提供的数据,父组件可以通过插槽接收这些数据。
  2. 父组件(ParentComponent.vue)
    • 使用 ListComponent 并传递 items 数据。
    • 使用作用域插槽 template #default="{ items }" 接收来自子组件的数据,并在插槽内容中使用这些数据。
    • 在插槽内容中,通过 v-for 循环渲染列表项。

优势

  1. 灵活性:父组件可以完全控制如何渲染插槽内容,同时还可以使用子组件提供的数据。
  2. 解耦:子组件不需要关心插槽内容的具体实现,只需提供数据即可。
  3. 重用性:同一个子组件可以在不同的父组件中以不同的方式渲染插槽内容,从而提高了组件的重用性。

重要的API

shallowRef与shallowReactive

shallowRefshallowReactive 是 Vue 3 中用于创建浅层响应式状态的方法,它们与 refreactive 有些类似,但有一些关键区别。

shallowRefshallowReactive 的作用

  1. shallowRef
    • 创建一个浅层响应的引用。当改变其值时,Vue 会追踪这个改变,但对其内部对象的变动不会进行深层次的响应式追踪。
  2. shallowReactive
    • 创建一个浅层响应的对象。这个对象的顶层属性会是响应式的,但内部嵌套对象的属性不会进行深层次的响应式追踪。

示例

shallowRef 示例

1
2
3
4
5
6
7
8
9
import { shallowRef } from 'vue';

const shallowValue = shallowRef({ name: 'Alice' });

// 更新整个对象是响应式的
shallowValue.value = { name: 'Bob' };

// 内部属性变化不是响应式的
shallowValue.value.name = 'Charlie';

shallowReactive 示例:

1
2
3
4
5
6
7
8
9
import { shallowReactive } from 'vue';

const shallowObj = shallowReactive({ name: 'Alice', info: { age: 25 } });

// 顶层属性变化是响应式的
shallowObj.name = 'Bob';

// 内部嵌套对象属性变化不是响应式的
shallowObj.info.age = 26;

区别

  1. 深度响应式 vs 浅层响应式
    • refreactive 会进行深层响应式追踪,意味着它们不仅仅追踪顶层属性的变化,还会追踪嵌套对象属性的变化。
    • shallowRefshallowReactive 只会追踪顶层属性的变化,不会对嵌套对象属性的变化进行响应式处理。
  2. 使用场景
    • refreactive 适用于需要深度响应式的数据结构,适用于大多数常见场景。
    • shallowRefshallowReactive 适用于不需要深层响应式的情况,可以提高性能和减少不必要的响应式开销。

readonly与shallowReadonly

readonly:当使用 readonly 时,它会使整个对象成为只读的。意味不能对对象的任何属性进行修改,包括添加、删除或更改属性。这对于确保对象在整个生命周期内保持不变非常有用。

示例:

1
2
3
4
5
6
7
8
9
10
import { readonly } from 'vue';

const obj = readonly({
name: 'John',
age: 30
});

// obj.name = 'Jane'; // 这会抛出错误
// obj.age = 31; // 这会抛出错误
// obj.newProperty = 'value'; // 这会抛出错误

shallowReadonly:当使用 shallowReadonly 时,它会使对象的顶层属性成为只读的,但不会阻止嵌套对象的修改。意味着可以修改嵌套对象的属性,但不能修改顶层对象的属性。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { shallowReadonly } from 'vue';

const obj = shallowReadonly({
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
});

// obj.name = 'Jane'; // 这会抛出错误
// obj.address.city = 'Los Angeles'; // 这会工作
// obj.newProperty = 'value'; // 这会抛出错误

toRaw与markRaw

toRaw 是一个内部方法,用于获取一个响应式对象的原始(非响应式)版本。这个方法通常在开发过程中不会直接使用,因为它会破坏响应式系统的功能。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
import { reactive, toRaw } from 'vue';

const state = reactive({
count: 0
});

const rawState = toRaw(state);

rawState.count = 10; // 修改原始状态的 count 属性

console.log(state.count); // 输出:0,因为响应式系统未检测到变化
console.log(rawState.count); // 输出:10,因为我们直接修改了原始状态

markRaw 是 Vue 3 中的一个方法,用于标记一个对象或属性为原始(非响应式)的。这意味着,通过 markRaw 标记的对象或属性将不再被 Vue 的响应式系统跟踪和观察变化。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
import { markRaw } from 'vue';

const state = {
count: 0
};

// 将 count 属性标记为原始的
markRaw(state.count);

state.count = 10; // 修改 count 属性

console.log(state.count); // 输出:10,但这个变化不会被响应式系统检测到

customRef

customRef 是 Vue 3 中的一个方法,用于创建一个自定义的响应式引用(Ref)。它允许在组件内部创建一个响应式引用,并且可以在组件外部访问和修改这个引用的值。

这个方法通常用于复杂的场景,例如需要在组件外部访问或修改组件内部的响应式状态。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<p>计数器值:{{ counter }}</p>
</div>
</template>

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

const internalCounter = ref(0);

const counter = customRef((get, set) => {
return {
get: () => internalCounter.value,
set: (newValue) => {
internalCounter.value = newValue;
}
};
});

function increment() {
counter.value++;
}

function decrement() {
counter.value--;
}
</script>

customReftracktrigger 是两个用于自定义响应式引用的方法。这些方法允许更精细地控制响应式系统的行为。

track

track 方法用于跟踪一个对象或属性的变化。当调用 track 时,Vue 的响应式系统会开始监控该对象或属性的变化。这对于需要在响应式系统中触发更新非常有用。

trigger

trigger 方法用于触发一个响应式对象的更新。当调用 trigger 时,Vue 的响应式系统会重新计算所有依赖于该对象或属性的组件。这对于需要手动触发更新非常有用。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<p>计数器值:{{ counter }}</p>
</div>
</template>

<script lang="ts" setup>
import { ref, customRef, track, trigger } from 'vue';

const internalCounter = ref(0);

const counter = customRef((get, set) => {
return {
get: () => internalCounter.value,
set: (newValue) => {
internalCounter.value = newValue;
trigger(internalCounter);
}
};
});

track(internalCounter);

function increment() {
counter.value++;
}

function decrement() {
counter.value--;
}
</script>

teleport

teleport 是一个用于将组件的内容插入到 DOM 的外部位置的指令。这个指令非常有用,因为它允许在组件内部定义内容,但将其渲染到页面的任何地方。

用法

teleport 的用法非常简单。你只需要在组件的模板中使用 <teleport> 标签,并指定一个目标元素的 to 属性,这个目标元素的 id 将被 teleport 使用。

示例:

1
2
3
4
5
6
7
<template>
<div>
<teleport to="body">
<div>这是被传送的内容</div>
</teleport>
</div>
</template>

正常情况下,这个组件是作为App.vue的子组件渲染到父组件的范围中

但是在这个示例中,<teleport to="body"> 将会将 div 元素插入到页面的 <body> 元素中。

优点

  • 灵活性:可以将组件的内容插入到页面的任何地方,而不需要担心组件的位置。
  • 简单:使用 teleport 只需要简单的标签和属性,非常方便。

注意事项

  • 目标元素:确保指定的目标元素存在,并且它的 id 是唯一的。
  • 组件渲染顺序teleport 插入的内容会在目标元素的子节点中,所以它的渲染顺序可能会影响页面的布局。

suspense

Suspense 是一个用于处理组件加载状态的功能。它允许在组件加载期间显示一个占位符,并在组件加载完成后自动替换为实际内容。这对于处理慢加载组件或异步数据非常有用。

不使用suspense

举例:

App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div id="fu">
<h2>我是父组件</h2>
<Child></Child>
</div>
</template>

<script lang="ts" setup name="" >

import Child from './components/Child.vue';

</script>

<style scoped>
#fu {
width: 500px;
height: 200px;
background-color: #ddd;
border-radius: 20px;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
padding: 20px;
font-family: Arial, sans-serif;
color: #333;
text-align: center;
display: flex; /* 添加 Flexbox */
flex-direction: column;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
</style>

Child.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<h2 id="zi">我是子组件</h2>
</template>

<script lang="ts" setup name="" >
import axios from 'axios';
let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json');
console.log(content);
</script>

<style scoped>
#zi {
width: 400px;
height: 100px;
background-color: #f0f0f0;
border-radius: 15px;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
padding: 20px;
font-family: Arial, sans-serif;
color: #333;
text-align: center;
}
</style>

解释:

  1. App.vue中,包含了子组件Child.vue
  2. Child.vue中,使用了Axios来异步获取数据

结果:

  1. 控制台可以正常输出数据
  2. 页面无法渲染出Child.vue

使用suspense

为了在App.vue中正常渲染子组件,可以利用suspense

修改后的App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div id="fu">
<h2>我是父组件</h2>
<Suspense>
<template v-slot:default>
<Child></Child>
</template>
<template v-slot:fallback>
加载中...
</template>
</Suspense>
</div>
</template>

<script lang="ts" setup name="" >
import { Suspense } from 'vue';
import Child from './components/Child.vue';
</script>

<style scoped>
#fu {
width: 500px;
height: 200px;
background-color: #ddd;
border-radius: 20px;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
padding: 20px;
font-family: Arial, sans-serif;
color: #333;
text-align: center;
display: flex; /* 添加 Flexbox */
flex-direction: column;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
</style>

解释:

  1. 引入suspense
1
import { Suspense } from 'vue';
  1. suspense包裹住要异步渲染的子组件
  2. 利用插槽将子组件标签包裹起来
  3. v-slot:default表示的是默认,用于展示子组件,v-slot:fallback表示在异步请求完成前的操作(信息)

全局API转移到应用对象

app.component

全局组件是指可以在应用的任何地方使用的组件,而不需要每次在本地引用。

步骤

  1. 定义组件: 创建一个新的 Vue 组件文件,例如 MyComponent.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <template>
    <div class="my-component">
    我是一个全局组件
    </div>
    </template>

    <script lang="ts" setup>
    </script>

    <style scoped>
    .my-component {
    color: blue;
    }
    </style>
  2. 注册全局组件: 在你的 main.ts 文件中,将这个组件注册为全局组件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { createApp } from 'vue';
    import App from './App.vue';
    import MyComponent from './components/MyComponent.vue'; // 导入组件

    const app = createApp(App);

    app.component('MyComponent', MyComponent); // 注册全局组件

    app.mount('#app');
  3. 使用全局组件: 现在就可以在应用的任何地方使用这个全局组件,而不需要在本地引用。例如,在 App.vue 中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <template>
    <div id="app">
    <h1>欢迎使用Vue 3</h1>
    <MyComponent></MyComponent> <!-- 使用全局组件 -->
    </div>
    </template>

    <script lang="ts" setup>
    </script>

    <style scoped>
    #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
    }
    </style>

app.config

app.config 是一个用于配置 Vue 应用的对象。通过 app.config,你可以设置一些全局配置项,来影响整个应用的行为。下面是一些常见的 app.config 选项及其使用方法:

常见的 app.config 选项

  1. app.config.globalProperties: 这个选项用于在全局属性中注册自定义的全局方法或变量,以便在整个应用中访问。
  2. app.config.errorHandler: 自定义全局错误处理器,以处理应用中的错误。
  3. app.config.warnHandler: 自定义全局警告处理器,以处理 Vue 产生的警告。
  4. app.config.isCustomElement: 自定义判断某个标签是否是自定义元素。

使用示例

下面是一个示例,展示如何使用这些配置选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 设置全局属性
app.config.globalProperties.$myGlobalMethod = () => {
console.log('这是一个全局方法');
};

// 自定义全局错误处理器
app.config.errorHandler = (err, instance, info) => {
console.error('全局错误处理器:', err, instance, info);
};

// 自定义全局警告处理器
app.config.warnHandler = (msg, instance, trace) => {
console.warn('全局警告处理器:', msg, instance, trace);
};

// 判断某个标签是否是自定义元素
app.config.isCustomElement = (tag) => {
return tag.startsWith('my-');
};

app.mount('#app');

解释

  1. app.config.globalProperties: 在全局属性中注册一个自定义方法 $myGlobalMethod,可以在任何组件中通过 this.$myGlobalMethod() 访问这个方法。
  2. app.config.errorHandler: 设置一个全局错误处理器,当应用中发生错误时,会调用这个处理器,输出错误信息。
  3. app.config.warnHandler: 设置一个全局警告处理器,当 Vue 产生警告时,会调用这个处理器,输出警告信息。
  4. app.config.isCustomElement: 设置一个判断函数,用于判断某个标签是否是自定义元素。在这个示例中,所有以 my- 开头的标签都会被认为是自定义元素。

app.directive

app.directive 方法来定义和注册自定义指令。自定义指令允许在 DOM 元素上应用自定义行为,这些指令可以在应用的任何地方使用,非常强大和灵活。

使用步骤

  1. 定义自定义指令: 创建一个自定义指令对象,并定义它的钩子函数,如 mountedupdated 等。
  2. 注册全局自定义指令: 在 main.ts 文件中使用 app.directive 方法将自定义指令注册为全局指令。
  3. 在模板中使用自定义指令: 在组件的模板中应用自定义指令。

示例

创建一个简单的自定义指令 v-focus,它会在元素挂载时自动获得焦点。

定义和注册自定义指令

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 定义自定义指令
const vFocus = {
mounted(el: HTMLElement) {
el.focus();
}
};

// 注册全局自定义指令
app.directive('focus', vFocus);

app.mount('#app');

在模板中使用自定义指令

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div id="app">
<h1>欢迎使用Vue 3</h1>
<input v-focus placeholder="自动获得焦点的输入框">
</div>
</template>

<script lang="ts" setup>
</script>

<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

解释

  1. 定义自定义指令: 在 main.ts 文件中创建一个自定义指令对象 vFocus,并定义 mounted 钩子函数,让元素在挂载时自动获得焦点。
  2. 注册全局自定义指令: 使用 app.directive('focus', vFocus)vFocus 注册为全局自定义指令 v-focus
  3. 在模板中使用自定义指令: 在 App.vue 的模板中,使用 v-focus 自定义指令,使得输入框在页面加载时自动获得焦点。

非兼容性改变

来自:非兼容性改变 | Vue 3 迁移指南

Vue 3 引入了一些重要的非兼容性变化,这些变化在从 Vue 2 迁移时需要特别注意。以下是一些主要的非兼容性变化:

  1. 全局 API 变化
    • 全局 Vue API 现在使用应用实例来调用,而不是全局对象。
    • 全局和内部 API 被重新构建,以支持 tree-shaking。
  2. 模板指令变化
    • v-model 在组件中的使用方式被重新设计,现在使用 v-bind.sync 替代。
    • <template v-for> 和非 v-for 节点的 key 使用方式有所变化。
    • v-ifv-for 在同一个元素上使用时的优先级发生了变化。
    • v-bind="{object}" 现在是顺序敏感的。
    • v-on:event.native 修饰符被移除。
  3. 组件变化
    • 函数型组件只能通过普通函数来创建。
    • 单文件组件(SFC)中的 <template>functional 属性已被弃用。
    • 非同步组件现在需要使用 defineAsyncComponent 方法来创建。
    • 组件事件现在应该通过 emits 选项来声明。
  4. 渲染函数变化
    • 渲染函数 API 发生了变化。
    • $scopedSlots 属性被移除,所有的 slots 都通过 $slots 作为函数公开。
    • $listeners 被移除并合并到 $attrs
    • $attrs 现在包含 classstyle 属性。
  5. 自定义元素变化
    • 自定义元素检查现在在模板编译时进行。
    • 特殊的 is 属性仅限于 <component> 标签。
  6. 其他小变化
    • destroyed 生命周期选项被重命名为 unmounted
    • beforeDestroy 生命周期选项被重命名为 beforeUnmount
    • props 的默认工厂函数不再有 this 上下文。
    • 自定义指令 API 被调整以与组件生命周期一致。
    • data 选项应始终声明为函数。
    • mixinsdata 选项现在浅合并。
    • 属性强制转换策略发生了变化。
    • 一些过渡类名被重命名。
    • <TransitionGroup> 默认不再渲染包装元素。
    • 监控数组时,回调只在数组被替换时触发。