Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

学了React,再学个VUE试试

VUE简介

采用mvvm架构设计的前端框架

  1. View:视图层(UI
  2. ViewModel :业务逻辑层(一切JS
  3. Model:数据层(存储数据及对数据处理

==渐进式==学习框架,即不仅支持使用VUE进行项目构建,还支持某一功能的VUE构建,甚至某一HTML页面的VUE构建

VUE的使用方式有如下几种

  • 无需构建步骤,渐进式增强静态HTML
  • 在任何页面中作为Web Components嵌入
  • 单页应用(SPA)
  • 全栈/服务端渲染(SSR)
  • Jamstack/静态站点生成(SSG)
  • 开发桌面端、移动端、WebGL,甚至是命令行终端中的界面

Vue2 VS Vue3

Vue2采用OptionsAPI,代码较为分散

Vue3采用CompositionAPI,使得代码分明

Vue3新特性

  1. 重写了双向数据绑定
    • 2中使用Object.defineProperty实现
    • 3中使用ES6标准中的Proxy劫持
    • 因为Proxy能够更好的处理数组
  2. VDOM性能瓶颈得到提升
    • 使用patch flag做静态标记,使得对比时不会进行全量对比,从而提升性能
  3. 支持Fragments
    • template中可以写多个节点了
    • 支持TSX和JSX的写法
    • 增加Suspense和teleport
    • 增加多v-model
  4. 支持Tree-Shaking
  5. Composition API
    • Setup语法糖式编程

环境配置

nodejs

nvm(nodejs环境管理工具)

vite

官方文档

https://cn.vuejs.org/

VSCode支持

  1. volar
    1. Vue Language Features
    2. TypeScript Vue Plugin

需要注意的是Vue2的代码提示Vetur与Volar存在冲突,需要禁用

Vue

Vue API风格

  • 选项式 API(Vue2)
  • 组合式 API(Vue3)

选项式API

使用包含多个选项的对象来描述组件的逻辑。

例如data,methods,mounted.选项所定义的属性都会暴露在函数内部的this上,它会指向当前的组件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setip>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log(`The initial count is ${count.value}.`)
}
}
</script>
<templare>
<button @click="increment">
Count is : {{ count }}
</button>
</templare>

组合式API

通过组合式API,可以使用导入的API函数来描述组件逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<templare>
<button @click="increment">
Count is : {{ count }}
</button>
</templare>

API风格选择

  • 当不需要使用构建工具,或者打算在低复杂度的场景使用Vue时,例如渐进增强的应用场景,采用==选项式API==
  • 当打算用Vue构建完整的单页应用时,推荐采用==组合式API==+单文件组件

Vue项目结构

使用如下命令创建Vue项目时可以很方便的选择为自己的项目添加哪些组件:

1
npm init vue@latest

例如添加ESLint、Vue Router、Prettier、对TS的支持、对TSX、JSX的支持等等

上述命令将会安装并执行create-vue命令

随后创建的目录包含以下结构:

1
2
3
4
5
6
7
8
9
10
11
--public //用于存放静态资源,例如浏览器图标,且不会被Vite编译
--src //同样用于存放静态资源
--components //存放组件
App.vue //Vue的全局入口
main.ts //全局TS文件
style.css //脚手架自带样式文件
vite-env.d.ts //声明文件扩充,例如为TS做.vue的文件声明扩充
index.html //vite与webpack不同,使用html作为入口文件,使用es module的形式
package.json //项目描述文件,项目名,版本信息,依赖版本,开发环境版本依赖
tsconfig.json //ts配置文件
vite.vonfig.ts //vite配置文件

使用npm install命令安装好依赖之后,使用npm run dev运行即可。

运行命令会首先再当前目录的node_module中寻找是否安装了相应命令软件,如果没有回去全局node_module中继续寻找、如果还是没有则会继续去环境变量中寻找,如果再没有则会报错。

Vue模板语法

Vue使用基于HTML的模板语法,弄个声明式地将其组件实例的数据绑定到呈现的DOM上。所有的Vue模板都是==语法层面合法的HTML==,可以呗符合规范的浏览器和HTML解析器解析

文本插值

最基本的数据绑定形式,使用“Mustache”语法(双大括号)

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

<script>
export default {
data() {
return {
msg:"神奇海螺"
}
}
}
</script>

使用JS表达式

每个绑定仅支持单一表达式,即能够被求值的JS代码,一个简单的判断方法是是否可以合法地写在return后面。

需要注意的是,不符合if…else这样的多行表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<p>
{{ number + 1 }}
</p>
<p>
{{ ok ? 'YES' : 'NO' }}
</p>
<p>
{{ message.split('').reverse().join('') }}
</p>
</template>

<script>
export default {
data() {
return {
number: 10,
ok: true,
msg:"神奇海螺"
}
}
}
</script>

原始HTML

如果想要使用这样的方式直接插入==可被渲染的HTML==,则需要使用v-html指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<p>
纯文本: {{ rawHTML }}
</p>
<p>
属性: <span v-html="rawHTML"></span>
</p>
</template>

<script>
export default {
data() {
return {
rawHTML: "<a href='https://baidu.com'> 百度</a>"
}
}
}
</script>

属性绑定

属性需要使用v-bind指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div v-bind:id="dynamicId" v-bind:class="msg">
测试
</div>
</template>

<script>
export default {
data() {
return {
msg:"active",
dynamicId:"appID"
}
}
}
</script>

如果绑定的元素值为null或者undefined,那么该属性将从渲染的元素上移除

此外由于v-bind比较常用,Vue提供了:语法糖,上面的v-bind可以直接省略v-bind写作:

1
<div :id="dynamicId" :class="msg">测试</div>

动态绑定多个属性

可以以直接使用JS对象对属性进行数据绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div v-bind="objectOfAttrs">
测试
</div>
</template>

<script>
export default {
data() {
return {
objectOfAttrs: {
class:"active",
id:"appID"
}
}
}
}
</script>

条件渲染

类似JS中的条件语句:

  • v-if
  • v-else
  • v-else-if
  • v-show
v-if

会在指令表达式返回为true时渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div v-if="flag">
你能看见我么
</div>
</template>

<script>
export default {
data() {
return {
flg:true,
}
}
}
</script>
v-else

可以为v-if添加一个else区块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div v-if="flag">
你能看见我么
</div>
<div v-else>
那你还是看看我吧
</div>
</template>

<script>
export default {
data() {
return {
flg:true,
}
}
}
</script>
v-else-if

可连续多次重复使用

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 v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
</template>

<script>
export default {
data() {
return {
type:"D",
}
}
}
</script>
v-show

用法与v-if基本相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div v-show="flag">
你能看见我么
</div>
</template>

<script>
export default {
data() {
return {
flg:true,
}
}
}
</script>
v-show与v-if

v-if确保了在切换状态时,条件块内的时间监听器和子组件都会被==销毁与重建==,同时它也是惰性的,即如果在出自渲染时条件值为false,则不会做任何事,只有当条件首次变为true时才会被渲染。

v-show元素无论初始条件如何,始终会被渲染,只有CSSdisplay属性会被切换

总的来说v-if有==更高的切换开销==,而v-show有==更高的初始渲染开==销。因此,如果需要频繁切换则使用v-show,如果运行时条件很少改变,则v-if更合适

列表渲染

使用v-for指令基于一个数组来渲染一个列表。v-for指令需要使用item in items形式的特殊语法,其中items是源数据的数组,而item是迭代项的别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p v-for="item in names">
{{ item }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
names:["A", "B", "C"]
}
}
}
</script>
复杂数据

例如请求得到的Json数据

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
<template>
<div v-show="item in result">
<p>
{{ item.title }}
</p>
<img :src="item.avator" alt="">
</div>
</template>

<script>
export default {
data() {
return {
result:[{
"id": 10010,
"title": 中国电信
"avator": "https://pic.dianxin.com/avator/10010"
},{
"id": 10086,
"title": 中国移动
"avator": "https://pic.dianxin.com/avator/10010"
}]
}
}
}
</script>

v-for也支持使用可选的位置索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p v-for="(item, index) in names">
{{ item }}:{{ index }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
names:["A", "B", "C"]
}
}
}
</script>

xxxxxxxxxx3 1input::placeholder {2    color: red3}css

v-for还可以用于遍历对象的所有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<p v-for="(value, key, index) in userInfo">
{{ value }}:{{ key }}:{{ index }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
userInfo:{
name:"iwen",
age:20
}
}
}
}
</script>
通过key管理状态

Vue默认采用就地更新的策略。

也就是说当渲染一组数据,例如[1, 2, 3]后,如果我们将数据修改为[1, 3, 2],那么Vue会将整个列表重新渲染一遍,而不是移动已经渲染的DOM元素。

