
Vue3学习笔记
最近想复习一下Vue3的整体过程,打算重新学一遍的Vue3+TS的教程,现在准备开始学,并且做好笔记。
Vue3官网 Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)
尚硅谷笔记链接大全:资料
一共是71集,全长大约14个小时,从2024.03.04下午16:30开始学习,至此,开始记录。
编码规范为:TS+组合式API+Setup语法糖
Webpack和Vite的构建区别
Webpack:从入口进入,先扫描路由,然后扫描每一个模块,然后开始bundle,最后ServerReady,也就是说不管你用不用到的东西,都给你加载了,然后再给你看结果,就会比较慢。
Vite:我直接给你看结果,你想看哪个我给你加载哪个,等于变聪明了。
一句话:Vite比Webpack快。
Vue3创建步骤
1
npm create vue@latest
然后根据所需进行配置即可。
然后
1
2
3
4
5
6
7
8
cd <your-project-name>
npm install
npm run dev
#or
yarn
yarn dev
项目整体文件介绍
.vscode - extensions.json
:表示所需要安装用到的扩展插件.
删了也没事,但是留着也不占地方.
env.d.ts
:这里面的配置代码,主要目的就是让TS认识各种类型的文件. 例如css,sass,jpg等各种类型的文件.
index.html
:项目的入口文件,通过该文件引入的src/main.ts
文件,可以让整个项目跑起来
vite.config.ts
:整个项目的配置文件,可以配置插件,代理等之类的
src/main.ts简单介绍
在 Vue 3 中,createApp 是用来创建一个 Vue 应用实例的工厂函数。import { createApp } from 'vue'
这行代码是导入了应用的根组件。import App from './App.vue'
把vue挂在到指定元素上createApp(App).mount('#app')
结构
选项式API和组合式API
选项式API一般是在Vue2中使用.
白色框这些都属于是配置项,也就是选项式API,学Vue2就等于是在学这些配置项。
选项式API的英文名叫OptionsAPI,但是在Vue3中提出了一种新的组合式API(CompositionAPI) 。Vue2的API是OptionsAPI风格的。Vue3的API是CompositionAPI风格的。
OptionsAPI(选项式API)的弊端就是,数据、方法、计算属性等等都被拆分开了,当你需要改动一个东西的时候,那么你就需要同时在Data、methods等等这些方法里面去修改,不便于修复和维护。
CompositionAPI(组合式API)把一个整体都放在一个函数方法里(Function),这样很符合逻辑而且也方便维护。
Vue3 核心语法
Setup
Setup
是Vue3中的一个新的配置项,它的值是一个函数,他是CompositionAPI
“表演的舞台”,组件中所有用到的数据、方法、计算属性、监视……等,均配置在Setup
里面。
Vue2的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script lang="ts">
export default {
name:'Person',
data(){
return{
name:'张三',
age:18,
tel:'12345678901',
}
},
methods:{
showTel() {
alert(this.tel)
}
}
}
</script>
改为Vue3
数据编写方法更集中了,但是如果通过方法来更改参数的值,那么会改变值但就不是响应式数据了,就无法同步到页面中。
1
2
3
4
5
6
7
8
9
10
11
setup() {
// setup函数中的this是undefined,因为setup函数本身没有this,vue3开始弱化this了。
// 此时的name、age、tel都不是响应式的数据。
let name = '张三'
let age = 18
let tel = '123456789'
function showTel() {
alert(tel)
}
return {name,age,showTel}
}
setup函数的返回值也可以直接是一个渲染函数,可以直接指定渲染的内容,例如
1
2
3
4
setup() {
return () => '你好'
}
此时的页面上就会只显示你好
这两个字,之前写的div之类的都没有用了,但是这个是不常用的。
Vue2中的语法比如data、methods是可以和vue3中的setup共存的,但是vue2的data、methods等,是可以通过this来调用setup里面的数据的,但是setup调用不了vue2其中的数据。
setup语法糖
语法糖说白了就是更简单的写法。
在script标签中,我想要无非就是数据以及函数方法,但是每个都还要进行导出,假如数据量比较大,其中一个我忘记return导出了,那岂不是出现很大的问题了,而且多了要一一导出代码也看着比较臃肿,所以就有了这个东西setup语法糖
,至于为什么叫这个,我估计是这样写起来会有种尝到甜头的感觉,很方便。
于是代码就变成了这样,所有的数据以及函数都不需要导出了,就很方便。
name名称插件
这样写看着也有些麻烦,写了两个script
,但可以通过一个官方插件来缩短成一个script
,开发依赖,所以加上-D
1
npm i vite-plugin-vue-setup-extend -D
然后在vite.config.ts
中引用,注意两个白线的地方。
此时就可以只写一个script
了
这种写name
的情况可以应对,有时候公司的组件会写成文件夹名/index.vue
,这种情况,就可以使用name来区分。
响应式数据
ref创建基本类型响应式数据
在vue2中,你的数据只要是写在data()里面,那么就默认是响应式数据。
- ref
reactive可以构建对象类型响应式数据也可以构建基本类型响应式数据。
首先引用ref
1
import {ref} from 'vue'
然后你想让哪个数据变成响应式数据就用ref包裹住。
1
let name = ref('张三')
此时name就已经变成了一个响应式数据,所谓响应式数据就是你在修改这个name
值的时候,在页面上会同步更新,很方便。
此时如果console.log
打印一下name
的话,那么就会发现已经变成了一个对象,但是在引用的时候不需要加.value
,因为vue3会自动帮你加。
但是在JS以及TS中!必须要写.value
!不然将会无法操作这个值!
ref也可以对对象类型进行创建响应式数据,和reactive一样,也是直接包裹住即可,但是当你在JS以及TS中进行调用的时候,值得注意的是
.value
的位置
1
2
3
4
5
6
7
8
9
// 对象
function changePrice() {
car.value.price += 10;
}
// 数组
function changeArr() {
arr.value[0].name = '王五'
}
reactive 构建对象类型响应式数据
1
import { reactive } from 'vue';
此时对于一个对象类型的数据,也变成响应式数据了
1
2
3
4
let arr = [
{ name: "张三", age: 18 },
{ name: "李四", age: 20 },
];
数组也是同样的包裹,此时该数组也变成响应式数组了,可以直接更改arr[0].name = '王五'
,此时页面也会进行渲染。
reactive也是深层次的,不管数据内嵌套了多少层,知道找到了就可以变成响应式。
- reactive包裹的对象打印出来一般是
Proxy
开头。 - 而ref包裹的对象打印出来一般是
RefImpl
开头。
ref
表面上什么类型都可以处理,但是对于对象类型,底层还是通过reactive
实现的。
ref和reactive的区别
ref的.value
每次都要手写,就很麻烦,如果忘了可能又是一个bug,所有volar插件提供了一个功能,可以在vscode的设置中找到
一开始是没有勾选的,给它勾上就行了。
reactive重新分配一个对象,就会失去响应式。
1
2
3
4
5
6
let car = reactive({ brand: "奔驰", price: 200 });
// 修改整个对象
function changeCar() {
car = {brand: "仰望",price:220}
}
如果这样,那么car将会失去响应式数据效果,那么应该如何解决这种情况。
只需要使用Object对象的assign方法
进行整体替换,如果有多个对象那么都是添加到第一个参数上。
1
2
3
4
5
6
let car = reactive({ brand: "奔驰", price: 200 });
// 修改整个对象
function changeCar() {
car = Object.assign(car,{brand: "仰望",price:220})
}
但是如果使用的是ref进行定义响应式数据,就可以直接进行更改,可以理解为只要是带.value
那么就必然是响应式数据,无论基础类型还是对象类型。
torefs和toref
torefs:把一个reactive所定义的所有对象都变成一个ref所组成的对象。
1
2
3
4
5
6
let person = {
name:'张三',
age:18
}
let {name,age} = toRefs(person)
上面的代码,如果在 =
后面没有加toRefs
那么name和age将不会是响应式数据,并且修改nage和age也会同步修改person.name和person.age。
toref:可以单独拎出来reactive定义的对象中的一个数据,并且也变成ref响应式数据。
computed计算属性
在Vue3中computed
是一个函数,需要引用使用,computed
拥有缓存,比function函数要聪明一些,例如要进行计算一个名称首字母大写。
1
2
3
4
5
6
7
import { ref,computed } from 'vue';
let firstName = ref('zhang')
let lastName = ref('san')
let fullName = computed(()=> {
let full = firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + ' ' + lastName.value
return full
})
此时在页面上调用fullName
即可出现首字母大写的名字组合,如果多次调用也只会计算一次,因为当computed是带有缓存的,但是这个是只读的,不能修改。
可读可写响应式数据需要调用get、set方法,set是带参数的,参数则是fullName的值
1
2
3
4
5
6
7
8
9
let fullName = computed({
get(){
return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + ' ' + lastName.value
},
set(val){
firstName.value = val.split(' ')[0]
lastName.value = val.split(' ')[1]
}
})
简单来说就是如果需要从多个数据中重新计算出来一个新的,就用computed
计算属性就可以了,计算属性计算出来的数据也是一个ref响应式数据
watch 监视
1.监视ref定义的基本类型数据
首先还是引用watch,然后定义一个sum = ref(0)
1
2
3
watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
})
当你改变了sum值后,数据结果为
1
2
3
sum变化了 1 0
sum变化了 2 1
sum变化了 3 2
重点:在vue3中watch
函数返回一个用于停止监控的函数,这个返回的函数就是用来取消监控的,从而停止执行回调函数。
例子:当sum大于10的时候停止监控
1
2
3
4
5
const stopWatch = watch(sum,(newValue,oldValue)=>{
if (newValue > 10) {
stopWatch();
}
})
2.监视ref定义的对象类型数据
监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视。
前面使用的时候只调用了watch函数的前两个参数,开启深度监视就需要添加第三个参数,假设sun是一个对象数据,那么代码为:
1
2
3
watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
},{deep:true}) // 深度模式为真
这样无论是修改整个对象数据,还是修改对象属性都会被监视。
第三个参数,是类似于配置项的参数,有多个配置,比如
1
2
// deep:深度模式 immediate:立即监视
{deep:true,immediate:true}
watch的第一个参数:被监视的数据
watch的第二个参数:监视的回调
watch的第三个参数:配置对象(deep、immediate等…)
newValue**,oldValue打印出来的值是相同的,这个问题的原因是地址值没有变,在打印出来的时候还是原来的地址,所以会相同,房子没换,换了壁纸和沙发。
3.监视reactive定义的对象类型数据
reactive是默认开启深度监视的,所以可以直接监视对象内部属性的变化,无需开启深度模式,但是也是不可以关闭的,官方说话:隐式创建深度模式。
watch函数和function一样都可以写多个。
4.监视ref或reactive定义对象类型数据中的某个属性
若监视对象类型的某个属性是基本类型,需要写成函数形式(箭头函数)
可以在watch的第一个参数中,使用箭头函数来监视对象中的具体的一个值,比如
1
2
3
watch(() => {return state.count}, (newValue, oldValue) => {
console.log(`count 值从 ${oldValue} 更新为 ${newValue}`);
});
但是大括号和return是可以省略的于是就可以直接
1
2
3
4
5
6
7
8
9
10
11
12
import { watch, reactive } from 'vue';
const state = reactive({
count: 0,
message: 'Hello!'
});
watch(() => state.count, (newValue, oldValue) => {
console.log(`count 值从 ${oldValue} 更新为 ${newValue}`);
});
// 修改 count 的值会触发 watch 的回调
state.count++;
但是如果监视的对象数据的属性是对象类型,可以直接写,也可以写成函数,但是建议写成函数,如果要监视对象类型的属性的细枝末节内部变化,需要手动开启深度模式deep:true
。
5.监视上述的多个数据
监视多个数据,通过使用数据包裹起来即可,还是普通数据用函数,对象数据建议函数也可以直接使用。
1
2
3
4
// 监视,情况五:监视上述的多个数据
watch([()=>person.name,person.car](newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
watchEffect
- 官网:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数。
就是刚开始就会执行一次,你不需要明确给我提出具体要监控谁,你要用到谁我就自动监控谁。
watch
对比watchEffect
- 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
watch
:要明确指出监视的数据watchEffect
:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用watch实现,需要明确的指出要监视:temp、height
watch([temp,height],(value)=>{
// 从value中获取最新的temp值、height值
const [newTemp,newHeight] = value
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if(newTemp >= 50 || newHeight >= 20){
console.log('联系服务器')
}
})
// 用watchEffect实现,不用
const stopWtach = watchEffect(()=>{
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if(temp.value >= 50 || height.value >= 20){
console.log(document.getElementById('demo')?.innerText)
console.log('联系服务器')
}
// 水温达到100,或水位达到50,取消监视
if(temp.value === 100 || height.value === 50){
console.log('清理了')
stopWtach()
}
})
标签的ref属性
作用:用于注册模板引用。
用在普通
DOM
标签上,获取的是DOM
节点。用在组件标签上,获取的是组件实例对象(函数)。
用在普通html标签上,是直接获取dom元素内容
用在组件标签上,需要通过引入defineExpose
来导出内容
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
<!-- 父组件App.vue -->
<template>
<Person ref="ren"/>
<button @click="test">测试</button>
</template>
<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {ref} from 'vue'
let ren = ref()
function test(){
console.log(ren.value.name)
console.log(ren.value.age)
}
</script>
<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
import {ref,defineExpose} from 'vue'
// 数据
let name = ref('张三')
let age = ref(18)
/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age})
</script>
TS中的接口、泛型、自定义
@
符号会直接跑到项目的根目录,从而方便引入。
接口
在TS使用接口来限制规范,新建一个
引用然后可以看到,如果我们设置的数据和接口设置的不一样,那么就会出错
如果是定义数组的话那就需要用到泛型
1
2
3
4
5
let personList:Array<Person> = [
{ id: "13", name: "张三", age: 18 },
{ id: "14", name: "李四", age: 19 },
{ id: "15", name: "王五", age: 20 }
]
代码意思是,数组中的每一项都要符合这个数据规范,所以用到了Array,多个泛型在Person
后面直接加就行,比如:Array<Person,Student,Order>
还有一种方法就是用到自定义类型
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个结构,用于限制person对象的具体属性
export interface Person {
id:string,
name: string,
age: number
}
// 定义一个自定义类型,用于处理数组对象的具体属性
export type Persons = Array<Person>
//or
export type Persons = Person[]
可选
在定义接口的时候,如果有些属性是可选的可以后面+?
,比如年龄可选,此时加上?
以后,age
参数可以不写
1
2
3
4
5
6
// 定义一个结构,用于限制person对象的具体属性
export interface Person {
id:string,
name: string,
age?: number
}
如果是响应式数据reactive,可以这样
props
首先引入defineProps
1
import {defineProps} from "vue"
然后就可以接收父组件传过来的内容,必须要用数组来接收。
1
2
3
4
// 只接收
defineProps(["list"])
//接收同时保存props,此时的x会把所有props返回值都接收
let x = defineProps(["list"])
只接收+限制类型
这样更加严谨,标明我需要收到一个list,并且这个list必须是符合之前写的Persons接口标准的list,当开启这种限制以后,如果父传过来一个别的数据那么是传不过来的,切父组件必须传list过来。
1
2
3
import {type Persons} from "@types"
defineProps<{list:Persons}>(["list"])
接收 + 限制类型 + 限制必要性 + 指定默认值
当在list后面加上了一个?
就可以让父组件可传可不传了。
但是如果不传数据,可以加一个默认值,在vue中引入withDefaults
1
2
3
4
5
import {withDefualts} from "vue"
import {type Persons} from "@types"
withDefults(defineProps<{list?:Persons}>(),{
list:()=> [{ id: "12", name: "张三", age: 18}]
})
这种做法通常用于在组件中定义默认的 props 值,以防止在使用组件时没有传入 props 导致错误。如果组件接收到了 list
prop,它将使用传入的值;如果没有传入 list
prop,则会使用默认提供的值 [{ id: '12', name: '张三', age: 18 }]
。
define开头的函数,其实可以不用引入,因为是宏函数可以直接拿来用。
生命周期
组件的生命周期(生命周期函数、生命周期钩子)
时刻 | 调用特定的函数 |
---|---|
创建 | created |
挂载 | mounted |
更新 | |
销毁 |
vue2的生命周期
beforeCreate(创建前):
- 你可以在这个阶段做些准备工作,但是此时还没有真正创建 Vue 实例。也就是说,Vue 实例还没有被初始化。
created(创建后):
- Vue 实例被创建好了,但是此时还没有开始创建真正的 DOM。你可以在这个时候做一些数据的初始化工作,或者请求一些初始数据。
beforeMount(挂载前):
- 这个阶段是在 Vue 开始把你的页面内容放到 DOM 中之前调用的。你可以在这里做一些DOM准备工作。
mounted(挂载后):
- 这时候 Vue 实例已经挂载到了页面上,你的页面已经显示了 Vue 组件的内容。通常你可以在这里做一些需要操作 DOM 的工作,比如初始化页面后的一些动画效果。
beforeUpdate(更新前):
- 在数据更新但页面尚未重新渲染时调用。你可以在这里做一些在更新前需要的操作,比如对比更新前后的数据。
updated(更新后):
- 数据已经更新,页面也已经重新渲染完毕。你可以在这个阶段做一些需要最新数据的操作,比如更新一些在页面上显示的内容。
beforeDestroy(销毁前):
- 当你要销毁 Vue 实例之前调用。你可以在这里做一些清理工作,比如取消定时器、移除绑定的事件等。
destroyed(销毁后):
- Vue 实例已经被销毁了,这时候你的 Vue 实例上的所有东西都被清理掉了。你可以在这里做一些最后的清理工作,确保你的组件在销毁时没有留下任何垃圾。
这些生命周期函数钩子的调用顺序是这样的:
- 创建阶段:
beforeCreate
->created
- 更新阶段:
beforeUpdate
->updated
- 销毁阶段:
beforeDestroy
->destroyed
vue3生命周期
创建
vue3的创建阶段已经包含在了setup
函数中,所以可以直接在setup
进行打印输出查看。
1
2
3
4
5
<script setup lang="ts" name="Person">
console.log("创建")
</script>
挂载
首先引入,其实就是vue2中的beforeMount
前面加了个on,然后B大写了。
挂载完毕也同样是。
1
import {onBeforeMount,onMounted} from 'vue'
vue3在挂在会会调用onBeforeMount
函数所指定的函数,所以需要传入一个回调函数。
1
2
3
4
5
6
7
8
// 挂载前
onBeforeMount(()=>{
console.log("挂载前")
})
// 挂载完毕
onMounted(()=>{
console.log("挂载后")
})
在父子组件中都有挂载函数时,子组件先挂载完毕。
更新
跟之前同样,也是前面+on
1
import {onBeforeUpdate,onUpdated} from 'vue'
同样原理。
1
2
3
4
5
6
7
8
// 更新前
onBeforeUpdate(()=>{
console.log("更新前")
})
// 更新完毕
onUpdated(()=>{
console.log("更新完毕")
})
卸载
在vue2中叫做销毁,但是在vue3中就叫卸载
1
import {onBeforeUnmount,onUnmounted} from 'vue'
同样原理。
1
2
3
4
5
6
7
8
// 卸载前
onBeforeUnmount(()=>{
console.log("卸载前")
})
// 卸载完毕
onUnmounted(()=>{
console.log("卸载完毕")
})
卸载的效果可以通过v-if
或v-show
来确定展示与否来测试。
自定义hooks
使用axios来请求一个数据
1
2
3
4
5
6
7
8
async function getData() {
try{
let result = await axios.get("api地址")
List.push(result.data.message)
} catch (error) {
alert(error)
}
}
async和await解析
解析:这是一个名为 getData
的异步函数,主要用于从某个 API 地址获取数据。
async function getData() { ... }
这是一个异步函数声明,async
关键字表示这个函数会返回一个Promise
对象。这意味着函数内部可以使用await
关键字,它可以等待一个异步操作(如HTTP请求)完成,然后再继续执行。
try { ... } catch (error) { ... }
try
块用于捕获可能发生的异常。如果异步操作成功,代码将正常执行;如果发生错误,控制流将跳转到catch
块来执行代码。
let result = await axios.get("api地址")
这行代码是异步操作的核心,使用了 Axios 库发送一个 GET 请求到指定的 API 地址,并使用 await
暂停等待响应结果,直到HTTP请求完成。请求完成后,响应数据被赋值给变量result
。
List.push(result.data.message)
这一行将 API 响应数据中的 message
属性值添加到一个名为 List
的数组中。它将result
对象中的data
属性的message
字段添加到List
数组中。这通常用于将获取的数据存储起来,以便后续使用。
catch (error) { alert(error) }
如果在执行 axios.get
时发生了错误,就会执行 catch
代码块中的语句,这里是使用 alert
函数在浏览器中弹出一个警告框,显示错误信息。error
变量将包含错误信息。
hooks的功能就是,让一个功能数据、方法都贴在一起,从而方便阅读。
一般是以useXXX
来命名,比如useOrder
、useSum
。
具体就是新建一个hooks文件夹,然后把每一个函数的功能以及所用到的数据,都放在这个以useXXX
来命名的.ts
文件里,代码正常写,但是需要默认导出,以及最后return
一个值。
hooks/useOrder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 正常引用所需要的东西
import {reactive} from 'vue'
// 最好默认导出
export default function() {
// 数据
...
// 方法
...
// 钩子
...
// 计算属性
...
// 向外部提供东西,一般是数据、方法等。
return {orderList,getOrder}
}
然后在原文件中进行调用
1
2
// 首先引入,多个的话就挨个引入多个
import useOrder from '@/hooks/useOrder'
因为hooks的本质就是一个一个可以调用的函数,所以可以直接解构,因为useOrder
是有返回值的。
1
const {orderList,getOrder} = useOrder()
这样写的好处在于,方便维护,假如以后要维护这个订单相关的代码,那么只需要维护好这个订单hooks就好了。
因为有了hooks,那么compositionAPI才发挥出了真正的威力。
路由
首先说明,站在程序员的角度,路由是路由,路由器是路由器。
路由的英文名叫:route
,路由器的英文名叫:router
,带r
的是路由器,不带的是路由,这两个有很大的区别。
路由就是一组key-value的对应关系,多个路由需要经过路由器的管理,一般在SPA应用上比较多。
SPA应用简单来说就类似于后台管理系统的那种单页面切换。
路由有一个核心的东西就是路径在变localhost:8080/xxx
。
学习路由使用步骤
- 导航区、展示区
- 请来路由器
安装路由
1
npm i vue-router
涉及到路由,就必须有router
这个文件夹,因为我们使用标准文件划分,并且指定制定路由事要想好路由器的工作模式。
Vue Router 提供了两种路由模式:history
和 hash
。这两种模式主要区别在于 URL 的显示方式和浏览器历史记录的管理方式。
**Hash 模式(哈希模式)**:在这种模式下,URL 中会包含一个
#
符号,例如http://example.com/#/home
。这个#
后面的部分(hash)不会发送到服务器,它只是客户端用来跟踪应用状态的一种方式。当用户点击浏览器的后退按钮时,应用会根据 URL 中的 hash 值来显示相应的视图。createWebHashHistory
History 模式:这种模式提供了一种更自然的 URL 体验,没有
#
符号,例如http://example.com/home
。在这种模式下,Vue Router 会利用 HTML5 History API 来实现干净的 URL。这意味着 URL 的变化会被浏览器记录在历史记录中,用户可以使用浏览器的前进和后退按钮来导航应用的不同视图。createWebHistory
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
// 创建一个路由并且暴露出去
// 第一步:引入createRouter
import {createRouter,createWebHistory} from 'vue-router'
// 引入一些可能要呈现的组件
import Home from '@/components/Home.vue'
import News from '@/components/News.vue'
import About from '@/components/About.vue'
// 第二步:创建路由器,一个路由器可以管理多个路由
const router = createRouter({
// 路由器工作模式 告诉vueRouter 使用history模式
history:createWebHistory(),
// 写一个名为routes的配置项
routes:[
{
path:'/home',
component:Home
},
{
path:'/news',
component:News
},
{
path:'/about',
component:About
}
]
})
// 最后暴露出去router
export default router
然后回到main.ts
中去引用路由
src/main.ts
1
2
3
4
5
6
7
8
9
10
// import ...
// 文件名为index可以省略
import router from './router'
// 使用路由器
app.use(router)
// 挂载整个应用到app容器中
app.mount('#app')
此时配置完毕以后,就已经用拥有了路由环境。
展示文件,RouterView
用于展示文件,RouterLink
用于切换文件。
RouterView
会自动把切换到的.vue
文件进行展示。
RouterLink
用于切换,其中
to="/xxx"
用于写切换路径
active-class="css样式"
表示被激活时候的类名,加上一个css样式,然后点击别的时候会自动去掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="app">
<h2 class="title">Vue路由测试</h2>
<!-- 导航区 -->
<div class="navigate">
<RouterLink to="/home" active-class="active">首页</RouterLink>
<RouterLink to="/news" active-class="active">新闻</RouterLink>
<RouterLink to="/about" active-class="active">关于</RouterLink>
</div>
<!-- 展示区 -->
<div class="main-content">
<RouterView></RouterView>
</div>
</div>
</template>
<script lang="ts" setup name="App">
import {RouterLink,RouterView} from 'vue-router'
</script>
两个注意点
路由组件通常存放在
pages
或views
文件夹,一般组件通常存放在components
文件夹。路由组件:靠路由规则渲染出来的,比如以下这是三个,一般是放在
pages
或views
文件夹里(视图)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
routes:[
{
path:'/home',
component:Home
},
{
path:'/news',
component:News
},
{
path:'/about',
component:About
}
]
一般组件:自己动手写出来的组件例如:<Demo/>
- 通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载。
components组合成为view,一个页面(page)由多个view组合(例如导航栏、显示区等)而成。
路由器工作模式
history
模式优点:
URL
更加美观,不带有#
,更接近传统的网站URL
。缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404
错误。history:createWebHistory()
hash
模式优点:兼容性更好,因为不需要服务器端处理路径。
缺点:
URL
带有#
不太美观,且在SEO
优化方面相对较差。history:createWebHashHistory()
to的两种写法
1
2
3
4
5
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>
命名路由
作用:可以简化路由跳转及传参。
给路由规则命名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
routes:[
{
name:'zhuye',
path:'/home',
component:Home
},
{
name:'xinwen',
path:'/news',
component:News,
},
{
name:'guanyu',
path:'/about',
component:About
}
]
跳转路由:
1
2
3
4
5
<!--简化前:需要写完整的路径(to的字符串写法) -->
<router-link to="/news/detail">跳转</router-link>
<!--简化后:直接通过名字跳转(to的对象写法配合name属性) -->
<router-link :to="{name:'guanyu'}">跳转</router-link>
嵌套路由
增加一个children
,注意里面的path路径是不需要加/
的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const router = createRouter({
history: createWebHistory(),
routes:[
{
...
},
{
name:'xinwen',
path:'/news',
component:News,
children:[
name:'xinwenneirong'
path:'detail',
component:Detail
]
}
]
})
在使用RouterLink
跳转时,to
的路径要写完整。
1
2
3
<Router-Link to="/news/detail">xxxx</Router-Link>
<!-- 或 -->
<Router-Link :to="{path:'/news/detail'}">xxxx</Router-Link>
一般组件用大驼峰,参数等命名用小驼峰。
路由传参
如果数据在父组件上,但是子组件需要展示,通过父组件向子组件传递参数方式有两种。
1.query参数
前情提要:News.vue
是父组件,Detail.vue
是子组件,父组件通过v-for
来渲染新闻标题。
1
2
3
4
5
<ul>
<li v-for="news in newsList" :key="news.id">
<RouterLink></RouterLink>
</li>
</ul>
步骤一:
父组件News.vue
在to
地址的同时传入参数,写法如下
1
<Router-Link to="/news/detail?a=1&b=2&c=3">xxxx</Router-Link>
此时传递了三个参数a,b,c
,多个参数使用&
隔开,这传入的是死数据,而且只是一个字符串。
所以要写成模板字符串,模版字符串则是在双引号""
里加上两个撇号,模版字符串里面嵌入JS
可以通过${}
来实现,并且要把to
变成:to
。
1
<RouterLink :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">
上述写法,一大长串儿,显得很不美观有点冗余,所以我们可以美化一下代码。
1
2
3
4
5
6
7
8
9
10
11
<RouterLink
:to="{
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
</RouterLink>
这样就看起来比较美观,毕竟要做一个格式化工程师,如果在路由中定义了name
参数,那么path
路径可以换成name
名称,更方便。
步骤二:
子组件Detail.vue
接收参数,需要引入useRouter
,可以看出是一个hooks
。
1
import {useRoute} from 'vue-router'
通过定义一个数据来接收useRouter
1
let route = useRoute()
如果你不知道route
里面包含了什么可以通过打印来查看,返回的里面包含了一个query
参数,这个参数里面包含了父组件传过来的值。
引用
1
2
3
4
5
<template>
<div>id={{route.query.id}}</div>
<div>title={{route.query.title}}</div>
<div>content={{route.query.content}}</div>
</template>
如果解构赋值。
1
2
let route = useRouter()
let {query} = route
重点:如果从响应式数据中直接解构属性,那么这个属性就会就此失去响应式,如果不想失去响应式,那么可以使用toRefs
就不会失去了。调用也更加方便即:
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>id={{query.id}}</div>
<div>title={{query.title}}</div>
<div>content={{query.content}}</div>
</template>
<script>
import {toRefs} from 'vue'
let route = useRouter()
let {query} = toRefs(router)
</script>
2.params参数
首先是要在路由中配置占位符,使用/:xxx
来进行占位符,占位符所对应的名称要和传入的参数名称相对应,在后面加上?
该参数可有可无。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const router = createRouter({
history: createWebHistory(),
routes:[
{
...
},
{
name:'xinwen',
path:'/news',
component:News,
children:[
name:'xinwenneirong'
// 后面加上?表示该参数可以没有值
path:'detail/:id/:title/:content?',
component:Detail
]
}
]
})
- 传递参数
这里的跳转只可以使用name
参数,不可以使用path
,如果是query
的话两个都可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 跳转并携带params参数(to的字符串写法) -->
<RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink>
<!-- 跳转并携带params参数(to的对象写法) -->
<RouterLink
:to="{
name:'xinwen', //用name跳转
params:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{news.title}}
</RouterLink>
- 接收参数:
1
2
3
4
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印params参数
console.log(route.params)
备注1:传递
params
参数时,若使用to
的对象写法,必须使用name
配置项,不能用path
。备注2:传递
params
参数时,需要提前在规则中占位。
路由的props配置
简单点就是可以获取值的时候不那么麻烦,响应式数据只写{{ xxx }}
这种语法是最棒的,所以我们就要用这种方法。
props
的功能可以这里么理解,当路由已经找到了并开始渲染<Detail />
组件时,如果我们开启了props
。
那么我们使用params
传递的三个参数就会进行转化为类似于组件传递值的方法,于是就变成了<Detail id=?? title=?? content=?? />
。
于是乎就特别简单了,这些props
参数可以直接使用defineProps
函数接收。
第一种props
第一种使用的是params
来传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// router.ts
{
name:'xinwen',
path:'/news',
component:News,
children:[
name:'xinwenneirong'
// 后面加上?表示该参数可以没有值
path:'detail/:id/:title/:content?',
component:Detail,
// 第一种写法:将路由收到的所有params参数座位props传给路由组件
props:true
]
}
1
2
3
4
5
6
7
8
9
10
11
// Detail.vue
<template>
<div>编号:{{ id }}</div>
<div>标题:{{ title }}</div>
<div>内容:{{ content }}</div>
</templat>
<script setup lang="ts" name="detail">
defineProps(['id','title','content'])
}
</script>
这就叫优雅的代码!!!(但是这个只能和params
参数进行配合,如果是query
就不行了),所以我们可以使用第二种props
写法,函数写法。
第二种props
使用query
传递参数
如果使用的是query
来传值,那么只能用这种办法,当然params
也同样适用,但是params
可以直接使用第一种更加方便。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// router.ts
{
name:'xinwen',
path:'/news',
component:News,
children:[
name:'xinwenneirong'
// 后面加上?表示该参数可以没有值
path:'detail/:id/:title/:content?',
component:Detail,
// 第二种写法:函数写法可以自己决定将什么作为props给路由组件
props(route) {
return route.query
}
]
}
1
2
3
4
5
6
7
8
9
10
11
// Detail.vue
<template>
<div>编号:{{ id }}</div>
<div>标题:{{ title }}</div>
<div>内容:{{ content }}</div>
</templat>
<script setup lang="ts" name="detail">
defineProps(['id','title','content'])
}
</script>
常用的就这两种,第三种是对象写法,就是传递固定的值,几乎不用。
1
2
3
4
5
6
7
8
9
10
11
12
13
// router.ts
...
children:[
name:'xinwenneirong'
// 后面加上?表示该参数可以没有值
path:'detail/:id/:title/:content?',
component:Detail,
props(route) {
a:100,
b:200,
c:300
}
]
这些都是路由组件的传递方式,如果是一般组件(一般组件就是自己可以动手写在代码里的那么可以直接传递props
,比如我们可以直接<Deatil a=1 b=2 c=3/>
)。
路由的replace属性
路由的每次跳转都会形成历史记录,可以在浏览器的左上角的左右箭头进行前进后退,这是因为路由跳转默认是push
模式,如果不想这样,那么可以改成replace
模式,即不能前进也不能后退。
要改为replace
,只需要在RouterLink
标签中,加入replace
属性即可。
1
2
3
<RouterLink replace to="/home">首页</RouterLink>
<RouterLink replace to="/news">新闻</RouterLink>
<RouterLink replace to="/about">关于</RouterLink>
如果想看到某一个具体的就不让他回去的话,那么就可以单独加上replace
。
编程式导航(重要)
我们之前的跳转,全部使用了<RouterLink />
标签进行实现,那么<RouterLink />
标签在最后会被转化为<a/>
标签,这就导致了,如果全部都使用,那么就会出现一堆<a/>
元素。
所谓编程式导航,其实就是脱离<RouterLink />
来实现路由跳转。
打开页面3秒后跳转到新闻页面。
1
2
3
4
5
6
7
8
9
10
11
12
import {onMounted} from 'vue'
// 注意是router 后面加了r
import {useRouter} from 'vue-router'
// 拿到router,相当于掌握了整个路由
const router = useRouter()
onMounted(()=>{
setTimeout(()=>{
router.push("/news")
},3000)
})
编程式路由导航的使用频率要远大于RouterLink
式。
router.push()
和to
的语法是一样的,都有两个模式字符串和对象,而且语法一模一样,直接拿过来用就行,但是参数需要传递过来。
通过按钮点击来查看新内容,那么就可以直接把之前的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
// ts会告诉你news参数有一个隐式any类型,简单方式就是后面直接加:any,不然就是使用interface来限制一下。
function showNewsDeatil(news:any){
router.push({
name:"xiang",
query:{
id:news.id,
title:news.title,
content:news.conten
}
})
}
// or
// 限制有限制的好处,可以让数据更严谨
interface NewInter {
id:string,
title:string,
content:string
}
function showNewsDeatil(news:NewInter){
router.push({
name:"xiang",
query:{
id:news.id,
title:news.title,
content:news.conten
}
})
}
当然了,如果不想使用push
留下痕迹,可以直接router.replace()
进行使用。
路由重定向
作用:将特定的路径,重新定向到已有路由。
我们想一打开页面就直接显示主页等想一开始就显示的内容,顺序不分先后。
1
2
3
4
5
6
7
8
9
10
11
12
13
// router.ts
{
name:'xinwen',
path:'/news',
component:News
},
{
...
},
{
path:'/',
redirect:'/home'
}
连续解构赋值+重命名写法
1
let {data:{content:title}}
↑返回的数据名叫data
,需要从data
中拿到content
值,又把content
名改为title
Pinia
官网:Pinia | The intuitive store for Vue.js (vuejs.org)
官方描述为:符合直觉的Vue.js状态管理哭,说白了就是用着很舒服,没有vuex那么臃肿。
集中式状态管理也叫集中式数据管理,常见的有:react的redux,vue2的vuex,vue3的pinia。
hooks的作用只是把代码集中在一起,而pinia是各个组件之间共享数据。
搭建Pinia环境
1
npm i pinia
引入Pinia
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/main.ts
// 导入Vue
import { createApp } from 'vue'
// 导入App组件
import App from './App.vue'
// 1.引入pinia
import {createPinia} from 'pinia'
// 2.创建pinia
const pinia = createPinia()
// 3.安装pinia
app.use(pinia)
// 创建Vue应用实例
createApp(App).mount('#app')
存储数据+读取数据
首先src
目录下必须有store
文件夹,pinia具体就体现在这里。
里面的文件命名一般是和组件相对应的,比如我们有一个count.vue
组件,那我们可以创建一个store/count.ts
文件,可以把这个文件看做是一个小仓库。
Pinia
官方鼓励我们使用类似hooks
的方式命名,所以我们可以命名为useCountStore
,引入defineStore
,它的第一个参数建议和组件名相同count
,然后在第二个参数就是开始配置。
state
要求要写成一个函数,并且要return
一个对象,可以是字符串也可以是对象,我们这里把sum
进行共享,并默认值设为6
。
1
2
3
4
5
6
7
8
9
10
11
import {defineStore} from 'pinia'
export default const useCountStore = defineStore('count',{
// 真正存储数据的地方
state({
return {
sum:6
}
})
})
count.vue
中不需要定义sum
,我们直接引入提到的小仓库,只要看到引入的目录中带store
,那就是引用了pinia
。
这里有值得注意的一点,如果是自己动手写定义的ref
属性,那么在使用的时候是必要.value
的。
但是如果是在reactive
里面定义的ref
,会自动拆包,也就是不需要.value
了,
1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入countStore
import { useCountStore } from '@/store/count'
// 使用useCountStore,得到一个专门保存count相关的store
conts countStore = useConutStore()
// 以下这两种方式都可以拿到state中的数据
console.log('输出',countStore.sum)
// 输出 6
console.log('输出',countStore.$state.sum)
// 输出 6
我们现在已经拿到了sum
的值,就可以直接正常使用了,并且数据也是共享的。
1
<h2>求和结果:{{ countStore.sum }}</h2>
修改数据三种方式
Pinia是支持直接把数据拿过来就修改的,符合直觉。
第一种修改方式,直接修改,比如我们要修改sum
。
1
2
3
function add() {
countStore.sum += 1
}
对,就是这么简单暴力,可以直接拿来用,这个在vuex
中是不可以的。
第二种修改方式,使用$patch
函数修改。
$patch
是一个实用函数,用于直接修改状态对象(state)的一个或多个属性,属于批量变更,它的好处是执行一次就可以修改多个参数。
1
2
3
4
5
6
7
function modify() {
countStore.$patch({
sum:10,
name:'张三',
address:'北京'
})
}
如果是多个值需要同时发生变化,就推荐使用$patch
,可以达到占用最少的资源。
第三种修改方式 actions
借助actions
实现(actions
中可以编写一些业务逻辑)。
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
import { defineStore } from 'pinia'
export const useCountStore = defineStore('count', {
//actions里面放的是一个一个的方法,用于响应组件中的"动作"(加减都是动作)
actions: {
//加操作
// 这里设定一个number类型的参数来接收传过来的数值
increment(value:number) {
// 操作state里面的数据可以直接使用this
// this就是当前的store,所以当前这个store里面的东西全都有
// 限制加到10不能加了
if (this.sum < 10) {
//操作countStore中的sum
this.sum += value
}
},
// 减操作
decrement(value:number){
if(this.sum > 1){
this.sum -= value
}
}
},
/*************/
})
组件中调用actions
1
2
3
4
5
// 使用countStore
const countStore = useCountStore()
// 调用对应action,加并且传入加的数值
countStore.incrementOdd(n.value)
这里的actions
感觉看起来好像就是正常的操作,还不如第一种来的实在,因为actions
可以完成的在第一种都可以完成,但是actions
中的业务逻辑,是可以进行复用的,所以也很有意义。
storeToRefs
这里想让你在读数据的时候更加优雅一些。
因为我们以上的操作在读取响应式数据的时候都是countStore.sum
等等前面都带一个countStore
,一点儿也不优雅,所以我们可以让他更优雅一些。
可能会想到,直接解构数据就好咯,但是这样不行,这样会导致数据就此丢失响应式,那么又想一下,引入一个toRefs
来包裹一下不就好了,确实可以,而且也可以正常运营,但是代价很大。
因为他会把你的store
中的所有数据,包括函数什么的,全部都变成ref
响应式数据,我们其实只需要数据罢了,所以不要用toRefs
,这样不好。
但是要解决这个问题也很简单,Pinia
也考虑到了这个问题,所以提供了storeToRefs
,大白话直说:这个只会把store
中的数据变成响应式数据给你。
1
2
3
4
import {storeToRefs} from 'pinia'
const {sum,name,address} = storeToRefs(useCountStore())
geeters
geeters
和state
、actions
都是 兄弟,所以他们都是同级,geeters
里面是写函数的,而且必须返回一个值,就相当于计算属性。
在geeters
里面定义的函数,都有一个参数叫state
,所以在getters
中可以直接通过这个参数来调用state
中的数据,也可以直接使用this
,不用this
可以直接写成箭头函数。
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
// 引入defineStore用于创建store
import {defineStore} from 'pinia'
// 定义并暴露一个store
export const useCountStore = defineStore('count',{
// 动作
actions:{
/************/
},
// 状态
state(){
return {
sum:1,
school:'nihao'
}
},
// 计算
getters:{
// 第一种写法
bigAdd(state){
return state.sum + 10
},
// 第二种写法 箭头函数形式
bigSum:(state):number => state.sum * 10,
// 将state中的school变成默认大写
// 后面的:string 就是声明是字符串类型,这是ts的检查不然就默认any
upperSchool():string{
return this.school.toUpperCase()
}
}
})
这里的计算属性,同样是和响应式数据一样,可以拿来直接使用,并且也是可以直接解构出来进行使用。
$subscribe 订阅的使用
每个store
中都有$subscribe
函数,类似于watch
监视。可以直接调用,并且要传入一个函数,如果你不需要用this
那么你可以使用箭头函数,官方文档使用的是箭头函数。
那么$subscribe
的主要作用是,当store
中的数据发生了变化以后,就会调用。
它收到两个参数,mutate
修改前的数据, state
修改后的数据,state
使用频率是最多的。
我们可以把state中的修改后的数据存入浏览器中
1
2
3
countStore.$subscribe((mutate,state)=>{
localStorage.setItem("countList",JSON.stringify(state.countList))
})
JSON.stringify
:用于把内容转化为字符串。JSON.parse
:用于把字符串转回来。
此时我们的数据可以这样定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// store/count.ts
import {defineStore} from 'pinia'
export default const useCountStore = defineStore('count',{
// 真正存储数据的地方
state({
return {
// as string进行断言,保证严谨性
// 这里后面的 或 ||[] 是为了避免第一次运行local storage中没有数据,然后导致错误,所以要加上。
countList:JSON.parse(localStorage.getItem('countList') as string) || []
}
})
})
store的组合式写法
我们之前写的,属于是选项式写法,比如actions
和state
都是写在了一个对象里,我们把defineStore
的第二个参数写成箭头函数,就可以使用组合式store
了,但是记住箭头函数中不可以使用this
。
组合式store
例子,你就把他当做一个setup
函数就可以了。
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
// store/count.ts
import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
// 直接引入
import {reactive} from 'vue'
// 这里第二个参数使用箭头函数形式
export default const useCountStore = defineStore('count',()=>{
// countList就相当于state,可以定义多个。
const countList = reactive(JSON.parse(localStorage.getItem('countList') as string) || [])
// getCount 相当于 actions
async function getCount(){
// 发请求,这个写法是连续解构赋值+重命名
let {date:{content:title}} = await axios.get('api')
// 把请求回来的字符串包装成一个对象
let obj = {id:nanoid(),title}
// 放入数组中
countList.unshift(obj)
}
// 不要忘记return返回值。
return {countList,getCount}
})
组件通信
组件通信就是组件之间互相传递数据,这个玩的6很重要。
props
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
28
29
<template>
<div class="father">
<h3>父组件,</h3>
<h4>我的车:{{ car }}</h4>
<h4>儿子给的玩具:{{ toy }}</h4>
// 在子组件中使用 :传递名="传递数据",这些名字都是可以任意取的没有限制,但是最好还是容易理解一些
//
<Child :car="car" :sendToy="getToy"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
// 数据
const car = ref('奔驰')
// 定义一个用来接收子组件传过来的数据
const toy = ref()
// 方法
// 定义一个方法来接收子组件传过来的数据
function getToy(value:string){
// value的值就是子传过来的值,然后接收就行了
toy.value = value
}
</script>
子组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
// 这里因为defineProps已经拿到了getToy所以可以直接使用,然后把玩具给传入进去,父组件那边正好可以接收。
<button @click="getToy(toy)">玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
const toy = ref('奥特曼')
// 声明接收的数据使用defineProps 无需引入,接收完了之后可以直接拿来使用
defineProps(['car','getToy'])
</script>
父传子,比较容易理解,直接就可以传到子组件中去。
但子传父,就需要父亲先给子传入一个函数,子收到这个函数,在调用这个函数的时候以传参的形式把值传递过去。
自定义事件 custom_event
当你定义函数的时候,有一个默认参数,这个参数就是事件对象,里边包含了很多事件,比如鼠标移动之类的坐标。
概述:自定义事件常用于:子 传 父。
注意区分好:原生事件、自定义事件。
在定义@click
点击事件的时候,传入$event
占位符,即表示这是一个事件对象,那么test
函数的c
参数,就是一个事件对象了。
1
2
3
4
5
6
7
8
9
//template
<button @click="test(5,6,$event)">点击</button>
// ts
function test(a:number,b:number,c:Event){
console.log('print',a,b,c)
}
原生事件:
- 事件名是特定的(
click
、mosueenter
等等) - 事件对象
$event
: 是包含事件相关信息的对象(pageX
、pageY
、target
、keyCode
)
- 事件名是特定的(
自定义事件:
- 事件名是任意名称
- 事件对象
$event
: 是调用emit
时所提供的数据,可以是任意类型!!!
父组件在调用子组件的时候,通过@xxx = "xxx"
来定义一个自定义事件
1
2
3
4
5
<!--在父组件中,给子组件Child绑定自定义事件:-->
<Child @send-toy="toy = $event"/>
<!--注意区分原生事件与自定义事件中的$event-->
<button @click="toy = $event">测试</button>
子组件,需要调用defineEmits
,并且传入一个数组来进行接收
1
2
3
4
5
6
// 声明事件
defineEmits(['send-toy'])
//子组件中,触发事件:
this.$emit('send-toy', 具体数据)
子组件emit('haha',666)
中的这个666在处罚的时候会以参数的形式被传递到父组件中的xyz
函数的value
参数中去,同理666可以替换为ref定义的toy
数据。
另外自定义组件的命名推荐使用肉串命名kebab-case
,比如my-event
mitt
可以实现任意组件通信,有种一拍即合的感觉,学到了就会了,mitt
就像是组件中的中间人。
首先就是要安装mitt
1
npm i mitt
一般是放在utils
或者tools
文件夹中写一个emitter.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
// 引入mitt
import mitt from 'miit'
// 调用miit得到emitter,emitter可以绑定以及触发事件。
const emitter = mitt()
// 绑定事件
emitter.on('abc',(value)=>{
console.log('abc事件被触发',value)
})
emitter.on('xyz',(value)=>{
console.log('xyz事件被触发',value)
})
setInterval(() => {
// 触发事件
emitter.emit('abc',666)
emitter.emit('xyz',777)
}, 1000);
setTimeout(() => {
// 清理事件 全全部绑定的都清理掉了
emitter.all.clear()
}, 3000);
// 暴露emitter
export default emitter
mitt
提供了四个主要方法:
- on(event, handler)
- 绑定事件
- off(event, handler)
- 解绑事件
- emit(event, …args)
- 触发事件
- all.clear()
- 清除全部事件
接收数据的组件中:绑定事件、同时在销毁前解绑事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
// 绑定事件
emitter.on('send-toy',(value)=>{
console.log('send-toy事件被触发',value)
})
// 在组件卸载时解绑send-toy事件,可以释放内存,很必要。
onUnmounted(()=>{
// 解绑事件
emitter.off('send-toy')
})
提供数据的组件,在合适的时候触发事件
1
2
3
4
5
6
import emitter from "@/utils/emitter";
function sendToy(){
// 触发事件
emitter.emit('send-toy',toy.value)
}
注意这个重要的内置关系,总线依赖着这个内置关系
v-model通信传值
ui组件库的底层,大量使用v-model进行通信,这里是v-model的底层原理。
v-model是双向绑定,它的底层原理是一个动态的value值:value='xxx'
,加上@input='xxx = $event.target.value'
$event:事件对象,target,发生事件的本体,value就拿到了他的值。
v-model用在html标签上
1
2
3
4
5
6
7
// 两个写法是等价的
// v-model写法
<input type='text' v-model='username'>
// 原生写法
<input type='text' :value="username" @input="username = $event.target.value">
v-model用在组件标签上
我们正常使用的是上面的写法,但是最终会被转化成为下面的写法。
组件接收使用
1
2
<input type='text' :value="modelValue"
@input="emit('update:modelValue = $event.target.value')" >
1
2
defineProps(['modelValue'])
const emit = defineEmits([update:modelValue])
$event就是dom事件对象,一般在组件标签上使用 $event,而且HTML原生标签中使用 $event.target用来获取DOM对象。
$event到底是啥?啥时候能.target?
对于原生事件,$event就是事件对象 => 能.target
对于自定义事件,$event就是触发事件时,所传递的数据 不能.target
v-model既能父传子也能子传父。
v-model多个写法
即可以在一个标签上可以写多个v-model
,写法如下,即可以传入多个值。
1
<el-input v-model:mingcheng='username' v-model:mima='password' />
$attrs
概述:
$attrs
用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。
可以理解父组件通过子组件来传递给孙组件。具体说明:
$attrs
是一个对象,包含所有父组件传入的标签属性。注意:
$attrs
会自动排除props
中声明的属性(可以认为声明过的props
被子组件自己“消费”了)
简单来说,就是你传了a、b、c三个值,但是只有a被props
接收了,那么剩下的b、c就都存在attrs
里面。
可以直接在模板上通过{{ $attrs }}
来查看
单项传递数据
父组件
v-bind="{x:100,y:200}"
== :x=100 :y=200
这两个写法是一样的,如果子组件不进行使用这些数据,那么可以直接在孙组件标签上添加v-bind="$attrs"
,来直接传递给孙组件。
然后孙组件就可以通过defineProps
来直接接收父组件的东西了。
如果要孙组件传递给父组件,那么在传入的时候加入一个回调函数,当然这个函数也会被包含在$attrs
中,孙组件直接接收进行使用就可以了。
1
2
3
4
5
6
7
8
9
// 父组件
let a = ref(1)
updateA(value:number){
a.value += value
}
// updateA="updateA" 在子组件标签中传入
// 孙组件接收后调用,加入给函数传入一个数字6,那么在子组件调用一次,那么a这个数据就会+6,无论是在父组件还是子组件是。
$refs 和 $parent
概述:
$refs
用于 :父→子。$parent
用于:子→父。
原理如下:
属性 说明 $refs
值为对象,包含所有被 ref
属性标识的DOM
元素或组件实例。$parent
值为对象,当前组件的父组件实例对象。
通过父组件来改子组件中的内容,可以在父组件中给子组件标签打上ref='xxx'
记号,然后在子组件通过宏函数把数据导出,导出后此时的ref
中的xxx
,就可以拿到子组件导出来的数据了,然后就可以进行操作了。
1
2
// 把数据交给外部
defineExpose({数据铭,函数名})
$refs
可以直接调用,里面包含了所有通过ref
打上标签的子组件。
假如父组件中引入了两个子组件,那么通过一个普通的按钮来创建一个点击方法,这个方法里面的参数传入$refs
,然后在就可以打印出来这两个子组件的实例对象
两个子组件中有相同的一个数据,想让他们每次点击都+2个。
1
2
3
<button @clikc="getAllChild($refs)" />
<Child1 ref="c1")/>
<Child2 ref("c2")/>
1
2
3
4
5
6
7
8
// 按钮中单击事件的函数,这个refs的确是一个对象,然后里面的key我可以确定是一个字符串,里面存的东西我不确定所以用any,但是可以any一把梭哈
function getAllChild(refs:{key:string:any}){
// 遍历出来每个组件中的相同那个数据,然后+2
for (key in refs) {
refs[key].number += 2
}
}
反过来$parent
就是拿到父亲的数据,用法都一样,当然了父组件也要加上defineExpose({数据名})
进行向外部提供数据。
provide、inject 很好用
- 概述:实现祖孙组件直接通信,不打扰子组件。
- 具体使用:
- 在祖先组件中通过
provide
配置向后代组件提供数据 - 在后代组件中通过
inject
配置来声明接收数据 - 这两个都不带响应式。
- 在祖先组件中通过
具体编码:
【第一步】父组件中,使用provide
提供数据。
引入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
28
29
30
<template>
<div class="father">
<h3>父组件</h3>
<h4>资产:{{ money }}</h4>
<h4>汽车:{{ car }}</h4>
<button @click="money += 1">资产+1</button>
<button @click="car.price += 1">汽车价格+1</button>
<Child/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref,reactive,provide } from "vue";
// 数据
let money = ref(100)
let car = reactive({
brand:'奔驰',
price:100
})
// 用于更新money的方法
function updateMoney(value:number){
money.value += value
}
// 向后代提供数据
// 数据 方法 对象 都提供出去,注意响应式数据不需要.value,如果加上那么就不会是响应式数据了。
provide('moneyContext',{money,updateMoney})
provide('car',car)
</script>
函数用于孙传父
注意:子组件中不用编写任何东西,是不受到任何打扰的
【第二步】孙组件中使用inject
配置项接受数据。
inject
第一个参数是接收的名称,和父组件提供的名称一样;第二个参数是默认值,即如果没有接收到就是用默认的替代;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>资产:{{ money }}</h4>
<h4>汽车:{{ car }}</h4>
<button @click="updateMoney(6)">点我</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue';
// 注入数据 可以写多个 并且可以直接解构
let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
// 数据名 默认值
let car = inject('car',{brand:'奔驰',price:100})
</script>
slot 插槽
一般是同时呈现多个组件,但是组件里面的内容只有些许的不一样,这样就不用写多个组件,只需要用一个组件来完成。
默认插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
父组件中:传递一个列表过去
<Category title="今日热门游戏">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
子组件中:
<template>
<div class="item">
<h3>{{ title }}</h3>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
在父组件中使用子组件,并且使用了双标签,可以在双标签的中间夹一些html标签,并且这些标签会在子组件中的<slot></slot>
标签的位置进行渲染。
假如你什么数据也没传过来,可以通过<slot>默认内容</slot>
这样当你没夹杂标签就会显示默认的内容。
这样当我们实现最上面图片的需求时,我们只需要写三个子组件标签,并夹杂着不同的内容带过去,这些不同的内容都被放在了子组件中的<slot></slot>
标签中。
注意slot
标签做的是呈现,比如样式什么的还是在父组件中进行修改比较好,并且只需要写一个slot
标签就行,写多个就会呈现多次。
简单理解:你在子组件中挖了一个坑,这个坑等着你来填土;这三个东西都可以填进来。
具名插槽
就是给坑取名字,然后让对应的名字到对应的坑中。
子组件的slot
使用name
来取一个名字,默认名为:default
1
2
3
4
<template>
<slot name='s1'></slot>
<slot name='s'></slot>
</template>
父组件把标签都放到template
中,然后通过v-slot:x
来命名,也可以通过#x
来命名,可以写多个;注意template
是写在组件标签内的。
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<Chirld>
<template v-slot:s>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
// 这里使用 '#' = 'v-slot'
<template #s1>
<h2>标题</h2>
</template>
</Chirld>
</template>
在子组件中通过名字来引用,在子组件中s1
名称对应的标题会在上方,而s
对应的会在下方。样式之类的只需要在父组件中进行设置就可以了。
作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News
组件中,但使用数据所遍历出来的结构由App
组件决定)
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
父组件中:
<Game v-slot="params">
<!-- <Game v-slot:default="params"> -->
<!-- <Game #default="params"> -->
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>
子组件中:
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈" x="hello"></slot>
</div>
</template>
<script setup lang="ts" name="Category">
import {reactive} from 'vue'
let games = reactive([
{id:'asgdytsa01',name:'英雄联盟'},
{id:'asgdytsa02',name:'王者荣耀'},
{id:'asgdytsa03',name:'红色警戒'},
{id:'asgdytsa04',name:'斗罗大陆'}
])
</script>
在父组件中的子组件标签中v-slot="params"
表明params
作为一个变量名接收了子组件slot标签<slot :games="games" a="哈哈" x="hello"></slot>
里面传递过来的games
、a
、x
三个参数、即为所有参数。
对于命名还是和之前一样,比如:v-slot:s="params"
,子组件:name='s'
。
数据的维护什么都在子组件中,但是根据这些数据所生成的结构是父组件决定的。
简单理解:压岁钱在孩子那,但是根据压岁钱买的东西,却由父亲来决定。
其他API
shallowRef 与 shallowReactive
需要从vue
中引用
shallowRef
- 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
- 用法:
1
let myVar = shallowRef(initialValue);
- 特点:只跟踪引用值的变化,不关心值内部的属性变化。
简单理解:我可以对person.value
进行修改值,并且有响应式处理,但是我对person.value.name
就没有响应式处理了,就是只可以.
一次。
如果你关注的是数据的整体修改,那么就使用shallowRef
。
shallowReactive
同理。
作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的。
通过使用
shallowRef()
和shallowReactive()
来绕开深度响应。浅层式API
创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
readonly 与 shallowReadonly
都是需要通过vue来引用的,然后直接使用。
readonly
- 作用:用于创建一个对象的深只读副本,限制所有层次。
- 用法:
1
2const original = reactive({ ... }); const readOnlyCopy = readonly(original);
当你在修改original
的时候,那么readOnlyCopy
的值也会跟着响应式进行变化,是有关联关系的,但是如果直接修改readOnlyCopy
则无法修改,会提示警告.
相当于复制了一份只能用来使用但是不能修改的数据。
- 特点:
- 对象的所有嵌套属性都将变为只读,并且readonly的参数必须是响应式数据(ref,reactive)。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
- 应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
shallowReadonly
与 readonly
类似,但只作用于对象的顶层属性,就是只限制第一层,更深层次的就不限制了。
1
2
const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);
toRaw 与 markRaw
需要从vue
中引用。
toRaw
函数用于获取由 ref
、reactive
、readonly
等创建的响应式对象的原始未代理的对象。当你需要访问到对象的原始数据,而不是其响应式代理时,就可以使用 toRaw
; toRaw
返回的对象不再是响应式的,不会触发视图更新。
语法:const raw = toRaw(proxy)
官网描述:这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
何时使用? —— 在需要将响应式对象传递给非
Vue
的库或外部系统时,使用toRaw
可以确保它们收到的是普通对象
1
2
3
4
5
6
7
import { reactive,toRaw,markRaw,isReactive } from "vue";
/* toRaw */
// 响应式对象
let person = reactive({name:'tony',age:18})
// 原始对象
let rawPerson = toRaw(person)
markRaw
作用:标记一个对象,使其永远不会变成响应式的。
例如使用
mockjs
时,为了防止误把mockjs
变为响应式对象,可以使用markRaw
去标记mockjs
1
2
3
4
5
6
7
8
9
/* markRaw */
let citys = markRaw([
{id:'asdda01',name:'北京'},
{id:'asdda02',name:'上海'},
{id:'asdda03',name:'天津'},
{id:'asdda04',name:'重庆'}
])
// 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了,citys2会没有作用。
let citys2 = reactive(citys)
customRef
需要从vue
中引用。
自定义Ref作用:创建一个自定义的ref
,并对其依赖项跟踪和更新触发进行逻辑控制。
普通的ref定义的数据,只要数据一变化,页面立马就更新,假如想要间隔一秒变化,那就办不到。
customRef实现修改数据一秒后页面变化:
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
import {customRef} from 'vue'
// 定义一个默认值
let initValue = '你好'
// 定义一个时间
let timer:number
// 使用Vue提供的customRef定义响应式数据
// 箭头函数要接收从底层传过来的两个参数,track(跟踪)、trigger(触发)
let msg = customRef((track,trigger)=>{
return{
// msg被读取调用get
get(){
track() // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就要去更新。
// 要有返回值
return initValue
},
// msg被修改调用set;value参数是修改后的最新值
set(value){
// 每次执行都清除一下
clearTimeout(timer)
// 定时器
time = setTimeout(()=>{
initValue = value
trigger() // 通知Vue,数据msg变化了
},1000)
}
}
})
读数据之前要调用track
,改数据改完了之后要调用trigger
,必须同时存在,这两个都加上才会实现响应式效果,可以理解为在自己实现ref
。
一般会把自定义ref封装成hooks
。
巩固一下hooks:一般以useXxx
命名比如useMsgRef.ts
,然后把所有代码都给拿过来,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
31
32
33
34
35
// useMsgRef.ts
import {customRef} from 'vue'
export default function(initValue:string,delay:number) {
// 定义一个时间
let timer:number
// 使用Vue提供的customRef定义响应式数据
// 箭头函数要接收从底层传过来的两个参数,track(跟踪)、trigger(触发)
let msg = customRef((track,trigger)=>{
return{
// msg被读取调用get
get(){
track() // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就要去更新。
// 要有返回值
return initValue
},
// msg被修改调用set;value参数是修改后的最新值
set(value){
// 每次执行都清除一下
clearTimeout(timer)
// 定时器
time = setTimeout(()=>{
initValue = value
trigger() // 通知Vue,数据msg变化了
},delay)
}
}
})
// hooks最后要有返回值
return {msg}
}
此时就可以在代码中进行引用useMsgRef
1
2
3
4
5
6
import {useMsgRef} from './useMsgRef'
// 普通ref
let msg = ref('你好')
// 自定义ref,可以增加自己的逻辑,注意解构
let {msg{} = useMsgRef('你好',2000)
总结:所谓customRef(自定义ref)就是在原来ref的基础上,加上一些自己的逻辑,这个主要聊的地方就是
track
(跟踪)和trigger
(触发)。