但可以通过key属性为数据提供标识,从而重用或重排现有元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p v-for="(item, index) in names" :key="index">
{{ value }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
names:["A", "B", "C"]
}
}
}
</script>

注意key在绑定时需要使用一个基础类型的值,例如string或number

推荐为每个item都加上key

并且在工作场景中,不推荐使用index作为key,因为我们无法确定新插入的数据是否会改变原有元素的index-item对应关系(比如在头部插入新数据)

事件处理

可以使用v-on(简写为@)来监听DOM事件,并在事件出发时执行对应的JS

事件处理器的值可以是:

  • 内敛事件处理器:事件被出发时执行的内敛JS语句(与onclick类似)
  • 方法事件处理器:一个指向组件上定义的方法的属性名或是路径
内联事件处理器

用于简单场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<button @click="count++">
Add 1
</button>
<p>
Count is : {{ count }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
count:0
}
}
}
</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
<template>
<div>
<button @click="addCount()">
Add 1
</button>
<p>
Count is : {{ count }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
count:0
}
},
methods: {
addCount() {
this.count+=1
}
}
}
</script>

事件参数

事件参数可以获取event对象和通过事件传递数据

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
<template>
<div>
<button @click="addCount()">
Add 1
</button>
<p>
Count is : {{ count }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
count:0
}
},
methods: {
addCount(e) {
console.log(e);
this.count+=1
}
}
}
</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
<template>
<div>
<button @click="count('hello')">
Add 1
</button>
<p>
Count is : {{ count }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
count:0
}
},
methods: {
addCount(msg) {
console.log(msg);
this.count+=1
}
}
}
</script>

传递参数的同时可能传递event,但需要增加$event$作为参数

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>
<button @click="count('hello', $event)">
Add 1
</button>
<p>
Count is : {{ count }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
count:0
}
},
methods: {
addCount(msg, e) {
console.log(e);
console.log(msg);
this.count+=1;
}
}
}
</script>

事件修饰符

调用事件时通常会调用event.preventDefault()event.stopPropagation()来组织事件冒泡。

如果可以更专注于数据逻辑而不用去处理DOM事件的细节会更方便

为了解决这一问题,Vue为v-on提供了时间修饰符,常用的有以下几种:

  • .stop
  • .prevent
  • .onece
  • .enter

阻止默认事件

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
<template>
<div>
<a @click.prevent="clickHandle" href="https://baidu.com">百度</a>
<div @click="clickDiv">
<p @click.stop="clickP">
测试冒泡
</p>
</div>
</div>
</template>

<script>
export default {
data() {
return {
count:0
}
},
methods: {
clickHandle(e) {
// 使用.prevent修饰后相当于调用了如下代码
// e.preventDefault();
console.log("点击了");
},
clickDiv() {
console.log("点击了Div");
},
clickP(e) {
// 使用.stop修饰后相当于调用了如下代码
// e.stopPropagation();
console.log("点击了P");
}
}
}
</script>

动态事件

vue中支持动态事件绑定:

使用[]将用于管理事件的变量包裹即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<button @[event]="addCount">
Add 1
</button>
<p>
Count is : {{ count }}
</p>
</div>
</template>

<script setup lang='ts'>
const event = 'click'
let count = 0
const addCount = () => {
count++
}
</script>

数组变化的侦测

变更方法

Vue能够监听响应式数组的变更方式,并在它们贝调用时出发相关的更新。这些变更方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice
  • sort()
  • reverse()
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
<template>
<div>
<p v-for="(item, index) in names" :key="index">
{{ item }}
</p>
<button @click="clickAddNamesHandle">
增加数据
</button>
</div>
</template>

<script>
export default {
data() {
return {
names: [
"iwen",
"ime",
"frank"
]
}
},
methods: {
clickAddNamesHandle() {
this.names.push("sakura")
}
}
}
</script>
替换一个数组

除了使用会对原数组进行更改的变更方法外,有一些不可变(immutable)方法,例如fliter()concat()slice(),这些都不会对原数组进行修改,而总是返回一个新数组。这种情况需要使用新数组替换旧数组

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
<template>
<div>
<h3>
数组1
</h3>
<p v-for="(item, index) in nums1" :key="index">
{{ item }}
</p>
<h3>
数组2
</h3>
<p v-for="(item, index) in nums2" :key="index">
{{ item }}
</p>
<button @click="filterHandle">
合并数据
</button>
</div>
</template>

<script>
export default {
data() {
return {
nums1:[1,2,3,4,5],
nums2:[6,7,8,9,10]
}
},
methods: {
filterHandle() {
this.nums1 = this.nums1.concat(this.nums2)
}
}
}
</script>

计算属性

template中虽然可以使用JS表达式进行一些计算,但在其中屑很多逻辑代码会使template变得臃肿,难以维护。因此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>
<div>
<h3>
{{ baidu.name }}
</h3>
<p>
{{ baidu.content.length > 0 ? "Yes" : "No"}}
</p>
</div>
</template>

<script>
export default {
data() {
return {
baidu: {
name: "百度",
content: ["前端", "Java", "python"]
}
}
}
}
</script>

上述代码在模板中使用了逻辑表达式,一旦出现bug,上述逻辑代码由于与逻辑块分散,难以定位。

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
<template>
<div>
<h3>
{{ baidu.name }}
</h3>
<p>
{{ baiduContent }}
</p>
</div>
</template>

<script>
export default {
data() {
return {
baidu: {
name: "百度",
content: ["前端", "Java", "python"]
}
}
},
// 计算属性
computed: {
baiduContent() {
return this.baidu.content.length > 0 ? "Yes" : "No"
}
}
}
</script>
计算属性缓存 VS 方法

可以注意到计算属性中仍然使用的时方法来进行表达式运算。

因此在methods中设计一个函数,然后再模板中调用函数也能达到一样的效果

那么两者有什么区别呢?

重点区别:

计算属性:计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算

方法:方法调用总是会在重渲染发生时再次执行函数

Class绑定

数据绑定的一个常见需求场景时操纵元素的CSS class列表,因为calss也是属性,因此可以使用v-bind将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易错的。因此Vue专门为classv-bind用法提供了特殊的功能增强。除了字符串外,class绑定的值也可以是对象和数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div :class="{ 'active': isActive, 'text-danger': hasError}">
isActive
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: true
}
}
}
</script>
多个对象绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div :class="classObject">
Class
</div>
</template>
<script>
export default {
data() {
return {
classObject: {
'active': true,
'text-danger': true
}
}
}
}
</script>
绑定数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div :class="[activeClass, errorClass]">
isActive
</div>
</template>
<script>
export default {
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
}
}
}
</script>

数组中也支持使用三目运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div :class="[isactive ? 'active text-danger' : '']">
isActive
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
}
}
}
</script>

数组和对象也能混合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div :class="[{'active': isActive}, errorClass]">

</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
errorClass: "text-danger"
}
}
}
</script>

注意事项

数组和对象嵌套过程中,只能是数组嵌套对象,反过来不行

style绑定

数据绑定的一个常见需求场景是操纵元素的CSS style列表,style也可以使用v-bind进行绑定,但与Class一样,style绑定同样支持对象和数组

需要注意的是和React一样,style中的属性需要使用驼峰命名法。否则需要使用‘’包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div :style="{ color: activeColor, fontSize: fontSize + 'px'}">
Style绑定
</div>
</template>
<script>
export default {
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div :style="styleObject">
Style绑定
</div>
</template>
<script>
export default {
data() {
return {
styleObject: {
color: 'red',
fontSize: '30px'
}
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div :style="[styleObject]">
Style绑定
</div>
</template>
<script>
export default {
data() {
return {
styleObject: {
color: 'green',
fontSize: '30px'
}
}
}
}
</script>

不推荐使用数组形式

侦听器

可以通过watch选项在每次响应式属性发生变化时触发一个函数

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>
<h3>
侦听器
</h3>
<P>
{{ message }}
</P>
<button @click="updateHandle">
修改数据
</button>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello";
}
},
methods: {
updateHandle() {
this.message = World;
}
},
watch: {
message(newValue, oldValue) {
console.log(newValue);
console.log(oldValue);
}
}
}
</script>

注意事项:

watch函数需要与被监听的响应式数据同名

表单输入绑定v-model

前端处理表单时,常需要将表单输入框的内容同步给JS中相应的变量。手动连接值绑定和更改事件侦听器会很麻烦,v-model简化了这一步骤

输入框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<input type="text" v-model="message">
<p>
{{ message }}
</p>
</template>
<script>
export default {
data() {
return {
message: ""
}
}
}
</script>

复选框

1
2
3
4
5
6
7
8
9
10
11
12
13
 <template>
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
</template>
<script>
export default {
data() {
return {
checked: true
}
}
}
</script>

修饰符

v-model也提供了修饰符:

  • .lazy
  • .number 只接受输入的数组
  • .trim 去掉输入前后的空格

.lazy

默认情况下v-model会在每次input事件后更新数据,可以添加lazy修饰符来改为每次change事件后更新数据

例如input标签默认会在失焦后出发change事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<input type="text" v-model.lazy="message">
<p>
{{ message }}
</p>
</template>
<script>
export default {
data() {
return {
message: ""
}
}
}
</script>
Vue3

在Vue3中,v-module具有许多新特性:

  1. 支持多个v-module绑定
  2. 支持自定义修饰符Modifiers
  3. 自定义组件的v-model发生了变化:
    1. 接收方式prop由value变为了modelValue
    2. 更新事件emit由input变为了update:modelValue
    3. v-bind的.sync修饰符和组件的model选项被移除

父组件给A组件绑定多个v-model并且包含一个自定义修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import A from './components/A.vue';
const showA = ref<boolean>(true)
const dialogText = ref<string>("Ender")

</script>

<template>
<div>
<h1>我是父组件</h1>
<div>showA: {{ showA }}</div>
<hr>
<div>dialogText: {{ dialogText }}</div>
<div><button @click="showA = !showA">开关</button></div>
<hr>
<!-- 多v-model时需要使用:指定名称 -->
<!-- 使用自定义修饰符isMagic -->
<A v-model:textValue.isMagic="dialogText" v-model="showA"></A>
</div>
</template>

<style scoped>
</style>

A组件通过props接收绑定的值,并利用emit实现双向绑定:

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
<script setup lang="ts">

const props = defineProps<{
// vue3默认使用modelValue接收,vue2中使用value
modelValue: boolean,
textValue: string,
// 获取自定义修饰符的命名是固定的,即“被修饰属性” + “Modifiers”
textValueModifiers?: {
isMagic: boolean
},
}>()


// 使用emit的方式操作v-model绑定的变量。命名规定要以update:开头
const emit = defineEmits(['update:modelValue', 'update:textValue'])
const closeDialog = () => {
// 直接将要修改的值作为参数传递
emit('update:modelValue', false)
}
const changeInputValue = (e:Event) => {
const target = e.target as HTMLInputElement
emit('update:textValue', props?.textValueModifiers?.isMagic ? target.value + " Magic" : target.value)
}

</script>

<template>
<div v-if="modelValue" class="model">
<div class="close"><button @click="closeDialog">关闭</button></div>
<h3>我是子组件model-dialog</h3>
<div>内容:<input @input="changeInputValue" :value="textValue" type="text"></div>
</div>
</template>

<style>
.model {
width: 500px;
border: 5px solid #ccc;
padding: 10px;
}
</style>

模板引用(读取DOM)

虽然Vue的声明性渲染模型抽象掉了大部分对DOM的直接操作,但在某些情况下,我们仍然需要直接访问底层DOM元素。此时需要使用ref属性

挂载结束后引用都会被暴露在this.$refs之上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div ref="container" class="container">{{ content }}</div>
<button @click="getElementHandle">
获取元素
</button>
</template>
<script>
export default {
data() {
return {
content: "内容"
}
},
methods: {
getElementHandle() {
console.log(this.$refs.container.innerHTML = "哈哈哈哈");
}
}
}
</script>

注意事项:

非特殊情况不要操作DOM

因为直接操作DOM很消耗性能

组件基础

由三部分组成:

  1. script
    • js代码的存放处
  2. template
    • html标签代码
  3. style
    • css样式代码

一般会将Vue组件定义为一个单独的.vue文件中,这被称为单文件组件(SFC)

单文件组件sfc

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
承载标签
</div>
</template>
<script>
export default {}
</script>
<!-- scoped: 让当前样式只在当前组件中生效 -->
<style scoped>
</style>

组件引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<!-- 显示组件 -->
<MyComponen />
<!-- 显示组件时还能使用如下命名方式 -->
<my-componen />
</template>
<script>
// 引入组件
import MyComponent from './component/MyComponent.vue'
// 注入组件
export default {
components: {
MyComponent
}
}
</script>
<style scoped>
</style>

单文件组件中只能出现一个template,且setup形式的script标签块只能有一个

每个组件必须抱恨一个template

组件嵌套关系

image-20230520203244054

组件尝被层层嵌套为树形结构

创建组件及引用关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h3>
Header
</h3>
</template>
<script>
export default {

}
</script>
<style scoped>
h3{
width: 100%;
height: 100%;
border: 5px solid #999;
text-align: center;
line-height: 100px;
box-sizing: border-box;
}
</style>
Main
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
<template>
<div class="main">
<h3>
Main
</h3>
<Article />
<Article />
</div>
</template>
<script>
import Article from './Article.vue'
export default {
components: {
Article
}
}
</script>
<style scoped>
.main{
float: left;
width: 70%;
height: 600px;
border: 5px solid #999;
box-sizing: border-box;
}
</style>
Asid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="aside">
<h3>
Aside
</h3>
</div>
</template>
<script>
export default {

}
</script>
<style scoped>
.aside{
float: right;
width: 30%;
height: 600px;
border: 5px solid #999;
box-sizing: border-box;
}
</style>
Article
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>
<h3>
Article
</h3>
<Item />
<Item />
<Item />
</template>
<script>
import Item from "./Item.vue"
export default {
components: {
Item
}
}
</script>
<style scoped>
h3{
width: 80%;
margin: 0 auto;
text-align: center;
line-height: 100px;
box-sizing: border-box;
margin-top: 50px;
background: #999;
}
</style>
Item
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h3>
Item
</h3>
</template>
<script>
export default {

}
</script>
<style scoped>
h3{
width: 80%;
margin: 0 auto;
text-align: center;
line-height: 100px;
box-sizing: border-box;
margin-top: 10px;
background: #999;
}
</style>

组件注册方式

Vue组件使用前需要先被注册,注册组件有两种方式:

  • 全局注册
  • 局部注册

全局注册

在最外层注册一次后,所有子组件中都能引用

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import GlobalComponent from "./components/GlobalComponent.vue"

const app = createApp(App);
// 名字 + 组件
// 之后使用该组件直接使用名字就行
app.component("GlobalComponent", GlobalComponent)
app.mount('#app');
1
2
3
4
5
<template>
<h3>
全局应用组件
</h3>
</template>

局部注册

全局注册虽然方便,但存在一下问题:

  • 全局注册时,没有被使用的组件也会被打包到项目中去
  • 全局注册在大型项目的使用中,依赖关系会变得不明确,不利于观察父子关系,可维护性低
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<GlobalComponent />
</template>
<script>
import Item from "./GlobalComponent.vue"
export default {
GlobalComponent
}
</script>
<style scoped>
*{
margin: 0;
padding: 0;
}
</style>

组件参数传递

Vue使用Props处理组件间通信

Parent

在父组件使用属性的形式,为子组件添加数据

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>
<h3>
Parent
</h3>
<Child title="Parent数据" />
<!-- 此外还可以使用v-bind传递动态数据 -->
<Child :activeTitle="message" :names="names" :userInfo="userInfo" />
</template>
<script>
import Child from "./Child.vue"
export default {
data() {
return{
childMessage: 20,
names: ["iwen", "ime", "frank"],
userInfo: {
name: "iwen"m
age: 20
}
}
},
components: {
Child
}
}
</script>
<style scoped>
</style>

Child

在子组件中使用props接受这些属性名

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>
<h3>
Child
</h3>
<p>
{{ title }}
</p>
<ul>
<li v-for="(item, index) of names" :key="index">{{ item }}</li>
</ul>
<p>
{{ userInfo.name }}
</p>
<p>
{{ userInfo.age }}
</p>
</template>
<script>
export default {
data() {

},
props: ["title", "activeTitle", "names", "userInfo"]
}
</script>
<style scoped>
</style>

注意事项:

props传递数据,只能父组件传递给子组件

props类型校验

parent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h3>
Parent
</h3>
<Child :title="childMessage" />
</template>
<script>
import Child from "./Child.vue"
export default {
data() {
return{
childMessage: 20,
}
},
components: {
Child
}
}
</script>
<style scoped>
</style>

child

子组件中使用type进行校验,支持多类型

还可以使用default设置默认值,当没有传递该参数时,直接使用该默认值

如果希望某个参数必须传值,那么可以使用required将其设置为必选项

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>
<h3>
Child
</h3>
<p>
{{ title }}
</p>
</template>
<script>
export default {
data() {

},
props: {
title: {
// 多类校验
type: [String, Number],
// 必选项
required: true
},
age: {
type: Number,
// 默认值
default: 0
},
names: {
type: Array,
// 工厂函数设置默认值
defalut() {
return ["Empty"]
}
}
}
}
</script>
<style scoped>
</style>

注意事项:

  • 数字和字符串可以直接使用default作为默认值,但数组和对象,必须通过工厂函数返回默认值,即使用default的函数形式进行返回。
  • 其次props是只读的

组件事件

组件数据传递

透传Attributes(属性继承)

插槽Slots

组件除了数据,还能接受模板内容,某些时候可能需要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段

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
<template>
<h3>
ComponentA
</h3>
<ComponentB>
<template>
<h3>
插槽传递试图内容
</h3>
</template>
</ComponentB>
</template>
<script>
import ComponentB from "./ConponentB.vue"
export default {
data() {

},
components: {
ComponentB
}
}
</script>
<style scoped>
</style>

在子组件中可以使用slot来显示插入的模板内容

slot元素是一个插槽出口(slot outlet),标示了父元素提供的插槽内容(slot content)将在哪里渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h3>
ComponentB
</h3>
<slot></slot>
</template>
<script>
export default {
data() {

}
}
</script>
<style scoped>
</style>

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身实在父组件模板中定义的

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>
<h3>
ComponentA
</h3>
<ComponentB>
<template v-slot>
<h3>
{{ message }}
</h3>
</template>
</ComponentB>
</template>
<script>
import ComponentB from "./ConponentB.vue"
export default {
data() {
return {
message: "message在父级"
}
},
components: {
ComponentB
}
}
</script>
<style scoped>
</style>

插槽默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容

1
2
3
4
5
6
<template>
<h3>
ComponentB
</h3>
<slot>插槽默认值</slot>
</template>

具名插槽

当我们想要设置多个不同的插槽时,需要指定名字

在父组件中使用v-slot来指定插槽名字,也可以使用简写#

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
<template>
<h3>
ComponentA
</h3>
<ComponentB>
<template v-slot:header>
<h3>
{{ message }}
</h3>
</template>
<template #main>
<h3>
{{ message }}
</h3>
</template>
</ComponentB>
</template>
<script>
import ComponentB from "./ConponentB.vue"
export default {
data() {
return {
message: "message在父级"
}
},
components: {
ComponentB
}
}
</script>
<style scoped>
</style>

在子组件中使用name为插槽命名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<h3>
ComponentB
</h3>
<slot name="header"></slot>
<hr>
<slot name="main"></slot>
</template>
<script>
export default {
data() {

}
}
</script>
<style scoped>
</style>

插槽混合数据

slot除了显示父组件的数据以外,实际上是可以通过其他方式显示子组件的数据的

slot支持使用类似props传递属性的方式,向插槽的出口传递数据

父组件可以使用v-slot中的对象对子组件中传递的数据进行显示

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>
<h3>
ComponentA
</h3>
<ComponentB>
<template v-slot="{ slotProps }">
<h3>
{{ message }} - {{ slotProps.msg }}
</h3>
</template>
</ComponentB>
</template>
<script>
import ComponentB from "./ConponentB.vue"
export default {
data() {
return {
message: "message在父级"
}
},
components: {
ComponentB
}
}
</script>
<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h3>
ComponentB
</h3>
<slot :msg="childMessage"></slot>

</template>
<script>
export default {
data() {
return {
childMessage: "子组件数据"
}
}
}
</script>
<style scoped>
</style>

对于具名组件,在父组件中=可以写在名称后进行数据接收:

1
2
3
4
5
6
7
8
9
<template>
<ComponentB>
<template #header="slotProps">
<h3>
{{ message }} - {{ slotProps.msg }}
</h3>
</template>
</ComponentB>
</template>

动态插槽

插槽的名称还可以是一个变量,允许我们动态的修改插槽内容插入的位置,例如:

插槽子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<h3>
ComponentB
</h3>
<slot name="header"></slot>
<hr>
<slot name="main"></slot>
</template>
<script>
export default {
data() {

}
}
</script>
<style scoped>
</style>

使用[]可以放入一个响应式对象来根据其中的值确定插槽的名称

插槽内容父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<h3>
ComponentA
</h3>
<ComponentB>
<template #[name]>
<h3>
插槽传递内容
</h3>
</template>
</ComponentB>
</template>
<script setup lang="ts">
import ComponentB from "./ConponentB.vue"
import { reactive, ref } from 'vue'
let name = ref('header')
</script>
<style scoped>
</style>

此时动态的修改name中的值即可改变插入的位置。

组件生命周期

image-20230522143306500

其中有如下生命周期比较重要:

  • 创建期
    • beforeCreate
    • created
  • 挂载期
    • beforeMount
    • mounted
  • 更新期
    • beforeUpdate
    • updated
  • 销毁期
    • beforeUnmount
    • unmounted

在组件的完整生命周期中,可以在这些自动执行的钩子函数中进行一些操作:

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
<template>
<h3>
组件生命周期
</h3>
<p>
{{ message }}
</p>
<button @click="updateHandle">
更新数据
</button>
</template>
<script>

export default {
data() {
return {
message: "更新之前"
}
},
components: {
ComponentB
},
methods: {
updateHandle() {
this.message = "更新之后"
}
},
beforeCreate() {
console.log("组件创建之前")
},
create() {
console.log("组件创建完成")
},
beforeMount() {
console.log("组件挂载之前")
},
Mounted() {
console.log("组件挂载完成")
},
beforeUpdate() {
console.log("组件更新之前")
},
upadte() {
console.log("组件更新完成")
},
beforeUnmount() {
console.log("组件销毁之前")
},
unmounted() {
console.log("组件销毁完成")
}
}
</script>
<style scoped>
</style>

例如我们可以在挂载完成之后获取到某个节点的DOM结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<h3>
生命周期应用
</h3>
<p ref = "name">
阿巴阿巴
</p>
</template>
<script>
export default {
beforeMount() {
console.log(this.$ref.name); // undefined
},
mounted() {
console.log(this.$ref.name);
}
}
</script>

可以在组件创建后从网络请求中获取数据,以为创建完成后,data才会变为可用的状态

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
<template>
<h3>
生命周期应用
</h3>
<ul>
<li v-for="(item, index) of banner" :key="item.key">
<h3>
{{ item.title }}
</h3>
<p>
{{ item.content }}
</p>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
banner: []
}
},
mounted() {
this.banner = [
{
"title": "bibubibu",
"content": "wahahah",
"key": "1"
}
]
},
mounted() {
console.log(this.$ref.name);
}
}
</script>

注意事项

但通常情况下,带有网络请求的页面中,认为结构数据更重要,因此通常网络请求会被放在mounted中,来达到先渲染结构再获取数据的效果

动态组件

有些场景需要几个组件间来回切换,比如Tab界面

A组件

1
2
3
4
5
6
7
<template>
<h3>
ComponentA
</h3>
</template>
<script>
</script>

B组件

1
2
3
4
5
6
7
<template>
<h3>
ComponentB
</h3>
</template>
<script>
</script>

容器组件可以使用vue提供的component标签处理多组件切换

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
<template>
<h3>
容器
</h3>
<component :is="tabComponent"></component>
<button @click="changeHandle">
切换组件
</button>
</template>
<script>
import ComponentA from "./ComponentA.vue"
import ComponentB from "./ComponentB.vue"
export default {
data() {
return {
tabComponent: "ComponentA"
}
},
methods: {
changeHandle(){
this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA"
}
}
components: {
ComponentA,
ComponentB
}
}
</script>

组合式API的写法

使用组合式API中的steup语法糖可以省略选项式API中的注册组件步骤,但要将组件变为响应式对象:

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>
<div style="display:flex">
<div @click="switchCom(item, index)" :class="[active == index ? 'active' : '']" class="tab" v-for="(item, index) in data" :key="index" >
<div>
{{ item.name }}
</div>
</div>
</div>
<component :is="comId"></component>
<button @click="changeHandle">
切换组件
</button>
</template>
<script setup lang='ts'>
import { ref, reactive, markRaw, shallowRef } from 'vue'
import ComponentA from "./ComponentA.vue"
import ComponentB from "./ComponentB.vue"

const comId = shallowRef(ComponentA)
const active = ref(0)
const data = reactive([
{
name: 'A组件',
com: markRaw(ComponentA)
},
{
name: 'B组件',
com: markRaw(ComponentB)
}
])
const switchCom = (item, index) => {
comId.value = item.com
active.value = index
}
</script>
<style scoped lang="less">
.active{
background: skyblue;
}
.tabs{
border: 1px solid #ccc;
padding: 5px 10px;
margin: 5px;
}
</style>

注意事项:

  1. 此处comId需要使用shallowRef注册为响应式对象而不使用reactive, ref是因为避免由于组件内部的改变导致该组件的切换模块更新
  2. 此处reactive注册的datalist中需要使用markRaw来避免其中组件内部的改变而触发datalist的更新

对于组件的注册还可以使用类似选项式API的做法,使用字符串代替组件名:

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
<template>
<div style="display:flex">
<div @click="switchCom(item, index)" :class="[active == index ? 'active' : '']" class="tab" v-for="(item, index) in data" :key="index" >
<div>
{{ item.name }}
</div>
</div>
</div>
<component :is="comId"></component>
<button @click="changeHandle">
切换组件
</button>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'

const comId = ref('ComponentA')
const active = ref(0)
const data = reactive([
{
name: 'A组件',
com: 'ComponentA'
},
{
name: 'B组件',
com: 'ComponentB'
}
])
const switchCom = (item, index) => {
comId.value = item.com
active.value = index
}
</script>
<script lang='ts'>
import ComponentA from "./ComponentA.vue"
import ComponentB from "./ComponentB.vue"
export default{
components: {
ComponentA,
ComponentB
}
}
</script>
<style scoped lang="less">
.active{
background: skyblue;
}
.tabs{
border: 1px solid #ccc;
padding: 5px 10px;
margin: 5px;
}
</style>

组件保持存活

在直接使用<component :is="tabComponent"></component>时,被切换掉的组件会被卸载,可以通过<keep-alive>组件强制将其保持“存活”状态

该组件包含一个include属性:

1
2
3
4
<keep-alive :include="['A']">
<A v-if="flag"></A>
<B v-else></B>
</keep-alive>

可以使用组件文件名字符串的形式来指定开启哪个组件的缓存,这样除了被开启的组件以外,其他组件都将在切换时被卸载

包含一个exclude属性:

  • 用于指定不被缓存的组件,用法与include一致

包含一个max属性:

  • 接收一个数组,用于指定最大被缓存组件数,使用LRU算法维护一个队列

  • 即当缓存数量达到上限时,将会把最早缓存的组件卸载

注意事项:

开启keep-alive的组件会多两个生命周期:

  1. onActivated
    • 被缓存组件每次消失时都会触发
  2. onDeactivated
    • 被缓存组件每次消失时都会触发

组件被卸载

A组件

组件被切换之后,出发组件的卸载相关钩子函数,控制台会显示组件卸载的信息

需要注意的是,组件卸载后重新挂载时,数据信息会变为初始值。因此切换前将老数据变为新数据后,切换后仍然显示老数据

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
<template>
<h3>
ComponentA
</h3>
<p>
{{ message }}
</p>
<button @click="updateHandle">
更新数据
</button>
</template>
<script>
export default {
beforeUnmount() {
console.log("组件卸载之前");
},
unmounted() {
console.log("组件卸载完成")
},
methods: {
updateHandle() {
this.message = "新数据"
}
}
}
</script>

B组件

1
2
3
4
5
6
7
<template>
<h3>
ComponentB
</h3>
</template>
<script>
</script>

容器组件

当我们使用keep-alive组件将component组件包裹时,切换后组件则不会进入卸载生命周期

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>
<h3>
容器
</h3>
<keep-live>
<component :is="tabComponent"></component>
</keep-live>
<button @click="changeHandle">
切换组件
</button>
</template>
<script>
import ComponentA from "./ComponentA.vue"
import ComponentB from "./ComponentB.vue"
export default {
data() {
return {
tabComponent: "ComponentA"
}
},
methods: {
changeHandle(){
this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA"
}
}
components: {
ComponentA,
ComponentB
}
}
</script>

异步组件

在大型项目中,可能需要拆分应用为更小的块,这会导致项目包含很多很多组件,因此应付实现在需要的时候从服务器加载相关组件,以避免同时加载许多组件导致响应过慢。Vue提供了defineAsyncComponent方法来实现这样的异步加载的组件

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
<template>
<h3>
组件切换
</h3>
<keep-live>
<component :is="tabComponent"></component>
</keep-live>
<button @click="changeHandle">
切换组件
</button>
</template>
<script>
import { defineAsyncComponent } from "vue"
import ComponentA from "./ComponentA.vue"
// import ComponentB from "./ComponentB.vue"
// 异步加载组件
const ComponentB = defineAsyncComponent(() => {
import("./ComponentB.vue")
})
export default {
data() {
return {
tabComponent: "ComponentA"
}
},
methods: {
changeHandle(){
this.tabComponent = this.tabComponent == "ComponentA" ? "ComponentB" : "ComponentA"
}
}
components: {
ComponentA,
ComponentB
}
}
</script>

Vue3提供了一个更方便的异步组件渲染方式Suspense,该组件包含两个插槽,一个是确认要渲染的组件#default,另一个是临时渲染占位的组件#fallback

注意此处的defineAsyncComponent中需要使用import的函数形式:

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
<template>
<div class="ender-content">
<div class="ender-content__item">
<waterFallVue :list="list"></waterFallVue>
</div>
<div class="ender-content__item">
<Suspense>
<template #default>
<syncVue></syncVue>
</template>
<template #fallback>
<Skeleton></Skeleton>
</template>
</Suspense>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, reactive, defineAsyncComponent } from 'vue'
import waterFallVue from '../../components/WaterFall/water-fall.vue'
import Skeleton from '../../components/Skeleton/skeleton.vue'

const syncVue = defineAsyncComponent(() => import('../../components/SyncCom/index.vue'))

const list = [
{
height: 300,
background: 'red'
},
{
height: 400,
background: 'pink'
},
{
height: 500,
background: 'blue'
}
]
</script>

<style scoped lang="scss">
@include b(content) {
flex: 1;
background-color: #fff;
overflow: auto;
overflow-y: hidden;
padding: 0 10px;
display: flex;
flex-direction: column;
@include e(item) {
flex: 1;
overflow: auto;
border-bottom: 1px solid #ccc;
padding: 10px;
}
}
</style>

此外defineAsyncComponent除了上述的回调函数写法,还有另外的一种对象写法:

1
2
3
4
5
const syncVue = defineAsyncComponent({
loadingComponent:,
errorComponent:,
timeout:
})

注意事项:

使用异步组件的好处在于:

  • 不使用异步组件时,使用npm run build打包后会讲所有代码打包为一个js文件
  • 使用异步组件后,打包会讲异步组件单独打包为另一个js

这样讲缩小主js文件的体积,缩短加载的时间,而异步组件对应的js文件只有在使用时才会被加载,优化用户体验。

依赖注入

VUE通过原型链的方式实现依赖注入,angular则是使用IOC和DI的方式实现

依赖注入用于解决,某一深层的子孙组件需要一个来自距离自己较远的祖先组件的数据。

vue种使用provideinject来解决这一问题,一个父组件对于其所有后代组件,作为依赖提供者,所有后代组件,都可以注入由父组件给整条链路的依赖

需要注意的是如果要传递动态数据,则provide要使用函数的形式

祖先组件

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>
<h3>
祖先
</h3>
<Parent />
</template>
<script>
import Parent from "./components/Parent.vue"
export default {
data() {
return{
activeMessage: "祖先的财产"
}
},
provide: {
message: "祖先财产"
},
provide() {
return {
message: this.activeMessage
}
},
components: {
Parent
}
}
</script>

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<h3>
Parent
</h3>
<Child />
</template>
<script>
import Child from "./components/Child.vue"
export default {
data() {
return{

}
},
components: {
Child
}
}
</script>

子孙组件

子孙组件如果要将注入的数据作为动态数据,则直接使用this访问并赋值即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h3>
Child
</h3>
<p>
{{ message }}
</p>
<p>
{{ fullMessage }}
</p>
</template>
<script>
export default {
inject: ["message"],
data() {
return{
fullMessage: this.message
}
}
}
</script>

注意事项:

依赖注入与props一样是单向的

传递动态数据

在setup语法糖模式下可以使用provide的函数形式,可以传递响应式的数据,并且子孙组件对数据的修改也会引起父组件的更新,可以使用readonly加以限制

父组件:

1
2
3
4
5
<script setup lang="ts">
import { ref, reactive, provide } from 'vue'
const message = ref<string>('red');
provide('msg', message)
</script>

子组件

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import { ref, reactive, inject } from 'vue'
import type { Ref } from 'vue'
const message = inject<Ref<string>>('msg')
const change = () => {
// 使用非空断言访问
message!.value = 'yellow'
}
</script>

全局注入

可以在main.js种直接提供全局数据

min.js

1
2
3
4
5
6
7
8
import { createApp } from 'vue'
import App from './App.vue'

const app = creare(App)

app.provide("globalData", "全局数据")

app.mount('#app')

子组件依然使用inject: ["globalData"]接受即可

Vue应用

应用实例

每个Vue应用通过createApp函数来创建一个Vue应用实例,使用脚手架创建的项目目录下的main.js即有如下写法

在一个项目种,有且仅有一个Vue实例对象

1
2
3
4
5
6
import { createApp } from 'vue'
// something thing

const app = creareApp({
/* 根组件选项 */
})

根组件

上述代码传入createApp的对象实际上是一个组件,每个项目都需要一个根组件,其他组件均为其子组件。

1
2
3
4
5
import { createApp } from 'vue'
// 从一个单文件组件种导入根组件
import App from './App,vue'

const app = createApp(App)

挂载应用

应用实例必须调用.mount方法后才能渲染,该方法接受一个容器参数,可以是一个实际的DOM元素或一个CSS选择器字符串

1
app.mount("#app")
1
<div id="app"></div>

公共资源文件夹

src下会有一个assets文件夹,用来存放公共资源

Vue Router

Vue是组件式的开发,那么与React将面临同样的问题,就是URL如何映射到单个组件上。

因为我们知道浏览器认识的语言只有HTML、CSS、JS,那么路由的作用就是告诉浏览器什么时候该渲染那个组件。

我们先从一个例子来感受一下Vue Router的作用

入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script>

<div id="app">
<h1>Hello App!</h1>
<p>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>

router-link

此处并没有使用a标签来处理跳转,原因是Vue Router中的router-link可以在不重新加载页面的情况下更改URL

router-view

此处显示与url对应的组件。

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
// 1. 定义路由组件.
// 也可以从其他文件导入
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }

// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]

// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})

// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router)

app.mount('#app')

// 现在,应用已经启动了!

调用app.use(router)触发第一次导航并且可以在任意组件中以this.$router的形式访问它,并且以this.$router的形式访问当前路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Home.vue
export default {
computed: {
username() {
// 我们很快就会看到 `params` 是什么
return this.$route.params.username
},
},
methods: {
goToDashboard() {
if (isAuthenticated) {
this.$router.push('/dashboard')
} else {
this.$router.push('/login')
}
},
},
}

要在 setup 函数中访问路由,请调用 useRouteruseRoute 函数。我们将在 Composition API 中了解更多信息。

在整个文档中,我们会经常使用 router 实例,请记住,this.$router 与直接使用通过 createRouter 创建的 router 实例完全相同。我们使用 this.$router 的原因是,我们不想在每个需要操作路由的组件中都导入路由。

脚手架抽离

事实上在脚手架中配置路由时,我们希望家路由从业务逻辑中抽离,使得维护和扩展时变得方便,例如新建一个Router目录后创建index.js文件,将路由全部写在此处后,将router抛出:

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 { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes:Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about/:id',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})

export default router

带参数的动态路由

有时我们希望路由前往的页面根据不同情况显示不同的效果,例如可能有一个User组件,它需要对所有用户进行渲染,但用户ID不同,需要显示的ID内容也不同,Vue Router中可以在路径中使用一个动态字段来实现,称之为路径参数

1
2
3
4
5
6
7
8
9
const User = {
template: '<div>User</div>',
}

// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]

那么现在如果有两个用户:johnnyjolyne

那么以下两个路径将会映射到同一个组件:

  • /users/johnny
  • /users/jolyne

路径参数使用:表示,当一个路由被匹配时,params值将在每个组件中以this.$router.params的形式暴露出来,因此在User组件的模板中我们可以这样显示:

1
2
3
const User = {
template: '<div>User {{ $route.params.id }}</div>',
}

路径与params的对应关系如下:

模式串 路径 $router.params
/users/:username /users/eduardo {username: 'eduardo'}
/users/:username/posts/:postId /users/eduardo/posts/123 { username: 'eduardo', postId: '123' }

响应路由参数变化

如果使用上述方式渲染同一组件,那么当发生用户切换时,由于两个路径映射了同一组件,Vue Router使用的策略是直接复用这一组件,而不是销毁后再创建。

那么这也一位置,URL更新了,但是组件的生命周期钩子函数并不会被触发。

因此需要使用侦听器来对$route.params来监听其变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 选项式API
watch: {
"$route.params": {
handler(newValue, oldValue) {
console.log("previous: ", oldValue)
console.log("to: ", newValue)
}
}
}

// 组合式API
watch(() => this.$route.params,
(toParams, previousParams) => {
// 对路由变化做出响应...
})

或者可以使用beforeRouteUpdate导航守卫,可以取消导航:

1
2
3
4
5
6
7
const User = {
template: '...',
async beforeRouteUpdate(to, from) {
// 对路由变化做出响应...
this.userData = await fetchUser(to.params.id)
},
}

捕获所有路由或 404 Not found 路由

常规参数只匹配 url 片段之间的字符,用 / 分隔。如果我们想匹配任意路径,我们可以使用自定义的 路径参数 正则表达式,在 路径参数 后面的括号中加入 正则表达式 :

1
2
3
4
5
6
const routes = [
// 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// 将匹配以 `/user-` 开头的所有内容,并将其放在 `$route.params.afterUser` 下
{ path: '/user-:afterUser(.*)', component: UserGeneric },
]

在这个特定的场景中,我们在括号之间使用了自定义正则表达式,并将pathMatch 参数标记为可选可重复。这样做是为了让我们在需要的时候,可以通过将 path 拆分成一个数组,直接导航到路由:

1
2
3
4
5
6
7
8
this.$router.push({
name: 'NotFound',
// 保留当前路径并删除第一个字符,以避免目标 URL 以 `//` 开头。
params: { pathMatch: this.$route.path.substring(1).split('/') },
// 保留现有的查询和 hash 值,如果有的话
query: this.$route.query,
hash: this.$route.hash,
})

高级匹配模式

Vue Router 使用自己的路径匹配语法,其灵感来自于 express,因此它支持许多高级匹配模式,如可选的参数,零或多个 / 一个或多个甚至自定义的正则匹配规则。请查看高级匹配文档来探索它们。!

路由传参数

使用编程时导航时可以利用queryparms两个参数进行传参

两者的区别:

  1. query 传参配置的是 path,而 params 传参配置的是name,在 params中配置 path 无效
  2. query 在路由配置不需要设置参数,而 params 必须设置
  3. query 传递的参数会显示在地址栏中
  4. params传参刷新会无效,但是 query 会保存传递过来的值,刷新不变 ;
  5. 路由配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// query 传参
router.push({
path: "/reg",
query: {
name: "jj"
}
})
// query 接收参数
import {useRoute} from "vue-router"
// 注意接收参数是使用route
const route = useRoute()
route.query.name


// parms 传参
router.push({
name: "reg",
parms: {
name: "jj"
}
})

// parms 接收参数

路由匹配语法

大多数情况下会使用/about

自定义正则

当定义像 :userId 这样的参数时,内部可以使用([^/])来从URL中提取参数

例如我们有两个路由:/:orderId/:productName,如果两者恰好匹配了相同的URL,同时我们并不像使用额外的静态路由来区分两个URL,那我们就可以通过两个字段的特点来进行区分:

例如对于orderId而言,他的取值总是一个数字,而productName的取值可以是任何形式,所以Vue Router支持在括号中为参数指定一个自定义的正则:

1
2
3
4
5
6
const routes = [
// /:orderId -> 仅匹配数字
{ path: '/:orderOd(\\d+)' },
// /:porductName -> 匹配其他任何内容
{ path: '/:productName' },
]

routes中的顺序并不会影响匹配

注意事项:

确保**转义反斜杠( \ )**,就像我们对 \d (变成\\d)所做的那样,在 JavaScript 中实际传递字符串中的反斜杠字符。

可重复参数

匹配例如/one/two/three这样的多个序列的地址

1
2
3
4
5
6
const routes = [
// /:chapters -> 匹配 /one, /one/two, /one/two/three, 等
{ path: '/:chapters+' },
// /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
{ path: '/:chapters*' },
]

接受时则需要使用一个数组来接收,并且在使用命名路由时也需要传递一个数组:

1
2
3
4
5
6
7
8
9
// 给定 { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 产生 /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// 产生 /a/b

// 给定 { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 抛出错误,因为 `chapters` 为空

此外也可以与自定义正则相结合:

1
2
3
4
5
6
7
const routes = [
// 仅匹配数字
// 匹配 /1, /1/2, 等
{ path: '/:chapters(\\d+)+' },
// 匹配 /, /1, /1/2, 等
{ path: '/:chapters(\\d+)*' },
]

sensitive和strict路由配置

默认情况下路由和标准URL一致不区分大小写,但可以通过strictsensitive来切换,它们可以既可以应用在整个全局路由上,又可以应用于当前路由上:

1
2
3
4
5
6
7
8
9
10
11
12
const router = createRouter({
history: createWebHistory(),
routes: [
// 将匹配 /users/posva 而非:
// - /users/posva/ 当 strict: true
// - /Users/posva 当 sensitive: true
{ path: '/users/:id', sensitive: true },
// 将匹配 /users, /Users, 以及 /users/42 而非 /users/ 或 /users/42/
{ path: '/users/:id?' },
],
strict: true, // applies to all routes
})

可修改参数

可以使用?修饰符将一个参数标记为可选项:

1
2
3
4
5
6
const routes = [
// 匹配 /users 和 /users/posva
{ path: '/users/:userId?' },
// 匹配 /users 和 /users/42
{ path: '/users/:userId(\\d+)?' },
]

嵌套路由

UI通常是由多层嵌套的组件构成例如:

1
2
3
4
5
6
7
8
/user/johnny/profile                     /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+

Vue Router可以使用嵌套路由来表达这种关系

例如上文提到的结构:

1
2
3
<div id="app">
<router-view></router-view>
</div>
1
2
3
4
5
6
const User = {
template: '<div>User {{ $route.params.id }}</div>',
}

// 这些都会传递给 `createRouter`
const routes = [{ path: '/user/:id', component: User }]

此处 <router-view></router-view>将作为一个顶层router-view。接着我们再向其中嵌套一层router-view,例如在User组件的模板中添加:

1
2
3
4
5
6
7
8
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`,
}

接着需要在路由中配置children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// 当 /user/:id/profile 匹配成功
// UserProfile 将被渲染到 User 的 <router-view> 内部
path: 'profile',
component: UserProfile,
},
{
// 当 /user/:id/posts 匹配成功
// UserPosts 将被渲染到 User 的 <router-view> 内部
path: 'posts',
component: UserPosts,
},
],
},
]

注意,以 / 开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而不必使用嵌套的 URL。

需要注意的是,使用如上配置后,当我们访问/user/eduardo时,页面并不能找到对应的组件,因为并没有这一路径的匹配方式。如果希望在父子组件之间显示一些内容的话需要添加一个空的嵌套路径

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功
// UserHome 将被渲染到 User 的 <router-view> 内部
{ path: '', component: UserHome },

// ...其他子路由
],
},
]

嵌套命名路由

处理命名路由时,通常回给子路由命名:

1
2
3
4
5
6
7
8
const routes = [
{
path: '/user/:id',
component: User,
// 请注意,只有子路由具有名称
children: [{ path: '', name: 'user', component: UserHome }],
},
]

这将确保导航到 /user/:id 时始终显示嵌套路由。

在一些场景中,你可能希望导航到命名路由而不导航到嵌套路由。例如,你想导航 /user/:id 而不显示嵌套路由。那样的话,你还可以命名父路由,但请注意重新加载页面将始终显示嵌套的子路由,因为它被视为指向路径/users/:id 的导航,而不是命名路由:

1
2
3
4
5
6
7
8
const routes = [
  {
    path: '/user/:id',
    name: 'user-parent',
    component: User,
    children: [{ path: '', name: 'user', component: UserHome }],
  },
]

编程式导航

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。

想要导航到不同的 URL,可以使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。

当你点击 <router-link> 时,内部会调用这个方法,所以点击 <router-link :to="..."> 相当于调用 router.push(...)

声明式 编程式
<router-link :to="..."> router.push(...)

该方法的使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

注意:如果提供了 pathparams 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path

1
2
3
4
5
6
7
8
9
10
11
12
const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo

// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo

// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo

// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user

当指定 params 时,可提供 stringnumber 参数(或者对于可重复的参数可提供一个数组)。任何其他类型(如 undefinedfalse 等)都将被自动字符串化。对于可选参数,你可以提供一个空字符串("")来跳过它。

torouter.push接受的对象种类相同,两者规则也相同

router.push 和所有其他导航方法都会返回一个 Promise,让我们可以等到导航完成后才知道是成功还是失败。

替代当前位置

它的作用类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目。

声明式 编程式
<router-link :to="..." replace> router.replace(...)

也可以直接在传递给 router.pushrouteLocation 中增加一个属性 replace: true

1
2
3
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })

使用replace进行跳转将不会留下历史记录(即浏览器无法进行前进后退

横跨历史

该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步,类似于 window.history.go(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 向前移动一条记录,与 router.forward() 相同
router.go(1)

// 返回一条记录,与 router.back() 相同
router.go(-1)
router.back(1)

// 前进 3 条记录
router.go(3)

// 如果没有那么多记录,静默失败
router.go(-100)
router.back(100)
router.go(100)

篡改历史

router.pushrouter.replacerouter.go实际上是windows.historyAPI的模仿

例如使用<router-link replace to="/"></router-link>标签进行的跳转不会留下历史记录

因此其中很多与Browser History APIs类似的操作

值得一提的是,无论在创建路由器实例时传递什么样的 history 配置,Vue Router 的导航方法( pushreplacego )都能始终正常工作。

命名路由

除了使用path进行路由以外,还可以使用name进行路由。

该方式具有如下优点:

  • 没有硬编码的URL
  • params的自动编码/解码
  • 防止在url中出现打错字
  • 绕过路径排序(如显示一个)
1
2
3
4
5
6
7
const routes = [
{
path: '/user/:username',
name: 'user',
component: User,
},
]

要链接到一个命名的路由,可以向 router-link 组件的 to 属性传递一个对象:

1
2
3
<router-link :to="{ name: 'user', params: { username: 'erina' }}">
User
</router-link>

对应的编程式导航写法如下:

1
2
router.push({ name: 'user', params: { username: 'erina' } })
// 路由地址:/user/erina

命名视图

有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default

1
2
3
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
],
})

嵌套命名试图

有时候对于更复杂的页面,可能需要使用命名试图创建嵌套视图的复杂布局。例如如下页面:

1
2
3
4
5
6
7
8
9
/settings/emails                                       /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | +------------> | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+
  • Nav 只是一个常规组件。
  • UserSettings 是一个视图组件。
  • UserEmailsSubscriptionsUserProfileUserProfilePreview 是嵌套的视图组件。

其中UserSettings组件的template结构如下:

1
2
3
4
5
6
7
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar />
<router-view />
<router-view name="helper" />
</div>

那么接下来可以通过如下的路由配置事项上述布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
path: '/settings',
// 你也可以在顶级路由就配置命名视图
component: UserSettings,
children: [{
path: 'emails',
component: UserEmailsSubscriptions
}, {
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview
}
}]
}

重定向与别名

重定向

重定向可以使用routes配置来完成,例如将/home重定向到/:

1
const routes = [{ path: '/home', redirect: '/' }]

重定向的目标也可以是一个命名的路由:

1
const routes = [{ path: '/home', redirect: { name: 'homepage' } }]

甚至是一个方法,动态返回重定向目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const routes = [
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
{
path: '/search',
// ...
},
]

请注意,**导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上**。在上面的例子中,在 /home 路由中添加 beforeEnter 守卫不会有任何效果。

在写 redirect 的时候,可以省略 component 配置,因为它从来没有被直接访问过,所以没有组件要渲染。唯一的例外是嵌套路由:如果一个路由记录有 childrenredirect 属性,它也应该有 component 属性。

相对重定向

也可以重定向到相对位置:

1
2
3
4
5
6
7
8
9
10
11
12
const routes = [
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: '/users/:id/posts',
redirect: to => {
// 该函数接收目标路由作为参数
// 相对位置不以`/`开头
// 或 { path: 'profile'}
return 'profile'
},
},
]

别名

重定向是指当用户访问 /home 时,URL 会被 / 替换,然后匹配成 /。那么什么是别名呢?

/ 别名为 /home,意味着当用户访问 /home 时,URL 仍然是 /home,但会被匹配为用户正在访问 /

上面对应的路由配置为:

1
const routes = [{ path: '/', component: Homepage, alias: '/home' }]

通过别名,你可以自由地将 UI 结构映射到一个任意的 URL,而不受配置的嵌套结构的限制。使别名以 / 开头,以使嵌套路径中的路径成为绝对路径。你甚至可以将两者结合起来,用一个数组提供多个别名:

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/users',
component: UsersLayout,
children: [
// 为这 3 个 URL 呈现 UserList
// - /users
// - /users/list
// - /people
{ path: '', component: UserList, alias: ['/people', 'list'] },
],
},
]

如果你的路由有参数,请确保在任何绝对别名中包含它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
{
path: '/users/:id',
component: UsersByIdLayout,
children: [
// 为这 3 个 URL 呈现 UserDetails
// - /users/24
// - /users/24/profile
// - /24
{ path: 'profile', component: UserDetails, alias: ['/:id', ''] },
],
},
]

SEO

关于 SEO 的注意事项: 使用别名时,一定要定义规范链接.

将props传递给路由组件

在你的组件中使用 $route 会与路由紧密耦合,这限制了组件的灵活性,因为它只能用于特定的 URL。虽然这不一定是件坏事,但我们可以通过 props 配置来解除这种行为:

我们可以将下面的代码

1
2
3
4
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]

替换为:

1
2
3
4
5
6
const User = {
// 请确保添加一个与路由参数完全相同的 prop 名
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]

布尔模式

props 设置为 true 时,route.params 将被设置为组件的 props

命名视图

对于有命名视图的路由,你必须为每个命名视图定义 props 配置:

1
2
3
4
5
6
7
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]

对象模式

props 是一个对象时,它将原样设置为组件 props。当 props 是静态的时候很有用。

1
2
3
4
5
6
7
const routes = [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]

函数模式

你可以创建一个返回 props 的函数。这允许你将参数转换为其他类型,将静态值与基于路由的值相结合等等。

1
2
3
4
5
6
7
const routes = [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]

URL /search?q=vue 将传递 {query: 'vue'} 作为 props 传给 SearchUser 组件。

请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 vue 才可以对状态变化做出反应。

不同历史模式

在创建路由器实例时,history 配置允许我们在不同的历史模式中进行选择。

Hash模式

hash 模式是用 createWebHashHistory() 创建的:

1
2
3
4
5
6
7
8
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes: [
//...
],
})

它在内部传递的实际 URL 之前使用了一个哈希字符(#)。由于这部分 URL 从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。如果你担心这个问题,可以使用 HTML5 模式。

该模式是通过locations中的方法实现跳转的:

1
location.hash = '/reg'

而浏览器的前进后退事件则是通过:

1
2
3
windows.addEventListener("hashchange", (e) => {
console.log(e)
})

事件进行监听

HTML5 模式

createWebHistory() 创建 HTML5 模式,推荐使用这个模式:

1
2
3
4
5
6
7
8
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes: [
//...
],
})

当使用这种历史模式时,URL 会看起来很 “正常”,例如 https://example.com/user/id

不过,问题来了。由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 https://example.com/user/id,就会得到一个 404 错误。这就尴尬了。要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html 相同的页面。

该模式通过h5新增的对象history实现路由切换

1
history.pushState({state: 1}, "", "/reg")

但直接使用该方法进行页面跳转不会触发popstate事件

监听前进后退事件则是通过popstate事件进行:

1
2
3
windows.addEventListener("popstate", (e) => {
console.log(e)
})

服务器配置示例

注意:以下示例假定你正在从根目录提供服务。如果你部署到子目录,你应该使用Vue CLI 的 publicPath 配置和相关的路由器的 base 属性。你还需要调整下面的例子,以使用子目录而不是根目录(例如,将RewriteBase/ 替换为 RewriteBase/name-of-your-subfolder/)。

Apache
1
2
3
4
5
6
7
8
9
10
11
12
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>

也可以使用 FallbackResource 代替 mod_rewrite

nginx
1
2
3
location / {
try_files $uri $uri/ /index.html;
}
原生Node.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const http = require('http')
const fs = require('fs')
const httpPort = 80

http
.createServer((req, res) => {
fs.readFile('index.html', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.html" file.')
}

res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
})

res.end(content)
})
})
.listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})

Caveat

这有一个注意事项。你的服务器将不再报告 404 错误,因为现在所有未找到的路径都会显示你的 index.html 文件。为了解决这个问题,你应该在你的 Vue 应用程序中实现一个万能的路由来显示 404 页面。

1
2
3
4
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:pathMatch(.*)', component: NotFoundComponent }],
})

另外,如果你使用的是 Node.js 服务器,你可以通过在服务器端使用路由器来匹配传入的 URL,如果没有匹配到路由,则用 404 来响应,从而实现回退。查看 Vue 服务器端渲染文档了解更多信息。

路由守卫

守卫可以在任何位置使用router进行添加

前置守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const router = useRouter()

// 定义一个白名单用于保证首页的正常访问
const whiteList = ['/']

// 前置守卫会在每一次路由跳转时触发
// 回调函数包含三个参数:
// to: 目的URL
// from: 起点URL
// next: 下一步操作函数(如果还有别的前置守卫,则该函数为下个一个守卫处理函数,如果没有该函数为实际跳转操作函数
router.forEach((to, from, next) => {
if(whiteList.includes(to) || localStorage.getItem("token")) {
// 在白名单中,或登陆过保存了token凭证则进行跳转
next()
} else {
// 否则跳转到路径
next("/")
}
})

后置守卫

后置守卫会在跳转完成之后执行,我们可以利用前置和后置两个守卫来实现一个加载进度条的功能:

先准备好进进度条组件:

loadingBar.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

<template>
<div class="wraps">
<div ref="bar" class="bar"></div>
</div>
</template>

<script setup lang='ts'>
import { ref, onMounted } from 'vue'
let speed = ref<number>(1)
let bar = ref<HTMLElement>()
let timer = ref<number>(0)
const startLoading = () => {
let dom = bar.value as HTMLElement;
speed.value = 1
// 使用Windows.requestAnimationFrame不会触发多次回流与重绘,浏览器会收集这些操作,并最最后触发一次重绘,并且会以60帧显示
// 如果使用interval每一次调用都会触发回流与重绘
timer.value = window.requestAnimationFrame(function fn() {
if (speed.value < 90) {
speed.value += 1;
dom.style.width = speed.value + '%'
timer.value = window.requestAnimationFrame(fn)
} else {
speed.value = 1;
// 使用cancleAnimationFrame取消事件绑定
window.cancelAnimationFrame(timer.value)
}
})

}

const endLoading = () => {
let dom = bar.value as HTMLElement;
setTimeout(() => {
window.requestAnimationFrame(() => {
speed.value = 100;
dom.style.width = speed.value + '%'
})
}, 500)

}

// 向外暴露两个方法用于开启和结束进度条
defineExpose({
startLoading,
endLoading
})
</script>

<style scoped lang="less">
.wraps {
position: fixed;
top: 0;
width: 100%;
height: 2px;
.bar {
height: inherit;
width: 0;
background: blue;
}
}
</style>

main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import loadingBar from './components/loadingBar.vue'
import {createVNode, render} from "vue"
const router = useRouter()

// 挂载部分可以抽离为单独的ts文件
// 全局添加进度条组件,注意先要将组件转化为虚拟DOM节点
const vNode = createVNode(loadingBar)
// 然后使用render全局挂载
render(vNode, document.body)

router.beforeEach((to, from, next) => {
// 跳转页面前调用开启进度条方法
vNode.component?.exposed?.startLoading()
})

router.afterEach((to, from) => {
// 跳转结束后调用结束进度条方法
vNode.component?.exposed?.endLoading()
})

路由原信息

通过路由记录的 meta 属性可以定义路由的元信息。使用路由元信息可以在路由中附加自定义的数据,例如:

  • 权限校验标识。
  • 路由组件的过渡名称。
  • 路由组件持久化缓存 (keep-alive) 的相关配置。
  • 标题名称

我们可以在导航守卫或者是路由对象中访问路由的元信息数据。

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

// 如果不使用扩展 meta中的属性将会推断为unknow 类型
declare module 'vue-router' {
interface RouteMeta {
title?: string
}
}

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/views/Login.vue'),
meta: {
title: "登录"
}
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
meta: {
title: "首页",
}
}
]

路由跳转动效

可以利用meta信息为组件添加一些过度动效,直接将动效的名称放在meta中,这里使用的时Animate.css动效库:

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

declare module 'vue-router'{
interface RouteMeta {
title:string,
transition:string,
}
}

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/views/Login.vue'),
meta:{
title:"登录页面",
transition:"animate__fadeInUp",
}
},
{
path: '/index',
component: () => import('@/views/Index.vue'),
meta:{
title:"首页!!!",
transition:"animate__bounceIn",
}
}
]
})

然后router4.X的router-view组件支持插槽,可以在插槽中引入动效标签:

1
2
3
4
5
6
7
<!-- 插槽接收两个返回值 分别代表目前页面的路由信息以及当前显示的组件 -->
<router-view #default="{route,Component}">
<!-- 将路由信息中的meta.transition动效名称拼接放到animate中即可 -->
<transition :enter-active-class="`animate__animated ${route.meta.transition}`">
<component :is="Component"></component>
</transition>
</router-view>

评论