前言
该文章记录一些Vue3的一些基本知识,后续将会逐步增加,所有内容均从网上整理而来,加上自己得理解做一个整合,方便工作中使用。
一、VUE3简介
1. vue3优点
- 打包体积减小
- 渲染速度加快
- 内存减少
2. 源码升级
- 使用Proxy代替defineProperty实现响应式
- 重写虚拟DOM的实现和Tree-Shaking
- 支持TypeScript
3. 新特性
- Composition API(组合api)
- setup 配置
- ref与reactive
- watch与watchEffect
- provide与inject
- 新的内置组件
- Fragment
- Teleport
- Suspense
- 其他改变
- 新的生命周期钩子
- data 选项应始终被声明为一个函数
- 移除keyCode支持作为 v-on 的修饰符
4. vscode安装新插件
- Vue Languague Features (Volar)
- TypeScript Vue Plugin (Volar)
二、VUE3项目创建
1. vue-cli 创建 官方文档
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create <project-name>
## 启动
cd <project-name>
npm run serve
2.使用 vite 创建 官方文档
## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev
三、VUE3基础用法
1、setup函数
1-1、setup函数介绍
- Vue3.0中一个新的配置项,值为一个函数,setup中this是undefined,setup函数会在beforeCreate之前调用,是所有生命周期函数之前执行。
- 组件中所用到的:数据、方法等等,均要配置在setup中
- setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
- 注意点:
- 不要与Vue2.x配置混用
- Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法
- 但在setup中不能访问到Vue2.x配置(data、methos、computed...)
- 如果有重名, setup优先
- setup不能是一个async函数(有特殊情况可以:使用Suspense和异步加载组件配合使用)
- 不要与Vue2.x配置混用
1-2、普通使用setup函数
<template>
<!-- vue3可以不用一个根目录包裹 -->
<div>{{name}}</div>
<button @click="sayHello">打招呼</button>
</template>
<script>
//setup函数如果想返回渲染函数,需要引入h函数
//import { h } from 'vue'
export default {
name: 'App',
//此处只是展示一下setup,暂不考虑数据响应式
setup() {
//在setup中配置data
let name = '胡歌'
//在setup中配置methods
function sayHello() {
console.log(`${name}向你问好!`)
}
//setup函数需要返回值,在模板中均可以直接使用
return {
name,
sayHello
}
//如果要返回一个渲染函数
//需要引入h函数
//return () => h('h1', '返回渲染函数') //渲染一个H1标签,内容为'返回渲染函数'
}
}
</script>
1-3、语法糖使用setup函数
<template>
<div class="home">{{ name }}---{{ age }}</div>
<button @click="count()">计算</button>
</template>
//语法糖:在script标签中加入setup,不在需要return变量和方法出去,模板依然可以使用
<script lang='ts' setup>
import { ref } from 'vue';
/* 使用defineOptions宏函数设置组件名字 */
defineOptions({
name: 'Login'
})
let name = '李白'
let age=ref(10)
const count = () => {
age.value+=10
}
</script>
2.VUE生命周期
2-1.生命周期介绍
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
2-2.Vue2的生命周期
创建阶段:
beforeCreate、created挂载阶段:
beforeMount、mounted更新阶段:
beforeUpdate、updated销毁阶段:
beforeDestroy、destroyed
2-3.Vue3的生命周期
创建阶段:
setup挂载阶段:
onBeforeMount、onMounted更新阶段:
onBeforeUpdate、onUpdated卸载阶段:
onBeforeUnmount、onUnmounted
注意: 子组件先挂载完毕,父组件后挂载完毕
2-4.常用钩子
常用的钩子:
onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)
2-5.使用案例
<script lang="ts" setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 生命周期钩子
onBeforeMount(()=>{
console.log('挂载之前')
})
onMounted(()=>{
console.log('挂载完毕')
})
onBeforeUpdate(()=>{
console.log('更新之前')
})
onUpdated(()=>{
console.log('更新完毕')
})
onBeforeUnmount(()=>{
console.log('卸载之前')
})
onUnmounted(()=>{
console.log('卸载完毕')
})
</script>
3.【ref函数与reactive函数】
3-1.【ref 创建:基本类型的响应式数据】
- 作用:定义响应式变量。
- 语法:
let xxx = ref(初始值)。 - 返回值:一个
RefImpl的实例对象,简称ref对象或ref,ref对象的value属性是响应式的。 - 注意点:
JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。- 对于
let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
<template>
<div class="person">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">年龄+1</button>
<button @click="showTel">点我查看联系方式</button>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
// name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
let name = ref('张三')
let age = ref(18)
// tel就是一个普通的字符串,不是响应式的
let tel = '13888888888'
function changeName(){
// JS中操作ref对象时候需要.value
name.value = '李四'
console.log(name.value)
// 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
// name = ref('zhang-san')
}
function changeAge(){
// JS中操作ref对象时候需要.value
age.value += 1
console.log(age.value)
}
function showTel(){
alert(tel)
}
</script>
3-2.【ref 创建:对象类型的响应式数据】
- 其实
ref接收的数据可以是:基本类型、对象类型。 - 若
ref接收的是对象类型,内部其实也是调用了reactive函数。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { ref } from 'vue'
// 数据
let car = ref({ brand: '奔驰', price: 100 })
let games = ref([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
a:{
b:{
c:{
d:666
}
}
}
})
console.log(car)
function changeCarPrice() {
car.value.price += 10
}
function changeFirstGame() {
games.value[0].name = '流星蝴蝶剑'
}
function test(){
obj.value.a.b.c.d = 999
}
</script>
3-3.【reactive 创建:对象类型的响应式数据】
- 作用:定义一个响应式对象(基本类型不要用它,要用
ref,否则报错) - 语法:
let 响应式对象= reactive(源对象)。 - 返回值:一个
Proxy的实例对象,简称:响应式对象。 - 注意点:
reactive定义的响应式数据是“深层次”的。
<template>
<div class="person">
<h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
<h2>游戏列表:</h2>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<h2>测试:{{obj.a.b.c.d}}</h2>
<button @click="changeCarPrice">修改汽车价格</button>
<button @click="changeFirstGame">修改第一游戏</button>
<button @click="test">测试</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { reactive } from 'vue'
// 数据
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
{ id: 'ahsgdyfa01', name: '英雄联盟' },
{ id: 'ahsgdyfa02', name: '王者荣耀' },
{ id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
a:{
b:{
c:{
d:666
}
}
}
})
function changeCarPrice() {
car.price += 10
}
function changeFirstGame() {
games[0].name = '流星蝴蝶剑'
}
function test(){
obj.a.b.c.d = 999
}
//注意:使用reactive生成的对象,如果想整个对象重新替换,使用Object.assign();ref生成的对象直接.value=xxx替换
function changeCar(){
Object.assign(car,{brand: '奥迪', price: 1000}) //正确,页面更新
car={brand: '奥迪', price: 1000}//页面不更新
car=reactive({brand: '奥迪', price: 1000})//页面不更新
}
</script>
3-4.【ref 对比 reactive】
- 宏观角度看:
ref用来定义:基本类型数据、对象类型数据;reactive用来定义:对象类型数据。
- 区别:
ref创建的变量必须使用.value(可以使用volar插件自动添加.value)。reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。
- 使用原则:
- 若需要一个基本类型的响应式数据,必须使用
ref。- 若需要一个响应式对象,层级不深,
ref、reactive都可以。- 若需要一个响应式对象,且层级较深,推荐使用
reactive。
3-5.【标签的 ref 属性】
- 作用1:用在普通
DOM标签上,获取的是DOM节点//用在普通DOM标签上,获取的是DOM节点 <template> <h1 ref="personName">姓名:{{ name }}</h1> <h2>年龄:{{age }}</h2> <h3>职业:{{career }}</h3> <button @click="showName">点击</button> <input ref="inpRef" type="text"> </template> <script lang='ts' setup> import { ref,onMounted } from 'vue'; let inpRef = ref() onMounted(() => { //元素加载完毕,就可以获得改元素了 inpRef.value.focus() //input框聚焦 }) let name = ref('胡歌') let age = ref(100) let career = ref('演员') // 在HTML元素上设置ref,变量名相同,即可获取DOM元素 let personName = ref() function showName() { //personName.value打印出来时是DOM元素 console.log(personName.value) } //导出需要暴露出去的数据或方法 defineExpose({ name, age, career }) </script>
- 作用2:用在组件标签上,获取的是组件实例对象
<template> <HomeView ref="homeView" /> <button @click="showLog">获取组件节点</button> </template> <script lang='ts' setup> import HomeView from './views/HomeView.vue'; import { ref } from 'vue'; let homeView = ref() function showLog() { //homeView.value打印的是HomeView组件实例,但是不能直接获取其内部数据、方法, //需要子组件defineExpose({})暴露出来 console.log(homeView.value) console.log(homeView.value.name) //输出胡歌 console.log(homeView.value.age) //输出100 } </script>
4.【toRefs 与 toRef】
- 作用:将一个响应式对象中的每一个属性,转换为
ref对象。 - 备注:
toRefs与toRef功能一致,但toRefs可以批量转换。 - 语法如下:
<template>
<div class="person">
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
<h2>性别:{{person.gender}}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeGender">修改性别</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {ref,reactive,toRefs,toRef} from 'vue'
// 数据
let person = reactive({name:'张三', age:18, gender:'男'})
// 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
let {name,gender} = toRefs(person)
// 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
let age = toRef(person,'age')
// 方法
function changeName(){
name.value += '~'
}
function changeAge(){
age.value += 1
}
function changeGender(){
gender.value = '女'
}
</script>
5.【计算属性-computed】
//注意:不要在computed函数中去写操作dom、调接口
<template>
<div class="person">
<div>姓:<input type="text" v-model="firstName"></div>
<div>名:<input type="text" v-model="lastName"></div>
<div>全名:<span>{{ fullName }}</span></div>
<div>全名2:<span>{{ fullName2 }}</span></div>
<div>全名3:<span>{{ fullName3('李明') }}</span></div>
<br>
<button @click="changeFullName">修改全名2改为:li-si</button>
</div>
</template>
<script setup lang="ts" name="App">
import { ref, computed } from 'vue'
let firstName = ref('zhang')
let lastName = ref('san')
// 计算属性——只读取,不修改
let fullName = computed(()=>{
return firstName.value + '-' + lastName.value
})
// 计算属性——既读取又修改
let fullName2 = computed({
// 读取
get() {
return firstName.value + '-' + lastName.value
},
// 修改
set(val) {
console.log('有人修改了fullName', val)
firstName.value = val.split('-')[0]
lastName.value = val.split('-')[1]
}
})
function changeFullName() {
fullName2.value = 'li-si'
}
// 计算属性-带参数
let fullName3 = computed(() => (val: string) => {
return firstName.value + '-' + lastName.value + val
})
</script>
6.【监视-watch函数】
- 作用:监视数据的变化(和
Vue2中的watch作用一致) - 特点:
Vue3中的watch只能监视以下四种数据:ref定义的数据。reactive定义的数据。- 函数返回一个值(
getter函数)。 - 一个包含上述内容的数组。
6-1.【情况一:监视ref定义的【基本类型】数据】
<template>
<div class="person">
<h1>情况一:监视【ref】定义的【基本类型】数据</h1>
<h2>当前求和为:{{ sum }}</h2>
<button @click="changeSum">点我sum+1</button>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
let sum = ref(0)
function changeSum() {
sum.value += 1
}
// 监视【ref】定义的【基本类型】数据--watch(监听对象,回调函数,配置项)
// 停止监视,满足需要的条件,执行watch函数返回值(函数)即可停止监视
const stopWatch = watch(sum, (newValue, oldValue) => {
console.log('sum变化了', newValue, oldValue)
if (newValue >= 10) {
stopWatch()
}
},{
immediate: true//注意:立即监视,默认immediate:false
})
</script>
<template>
<div class="person">
<h1>情况二:监视 【多个】【ref】定义的【基本类型】数据</h1>
<button @click="changeName">改变姓名</button>
<button @click="changeAge">改变年龄</button>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
let name = ref('胡歌')
let age = ref(20)
function changeName() {
name.value+='-'
}
function changeAge() {
age.value += 1
}
//监视【多个】写法---watch([监听对象,监听对象],回调函数,配置项)
watch([name, age], (newValue, oldValue) => {
console.log(newValue, oldValue)
},{immediate:true})
// 合并监听-返回值不一样, 以数组的方式返回所有被监听的对象,无论是否发生改变
// immediate:true立即监听-输出: (2) ['胡歌',20] []
// 改变name-输出: (2) ['胡歌-',20] (2) ['胡歌',20]
// 点击age-输出: (2) ['胡歌-',21] (2) ['胡歌-',20]
</script>
6-2.【情况二:监视ref定义的【对象类型】数据】
<template>
<div class="person">
<h1>情况二:监视【ref】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
let person = ref({
name: '张三',
age: 18
})
function changeName() {
person.value.name += '~'
}
function changeAge() {
person.value.age += 1
}
function changePerson() {
person.value = { name: '李四', age: 90 }
}
/*
情况一:监视整个person, 未开启深度监视
使用changeName、changeAge方法修改单个属性值,【不会触发监视】;
使用changePerson方法改变整个对象,【会触发监视】
*/
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
})
/*
情况二:监视整个person, 开启深度监视deep: true,
使用changeName、changeAge方法修改属性值,触发监视,但是【newValue, oldValue返回值是一样的】;
使用changePerson方法改变整个对象,也会触发监视,【newValue, oldValue返回值不一样的】
*/
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
}, { deep: true })
</script>
6-3.【情况三:监视reactive定义的【对象类型】数据,且默认开启了深度监视】
<template>
<div class="person">
<h1>情况三:监视【reactive】定义的【对象类型】数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<script lang="ts" setup name="Person">
import { reactive, watch } from 'vue'
let person = reactive({
name: '张三',
age: 18
})
function changeName() {
person.name += '~'
}
function changeAge() {
person.age += 1
}
function changePerson() {
Object.assign(person, { name: '李四', age: 80 })
}
/*
监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的;
无论怎么修改对象都会被监听,且【newValue, oldValue都是最新值】
*/
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
})
</script>
6-4.【情况四:监视ref或reactive定义的【对象类型】数据中的某个属性】
- 若该属性值不是【对象类型】,需要写成函数形式。
- 若该属性值是依然是【对象类型】,推荐写成函数。
结论:监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
<template>
<div class="person">
<h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视响应式对象中的某个属性,且该属性是【基本类型】的,要写成函数式
watch(()=> person.name,(newValue,oldValue)=>{
console.log('person.name变化了',newValue,oldValue)
})
/*
监视响应式对象中的某个属性,且该属性是【对象类型】的,可以直接写,也能写函数,更推荐写函数
单个属性变化时,newValue,oldValue一样,均为新值
当整个对象变化时,newValue,oldValue不一样
*/
watch(()=>person.car,(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
6-5.【情况五:监视ref或reactive定义的【对象类型】数据中的多个属性】
<template>
<div class="person">
<h1>情况五:监视上述的多个数据</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>年龄:{{ person.age }}</h2>
<h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>
<button @click="changeName">修改名字</button>
<button @click="changeAge">修改年龄</button>
<button @click="changeC1">修改第一台车</button>
<button @click="changeC2">修改第二台车</button>
<button @click="changeCar">修改整个车</button>
</div>
</template>
<script lang="ts" setup name="Person">
import {reactive,watch} from 'vue'
// 数据
let person = reactive({
name:'张三',
age:18,
car:{
c1:'奔驰',
c2:'宝马'
}
})
// 方法
function changeName(){
person.name += '~'
}
function changeAge(){
person.age += 1
}
function changeC1(){
person.car.c1 = '奥迪'
}
function changeC2(){
person.car.c2 = '大众'
}
function changeCar(){
person.car = {c1:'雅迪',c2:'爱玛'}
}
// 监视对象多个属性--watch([()=>对象.xx , ()=>对象.xx,···],()=>{xxx},{deep:true})
watch([()=>person.name,()=>person.car],(newValue,oldValue)=>{
console.log('person.car变化了',newValue,oldValue)
},{deep:true})
</script>
7.【监视进阶方法:watchEffect函数】
<template>
<div class="person">
<h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
<h2>水温:{{ temp }}</h2>
<h2>水位:{{ height }}</h2>
<button @click="changeTemp">水温+10</button>
<button @click="changeHeight">水位+1</button>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, watchEffect } from 'vue'
// 数据
let temp = ref(0)
let height = ref(0)
// 方法
function changeTemp() {
temp.value += 10
}
function changeHeight() {
height.value += 1
}
// 用watch实现,需要明确的指出要监视:temp、height
watch([temp, height], (value) => {
// 从value中获取最新的temp值、height值
const [newTemp, newHeight] = value
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if (newTemp >= 50 || newHeight >= 20) {
console.log('联系服务器')
}
})
/*
用watchEffect实现,不用指明监视谁,类似computed,用到谁监视谁;
自动调用一次,如果没有使用变量,什么都不监视;
注意如果是复杂类型,没有深度监视,必须使用具体属性
*/
const stopWtach = watchEffect(() => {
// 室温达到50℃,或水位达到20cm,立刻联系服务器
if (temp.value >= 50 || height.value >= 20) {
console.log('联系服务器')
}
// 水温达到100,或水位达到50,取消监视
if (temp.value === 100 || height.value === 50) {
console.log('清理了')
stopWtach()
}
})
</script>
8.【组件传值】
8-1.【props传值:父子组件相互传递】
- 若 父传子:属性值是非函数。
index.ts设置类型export interface PersonInter{ id: string, name: string, age:number } export type Persons = Array<PersonInter>
父组件.vue中代码:<template> <HomeView :personList="personList" /> </template> <script lang='ts' setup> import HomeView from '@/views/HomeView.vue' import {type Persons} from '@/types' import { reactive } from 'vue'; let personList = reactive<Persons>([ { id: 'xxxxx01', name: '胡歌', age: 19 }, { id: 'xxxxx02', name: '霍建华', age: 20 }, { id: 'xxxxx03', name: '刘思思', age: 21 } ]) </script>
子组件.vue中代码:<template> <ul> <li v-for="item in personList" :key="item.id"> {{ item.name }} --- {{ item.age }} </li> </ul> </template> <script lang='ts' setup> import { type Persons } from '@/types' /* 宏函数不用引入 withDefaults(),defineProps() */ /* 只接收prop*/ // defineProps(['personList']) /* 接收+传递类型 */ // defineProps<{personList:Persons}>() /* 接收+限制类型+指定默认值+限制必要性 */ let {personList} = withDefaults(defineProps<{personList?:Persons}>(), { personList: () => [{ id: 'xxxxx00', name: '默认', age: 1 }] }) /* defineProps宏函数 其他写法 */ defineProps({ hobby: String, //简写 nums: { type: Number, //配置类型 required: true, //配置是否必传 default:500 //配置默认值 } }) </script>
- 若 子传父:属性值是函数。
父组件.vue<template> <h2>子组件传递--{{ toy }}</h2> <HomeView :sendToy="getToy" /> </template> <script lang='ts' setup> import HomeView from '@/views/HomeView.vue' import { ref } from 'vue'; let toy = ref('') function getToy(value:string) { toy.value=value } </script>
子组件.vue<template> <button @click="sendToy(toy)">点我传递内容给父组件</button> </template> <script lang='ts' setup> import { ref } from 'vue'; let toy=ref('玩具车') defineProps(['sendToy']) </script>
8-2.【自定义事件:子传父】
子组件.vue,声明事件<template> <button @click="clickHandler">点我传递内容给父组件</button> </template> <script lang='ts' setup> import { ref } from 'vue'; let toy = ref('玩具车') /* 声明事件 使用宏函数defineEmits([xxx,xxx])*/ const emit = defineEmits(['send-toy']) function clickHandler() { emit('send-toy',toy.value) } </script>
父组件.vue,绑定自定义事件<template> <h2>子组件传递--{{ toy }}</h2> <!-- 给子组件绑定 自定义事件 --> <HomeView @send-toy="getToy" /> </template> <script lang='ts' setup> import HomeView from '@/views/HomeView.vue' import { ref } from 'vue'; let toy = ref('') /* 处理子组件传递的数据 */ function getToy(value:string) { toy.value=value } </script>
8-3.【provide、inject】
-
概述:实现祖孙组件直接通信,只要是后代组件,都可以接收到。
-
具体使用:
provide(key,value)/ inject(key,默认值)- 在祖先组件中通过
provide配置向后代组件提供数据 - 在后代组件中通过
inject配置来声明接收数据
- 在祖先组件中通过
grandpa.vue<template> <HomeView /> <div class="father"> <h3>祖先组件</h3> <h4>资产:{{ money }}万元</h4> <h4>汽车:{{ car.brand }}---价格:{{ car.price }}</h4> <button @click="money += 1">资产+1</button> <button @click="car.price += 1">汽车价格+1</button> <Child/> </div> </template> <script lang='ts' setup> import HomeView from '@/views/HomeView.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 } // 提供数据 provide('moneyContext', { money, updateMoney }) provide('car', car) </script>
grandson.vue<template> <h3>我是孙组件</h3> <h4>资产:{{ money }}万元</h4> <h4>汽车:{{ car.brand }}---价格:{{ car.price }}</h4> <button @click="updateMoney(6)">花爷爷的钱</button> </template> <script lang='ts' setup> import { inject } from "vue"; //注入数据,既可以接收祖先传递的数据、方法,也可以利用方法传递数据给祖先组件 const { money, updateMoney } = inject('moneyContext', { money: 0, updateMoney: (value: number) => { } }) const car = inject('car', { brand: '未知', price: 0 }) </script>
8-4.【mitt-任意组件间通信】
- 概述:与消息订阅与发布(
pubsub)功能类似,可以实现任意组件间通信。 - 安装:
npm i mitt
第一步:新建文件--src\utils\emitter.ts// 引入mitt import mitt from "mitt"; // 创建emitter const emitter = mitt() /* // 绑定事件 emitter.on('text',(value)=>{ console.log('text事件被触发',value) }) // 触发事件 emitter.emit('text',666) // 解绑事件 emitter.off('text') // 清理全部事件 emitter.all.clear() */ // 创建并暴露mitt export default emitter
第二步:在接收数据的组件中-绑定事件、同时在组件卸载前解绑事件<template> <h2>接收玩具---{{ toy }}</h2> </template> <script lang='ts' setup> import emitter from '@/utils/emitter' import { ref,onUnmounted } from "vue"; import type { Handler } from "mitt"; let toy = ref('') //TS类型判断 const handleSendToy: Handler<unknown> = (value: unknown) => { toy.value = value as string; } // 绑定事件 emitter.on('send-toy', handleSendToy) // 解绑事件 onUnmounted(() => { emitter.off('send-toy') }) </script>
第三步:提供数据的组件,在合适的时候触发事件<template> <button @click="sendToy">点击发送玩具</button> </template> <script lang='ts' setup> import { ref } from "vue"; import emitter from '@/utils/emitter' let toy = ref('奥特曼') function sendToy() { emitter.emit('send-toy',toy.value) } </script>
8-5.【v-model--父子组件通信】
v-model本质:
<!-- 使用v-model指令 -->
<input type="text" v-model="userName">
<!-- v-model的本质是下面这行代码 -->
<input
type="text"
:value="userName"
@input="userName =(<HTMLInputElement>$event.target).value"
>
- 组件标签上的
v-model的本质::moldeValue+update:modelValue事件
一、基础写法
<template> <h1>{{ firstName }}</h1> <HomeView v-model:modelValue="firstName" /> </template> <script lang='ts' setup> import { ref } from "vue"; import HomeView from "./views/HomeView.vue"; let firstName = ref('胡歌') </script><template> <input type="text" :value="modelValue" @input="emit('update:modelValue', (<HTMLInputElement>$event.target).value)" > </template> <script lang='ts' setup> //接收props,v-model="firstName"===v-model:modelValue="firstName",所以默认为modelValue defineProps(['modelValue']) //声明事件, 默认为'update:modelValue' const emit = defineEmits(['update:modelValue']) </script>
二、重命名写法
<template> <h1>{{ firstName }}</h1> <HomeView v-model:firstName="firstName" /> </template><template> <input type="text" :value="firstName" @input="emit('update:firstName', (<HTMLInputElement>$event.target).value)" > </template> <script lang='ts' setup> //接收props,v-model:firstName="firstName" defineProps(['firstName']) //声明事件, 重名为'update:firstName' const emit = defineEmits(['update:firstName']) </script>
三、使用defineModel宏函数--测试阶段
vite.config.js文件设置--defineModel:true//此时vue版本为3.3.4, defineModel宏函数还是测试阶段,需要单独配置才能使用 export default defineConfig({ plugins: [ vue({ script: { defineModel:true; //开启 } }), ], })
父组件.vue<template> <h1>{{ firstName }}</h1> <HomeView v-model="firstName" /> </template> <script lang='ts' setup> import { ref } from "vue"; import HomeView from "./views/HomeView.vue"; let firstName = ref('胡歌') </script>
子组件.vue<template> <input v-model="firstName"> </template> <script lang='ts' setup> //此时vue版本为3.3.4, defineModel宏函数还是测试阶段,需要单独配置才能使用 import { defineModel } from 'vue'; //firstName在父组件、子组件完成双向绑定,当子组件中input值修改时,所有使用firstName的都会跟着改变. const firstName = defineModel() </script>
8-6.【$refs 和 $parent :父子组件相互通信】
-
概述:
$refs用于 :父→子。$parent用于:子→父。
-
原理如下:
属性 说明 $refs值为对象,包含所有被 ref属性标识的DOM元素或组件实例。$parent值为对象,当前组件的父组件实例对象。
<template> <h2>房产:{{ house }} 套</h2> <!-- $refs 获取该组件内所有 ref标记的子组件的实例对象 --> <button @click="changeToy($refs)">改变儿子玩具</button> <HomeView ref="homeRef" /> </template> <script lang='ts' setup> import { ref } from "vue"; import HomeView from "./views/HomeView.vue"; let house = ref(5) let homeRef=ref() function changeToy(refs:any) { refs.homeRef.toy = '小汽车' //等价于: homeRef.value.toy= '小汽车' } defineExpose({ house }) </script><template> <h2>玩具:{{ toy }}</h2> <!-- $parent 获取父组件的实例对象 --> <button @click="getHouse($parent)">获取父亲房产</button> </template> <script lang='ts' setup> import { ref } from 'vue'; let toy =ref('奥特曼') function getHouse(parent: any) { parent.house -= 1 } defineExpose({toy}) </script>
四、自定义hook
-
什么是
hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin。 -
自定义
hook的优势:复用代码, 让setup中的逻辑更清楚易懂。
组件.vue<template> <h2>当前求和为:{{ sum }} --- 双倍{{ doubleSum }}</h2> <button @click="increment">点我+1</button> <button @click="decrement">点我-1</button> <hr> <img v-for="(u, index) in dogList" :key="index" :src="(u as string)"> <button @click="getDog">再来一只狗</button> </template> <script lang='ts' setup> /* 引入hooks */ import useCount from "@/hooks/useCount"; import useDog from "@/hooks/useDog"; const { sum, increment, decrement, doubleSum } = useCount() const { dogList, getDog } = useDog() </script>
useDog.tsimport { reactive, onMounted } from 'vue' import axios, { AxiosError } from 'axios' export default function () { let dogList = reactive<string[]>([]) // 方法 async function getDog() { try { // 发请求 let { data } = await axios.get('https://dog.ceo/api/breed/pembroke/images/random') // 维护数据 dogList.push(data.message) } catch (error) { // 处理错误 const err = <AxiosError>error console.log(err.message) } } // 挂载钩子 onMounted(() => { getDog() }) //向外部暴露数据 return {dogList,getDog} }
useCount.tsimport { ref, onMounted,computed } from 'vue' export default function () { let sum = ref(0) const increment = () => { sum.value += 1 } const decrement = () => { sum.value -= 1 } const doubleSum = computed(() => { return sum.value*2 }) onMounted(() => { increment() }) //向外部暴露数据 return { sum, increment, decrement, doubleSum } }
五、插槽
1.【默认插槽】
<template> <HomeView title="游戏推荐"> <ul> <li v-for="item in games" :key="item.id">{{ item.name }}</li> </ul> </HomeView> <HomeView title="图片推荐"> <img :src="imgUrl" alt=""> </HomeView> <HomeView title="视频推荐"> <video :src="videoUrl" controls></video> </HomeView> </template> <script lang='ts' setup> import { reactive, ref } from "vue"; import HomeView from "./views/HomeView.vue"; let games = reactive([ { id: 'xx01', name: '英雄联盟' }, { id: 'xx02', name: '穿越火线' }, { id: 'xx03', name: 'DNF' } ]) let imgUrl=ref('https://www.bilibili.com/') let videoUrl=ref('https://www.bilibili.com/') </script><template> <h2>{{ title }}</h2> <!-- 占位符 --> <slot> <div>默认内容</div> </slot> </template> <script lang='ts' setup> defineProps(['title']) </script>
2.【具名插槽:父组件将内容传递给子组件对应slot标签上】
<template> <!-- 具名插槽写法1 v-slot:名字 --> <!-- 具名插槽写法2 #名字 --> <!-- 具名插槽只能写在组件标签/template标签上 --> <HomeView> <template v-slot:slotName> <h2>游戏推荐</h2> </template> <template v-slot:slotContent> <ul> <li v-for="item in games" :key="item.id">{{ item.name }}</li> </ul> </template> </HomeView> <HomeView> <template v-slot:slotName> <h2>图片推荐</h2> </template> <template v-slot:slotContent> <img :src="imgUrl" alt=""> </template> </HomeView> <HomeView> <template #slotName> <h2>视频推荐</h2> </template> <template #slotContent> <video :src="videoUrl" controls></video> </template> </HomeView> </template> <script lang='ts' setup> import { reactive, ref } from "vue"; import HomeView from "./views/HomeView.vue"; let games = reactive([ { id: 'xx01', name: '英雄联盟' }, { id: 'xx02', name: '穿越火线' }, { id: 'xx03', name: 'DNF' } ]) let imgUrl=ref('https://www.bilibili.com/') let videoUrl=ref('https://www.bilibili.com/') </script><template> <slot name="slotName"> 默认标题 </slot> <slot name="slotContent"> 默认内容 </slot> </template>
3.【作用域插槽:数据在子组件,父组件根据传递过来的数据生成不同内容 】
<template> <!-- 通过v-slot="获取一个对象,包裹所有slot传递过来的信息" --> <HomeView> <template v-slot="params"> <h2>{{ params.title }}1</h2> <ul> <li v-for="item in params.youxi" :key="item.id">{{ item.name }}</li> </ul> </template> </HomeView> <HomeView> <!-- 可以直接使用 对象结构 --> <template v-slot="{youxi,title}"> <p>{{ title }}2</p> <ol> <li v-for="item in youxi" :key="item.id">{{ item.name }}</li> </ol> </template> </HomeView> <!-- 可以和具名插槽一起使用 v-slot:名字="" / #名字="" --> <HomeView> <template v-slot:slotContent="{ youxi, title }"> <p>{{ title }}3</p> <ol> <li v-for="item in youxi" :key="item.id">{{ item.name }}</li> </ol> </template> </HomeView> <HomeView> <template #slotContent="{ youxi, title }"> <h3>{{ title }}4</h3> <ul> <li v-for="item in youxi" :key="item.id">{{ item.name }}</li> </ul> </template> </HomeView> </template> <script lang='ts' setup> import HomeView from "./views/HomeView.vue"; </script><template> <slot :youxi="games" title="推荐游戏"></slot> <!-- 可以和具名插槽一起使用--> <slot name="slotContent" :youxi="games" title="游戏排行榜"></slot> </template> <script lang='ts' setup> import { reactive } from "vue"; let games = reactive([ { id: 'xx01', name: '英雄联盟' }, { id: 'xx02', name: '穿越火线' }, { id: 'xx03', name: 'DNF' } ]) </script>
六、宏函数总结
1.defineOptions
defineOptions(),可以定义任意的选项,除(props,emits,expose,slots)外
//因为使用了语法糖,使得setup同级的配置无法在使用了,所以需要一个宏函数来配置.
//必须是在vue3.3版本及以上
<script setup>
//常见定义该组件的name
defineOptions({
name:'Login'
})
</script>
2.defineProps
defineProps(),用于子组件接收父组件传递的props,模板中可直接使用,但不能直接修改值
<script lang='ts' setup>
//接收props
defineProps(['xx','xxx',···])
//配置写法
defineProps({
hobby: String, //简写
nums: {
type: Number, //配置类型
required: true, //配置是否必传
default:500 //配置默认值
}
})
//ts声明方法
/* 接收+ 声明类型 */
// defineProps<{变量:类型}>()
/* 接收+限制类型+指定默认值+限制必要性 */
//let { xx } = withDefaults(defineProps<{ xx?: 类型 }>(), {
// 给变量设置默认值
// xx: () => 默认值
//})
</script>
3.defineEmits
defineEmits(),用于子组件接收父组件传递的Emits,模板中可直接使用其方法
<script setup>
/* 声明事件 使用宏函数defineEmits([xxx,xxx])*/
const emit = defineEmits(['send-toy'])
function clickHandler() {
emit('send-toy',toy.value)
}
</script>
4.withDefaults
withDefaults(),用于子组件接收父组件传递的props
<script setup>
</script>
七、路由:vue-router
安装:npm install vue-router@next
1. 【两个注意点】
- 路由组件通常存放在
pages或views文件夹,一般组件通常存放在components文件夹。 - 通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载。
2. 【router基础使用】
Vue3中要使用vue-router的最新版本,目前是4版本。
src/router/index.ts路由配置文件//创建一个路由器,并暴露出去 //第一步引入createRouter import { createRouter,createWebHistory } from 'vue-router' //创建路由器 const router = createRouter({ //路由器的工作模式 history: createWebHistory(), //设置路由对应相应组件 routes: [ /* { path: '路径', name:'命名路由',//非必须 component:对应的组件, redirect: '重定向路径',//非必须 meta:{title:'我是某页'},//非必须,该路由页的元信息 children:[]//非必须,子级路由配置,属性一样 } */ { path: '/', name: 'Home', component: () => import("@/views/HomeView.vue") }, { path: '/login', name: 'Login', component: () => import("@/views/LoginView.vue") }, { path: '/404', name: 'Notfind', component: () => import("@/views/NotFound.vue") }, // ':' -- 表示这是一个动态参数 // 'catchAll' -- 自定义名称 // '(.*)' -- 表示匹配任意字符的正则表达式,用于捕获所有路径的通配符 { //用于捕获所有错误的路由,都进入404组件 path: '/:catchAll(.*)', name: '*', redirect: '/404' } ] }) export default router
main.ts文件引入routerimport { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')
App.vue使用router<template> <div class="app"> <h2 class="title">Vue路由测试</h2> <!-- 导航区 --> <div class="navigate"> <!-- 第一种:to的字符串写法 --> <RouterLink to="/home" active-class="active">首页</RouterLink> <!-- 第二种:to的对象写法 --> <RouterLink :to="{path:'/home'}" active-class="active" >Home</RouterLink> <RouterLink to="/news" active-class="active">新闻</RouterLink> <RouterLink to="/about" active-class="active">关于</RouterLink> </div> <!-- 展示区 RouterView占位--> <div class="main-content"> <RouterView></RouterView> </div> </div> </template> <script lang="ts" setup name="App"> import {RouterLink,RouterView} from 'vue-router' </script>
3. 【路由器工作模式】
history模式
优点:
URL更加美观,不带有#,更接近传统的网站URL。缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404错误。
hash模式
优点:兼容性更好,因为不需要服务器端处理路径。
缺点:
URL带有#不太美观,且在SEO优化方面相对较差。
import { createRouter,createWebHistory,createWebHashHistory } from 'vue-router'
//创建路由器
const router = createRouter({
// 1. createWebHistory(path) -- history模式 不带#
// 2. createWebHashHistory(path) -- hash模式 带#
// 3. path--指所有路由前的路径,默认是'/',也可以使用import.meta.env.BASE_URL
//Hash模式
history: createWebHashHistory(),
//History模式
history: createWebHistory(),
//设置路由对应相应组件
routes: []
})
export default router
4. 【路由嵌套】
import { createRouter,createWebHashHistory } from 'vue-router' const router = createRouter({ history: createWebHashHistory(), routes: [ { path: '/', name: 'Home', component: () => import("@/views/HomeView.vue"), redirect: '/home', // 路由嵌套,嵌套的路由地址要完整书写,例如'/home/one' children: [ { path: 'one', component:()=>import("@/views/Child/ChildOne.vue") }, { path: 'two', component: () => import("@/views/Child/ChildTwo.vue") } ], meta: { title:'我是home页' } }, ] }) export default router//vue文件 //跳转路由(记得要加完整路径) <router-link to="/home/one">xxxx</router-link> <!-- 或 --> <router-link :to="{path:'/home/one'}">xxxx</router-link>
5. 【路由跳转+ 传参】
5-1.RouterLink
- 【RouterLink跳转】
//vue文件
<template>
<!-- 第一种:to的字符串写法 -->
<RouterLink to="/home" active-class="active">首页</RouterLink>
<!-- 第二种:to的对象写法 path-->
<RouterLink :to="{path:'/home'}" active-class="active" >Home</RouterLink>
<!-- 第三种:to的对象写法 name:配置路由时的name-->
<RouterLink :to="{name:'Home'}" active-class="active" >Home</RouterLink>
<!-- 引入路由页面 -->
<RouterView></RouterView>
</template>
<script lang='ts' setup>
//引入RouterLink
import { RouterLink,RouterView } from 'vue-router';
</script>
-
【RouterLink传参】
query参数
//vue文件 <!-- 第一种 跳转并携带query参数(to的字符串写法) --> <router-link to="/news/detail?id=1&title=新闻&content=欢迎你">跳转1</router-link> <!-- 第二种 跳转并携带query参数(to的对象写法) --> <router-link :to="{ //name:'xiang', //用name也可以跳转 path:'/news/detail', query:{ id:1, title:"新闻", content:"发生火灾" } }" > 跳转1 </router-link> //接收参数 <script lang='ts' setup> import { useRoute } from 'vue-router'; const route = useRoute() // 打印query参数 console.log(route.query) </script>params参数
在路由配置文件中提前设置好参数的keyroutes: [ { path: '/home', name: 'Home', component: () => import("@/views/HomeView.vue"), children: [ { //必须提前使用:key 提前设置好参数的key path: 'one/:id/:title/:content', component:()=>import("@/views/Child/ChildOne.vue") }, { path: 'two/:id/:name', component: () => import("@/views/Child/ChildTwo.vue") } ] }, ]//vue文件 <!-- 第一种 跳转并携带params参数(to的字符串写法) --> <router-link :to="`/home/one/001/新闻/内容001`">跳转</router-link> <!-- 第二种 跳转并携带params参数(to的对象写法) --> <router-link :to="{ name:'Home', //只能用name跳转 params:{ id:1, title:"新闻", content:"发生火灾" } }" > 跳转2 </router-link> //接收参数的vue文件 <script lang='ts' setup> import { useRoute } from 'vue-router'; const route = useRoute() // 打印params参数 console.log(route.params) </script>
5-2.编程式导航
-
注意:
- 路由组件的两个重要的属性:
$route和$router变成了两个hooks router.push()是追加历史记录(默认值)。router.replace()是替换当前记录。
- 路由组件的两个重要的属性:
-
【跳转】
//vue文件 <template> <!-- template 上可以直接使用 $router进行跳转 --> <button @click="$router.push('/')">首页</button> <!-- 使用方法跳转 --> <button @click="goToLogin">登录页</button> <!-- 引入路由页面 --> <RouterView></RouterView> </template> <script lang='ts' setup> //引入RouterLink import { RouterView } from 'vue-router'; //要想使用路由跳转--引入useRouter //要想使用路由参数--引入useRoute import {useRoute,useRouter} from 'vue-router' const router = useRouter() const route = useRoute() function goToLogin(){ router.push('/login')//是追加浏览器历史记录(默认值) router.replace('/login')//是替换当前记录。 } </script> -
【传参】
跳转vue<template> <button @click="goQuery">跳转query传参</button> <button @click="goParams">跳转params传参</button> <RouterView></RouterView> </template> <script lang='ts' setup> import { RouterView,RouterLink,useRouter } from 'vue-router'; const router= useRouter() function goQuery() { //query传参--name和path都可以使用 //弊端:url地址会显示参数 //优点:刷新页面,参数依然存在 //router.replace()参数一样 router.push({ path: '/login', query: { id: '001', title: '新闻', content: '吃饭了吗' } }) } function goParams() { //query传参--只能使用name //弊端:刷新页面,参数就不存在了 //优点:url地址会隐藏参数 //router.replace()参数一样 router.push({ name: 'Login', params: { id: '001', title: '新闻', content: '吃饭了吗' } }) } </script>目的vue<template> <div class="login"> <h2>{{ route.params.id }}</h2> <h2>{{ route.params.title }}</h2> <h2>{{ route.params.content }}</h2> <h2>{{ route.query.id }}</h2> <h2>{{ route.query.title }}</h2> <h2>{{ route.query.content }}</h2> </div> </template> <script lang='ts' setup> import { useRoute } from 'vue-router' const route = useRoute() </script>
6. 【路由的props配置】
一般组件使用时
<Dome title="biaot"/>可以直接在标签上自定义属性,组件内通过props接收;但是路由组件是通过<RouterView/>占位引入,无法直接在标签上写自定义属性,但是在路由配置时引入了组件,即可以设置自定义属性,然后通过props接收
-
query参数
//路由的配置文件 routes: [ //写法:函数写法,相当于将route.query传递给props { path: '/login', name: 'Login', component: () => import("@/views/LoginView.vue"), props(route){ return route.query } } ]使用跳转组件<RouterLink to="/login?id=001&title=新闻&content=吃饭了吗">登录页</RouterLink> <RouterLink :to="{ name:'Login', query:{ id:'001', title:'新闻', content:'吃饭了吗' } }" >登录页</RouterLink>路由组件<template> <div class="login"> <h2>{{ id }}</h2> <h2>{{ title }}</h2> <h2>{{ content }}</h2> </div> </template> <script lang='ts' setup> defineProps(['id','title','content']) </script> -
params参数
在路由配置文件中提前占位,设置好参数的key//路由的配置文件 routes: [ //写法一:布尔值写法 { path: '/login/:id/:title/:content', name: 'Login', component: () => import("@/views/LoginView.vue"), props:true } //写法二:函数写法,相当于将route.params传递给props { path: '/login/:id/:title/:content', name: 'Login', component: () => import("@/views/LoginView.vue"), props(route){ return route.params } } //写法三:对象写法,传入固定值 { path: '/login/:id/:title/:content', name: 'Login', component: () => import("@/views/LoginView.vue"), props:{id:1,title:'标题',content:'吃饭了吗'} } ]使用跳转组件<RouterLink to="/login/001/新闻/吃饭了吗">登录页</RouterLink> <RouterLink :to="{ name:'Login', params:{ id:'001', title:'新闻', content:'吃饭了吗' } }" >登录页</RouterLink>路由组件<template> <div class="login"> <h2>{{ id }}</h2> <h2>{{ title }}</h2> <h2>{{ content }}</h2> </div> </template> <script lang='ts' setup> defineProps(['id','title','content']) </script>
7. 【动态路由】
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: () => import("@/views/HomeView.vue"),
},
{
path: '/login',
name: 'Login',
component: () => import("@/views/LoginView.vue"),
// props:true
props(route) {
return route.query
},
}
]
})
//`router.addRoute` 方法来动态添加路由
router.addRoute({
path: '/about',
name: 'About',
component: () => import('@/views/Child/ChildOne.vue')
})
// 添加 About的子路由
router.addRoute('About', {
path: 'test',
name: 'Test',
component: () => import('@/views/Child/ChildTwo.vue')
});
//路由添加成功后执行一些操作,可以使用 router.isReady() 方法来检查路由是否已经准备就绪
router.isReady().then(() => {
console.log('路由添加成功后执行的操作')
}).catch(() => {
console.log('路由添加失败后执行的操作')
})
export default router
- 从后端获取数据之后,再动态添加路由
<template>
<button @click="getRouter">调取接口,添加路由</button>
<button @click="goAbout">去往动态路由</button>
<RouterView></RouterView>
</template>
<script lang='ts' setup>
import { RouterView,useRouter } from 'vue-router';
const router = useRouter()
function getRouter() {
setTimeout(() => {
router.addRoute({
path: '/about',
name: 'About',
component: () => import('@/views/Child/ChildOne.vue')
})
router.addRoute({
path: '/news',
name: 'News',
component: () => import('@/views/Child/ChildTwo.vue')
})
router.isReady().then(() => {
// 进行路由跳转
router.push('/news');
});
}, 500);
}
function goAbout() {
router.push('/about')
}
</script>
8. 【路由守卫】
- 全局路由守卫和路由独享守卫
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: ()=>import("@/views/HomeView.vue"),
// 路由独享守卫逻辑
beforeEnter(to, from, next) {
console.log('路由独享守卫逻辑')
next()
}
},
{
path: '/login',
name: 'Login',
component: () => import("@/views/LoginView.vue"),
// 路由独享守卫逻辑
beforeEnter(to, from, next) {
console.log('路由独享守卫逻辑')
next()
}
}
]
})
// to-- 对象,目的地路由的信息
// from-- 对象,出发地路由的信息
// next-- 函数,next(path) path不填就去往目的地路由,填就去往指定路由
router.beforeEach((to, from, next) => {
// 全局前置守卫逻辑
console.log('路由跳转-前置守卫', to, from)
});
router.afterEach((to, from, next) => {
// 全局后置守卫逻辑
console.log('路由跳转-后置守卫')
});
export default router
- 全局路由守卫和路由独享守卫
<script lang='ts' setup>
import { useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteLeave((to, from) => {
// 用于离开当前组件前
console.log(to, from)
const bool = window.confirm('你确定要离开当前页面吗?');
if (!bool) { return false }
})
onBeforeRouteUpdate((to, from) => {
// 用于在当前 路由参数等 发生改变;通常在<router-view>组件的子组件中调用
console.log('更新', to, from)
})
</script>
9. 【监听路由】
<template>
<button @click="goQuery">跳转query传参</button>
<button @click="goParams">跳转params传参</button>
<RouterView></RouterView>
</template>
<script lang='ts' setup>
import { RouterView,useRouter, useRoute } from 'vue-router';
import { watch, onBeforeUnmount } from 'vue';
const router = useRouter()
const route = useRoute()
function goQuery() {
router.push({
path: '/login',
query: {
id: '001',
title: '新闻',
content: '吃饭了吗'
}
})
}
function goParams() {
router.push({
name: 'Login',
params: {
id: '001',
title: '新闻',
content: '吃饭了吗'
}
})
}
//监视:路由的参数、路径等
watch(() => route.query, (newValue, oldValue) => {
console.log('监视query', newValue, oldValue)
})
watch(() => route.params, (newValue, oldValue) => {
console.log('监视params', newValue, oldValue)
})
watch(() => route.path, (newValue, oldValue) => {
console.log('path', '当前地址:' + newValue, '旧地址:' + oldValue)
})
</script>
八、全局数据存储-pinia
1.【安装引入】
安装:
npm i pinia
//在main.js中引入
import { createApp } from 'vue'
import {createPinia} from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia).mount('#app')
2.【基础使用方法:存储+读取数据】
第一步:在/src/store/文件夹下创建TS文件,每个ts文件作为一个模块import { defineStore } from 'pinia' import { computed ,reactive } from 'vue' type User = { name:string, sex: string, age: number, wealth: number, car: { brand: string, price:number } } interface ListInter{ id?: number, name?: string } //defineStore(仓库的唯一标识,()=>{xxx}) export const useUserInfoStore = defineStore('userInfo', () => { //声明数据state const userInfo:User = reactive({ name: '胡歌', sex: '男', age: 20, wealth:5000, car: { brand: '奔驰', price: 230000 } }) //声明修改数据的方法actions const changeCar = () => { userInfo.car = Object.assign(userInfo.car, { brand: '宝马', price: 32000 }) } const changeName = (value:string) => { userInfo.name = value } const changeAge = () => { userInfo.age += 1 } const changeWealth = (value:number) => { userInfo.wealth += value } let list: Array<ListInter> = reactive([]) //声明操作数据方法,支持异步 const getList = async () => { let { data: { data } } = await axios.get('http://geek.itheima.net/v1_0/channels') console.log(data.channels) list = Object.assign(list, data.channels) // list.value = data.channels } //声明基于数据派生的计算属性 getters const doubleMoney = computed(() => userInfo.wealth*3) //return 变量、方法 return { userInfo, changeCar, changeName, changeAge, changeWealth, doubleMoney, list, getList } })
Home.vue<template> <div class="login"> <h2>我是Login</h2> <div>姓名:{{ userInfo.name }}</div> <div>年龄:{{ userInfo.age }}</div> <div>财富:{{ userInfo.wealth }}</div> <div> 车辆:{{ userInfo.car.brand }} -- 价格:{{ userInfo.car.price }} </div> <button @click="changeAge">修改年龄</button> <br> <button @click="changeCar">修改汽车</button> </div> </template> <script lang='ts' setup> import { storeToRefs } from 'pinia' import { useUserInfoStore } from '@/store/userInfo' // 不结构接收 const userInfoStore = useUserInfoStore() //解构,数据需要使用storeToRefs;方法可直接解构,毕竟方法又不是响应式 const { userInfo } = storeToRefs(userInfoStore) const { changeCar, changeAge } = userInfoStore </script>
About.vue<template> <div class="home"> <h2>我是Home</h2> <div>姓名:{{ userInfo.name }}</div> <div>年龄:{{ userInfo.age }}</div> <div>财富:{{ userInfo.wealth }}</div> <div> 车辆:{{ userInfo.car.brand }} -- 价格:{{ userInfo.car.price }} </div> <br> <button @click="changeName('李白')">修改姓名</button> <br> <button @click="changeWealth(500)">修改财富</button> </div> </template> <script lang='ts' setup> import { storeToRefs } from 'pinia' import { useUserInfoStore } from '@/store/userInfo' // 不结构接收 const userInfoStore = useUserInfoStore() //解构,数据需要使用storeToRefs;方法可直接解构,毕竟方法又不是响应式 const { userInfo } = storeToRefs(userInfoStore) const { changeName,changeWealth } = userInfoStore </script>
3.【pinia持久化】
使用pinia存储的数据,刷新页面数据就失效了,需要使用插件,插件官网(本质是数据本地存储)
注意:
- 只有修改了pinia-plugin-persistedstate包裹的数据,才会持久化(本地存储)
- 如果本地缓存已有数据,pinia不会主动获取,赋值给变量初始值,需要在初值时判断是否已经有本地存储
1.安装 npm i pinia-plugin-persistedstate
2.引入//将插件添加到 pinia 实例上 import { createApp } from 'vue' import { createPinia } from 'pinia' import persisted from 'pinia-plugin-persistedstate' import App from './App.vue' const app = createApp(App) const pinia = createPinia() app.use(pinia.use(persisted)).mount('#app')
3.配置使用//在模块中,作为defineStore函数的第三个参数 //persist:true ,如果不配置默认将所有变量存储在localStorage中 //defineStore(仓库的唯一标识,()=>{xxx}, {persist:true}) import { defineStore } from 'pinia' import { computed ,reactive, ref } from 'vue' import axios from 'axios' type User = { name:string, sex: string, age: number, wealth: number, car: { brand: string, price:number } } interface ListInter{ id?: number, name?: string } //defineStore(仓库的唯一标识,()=>{xxx},{配置项}) export const useUserInfoStore = defineStore('userInfo', () => { //声明数据state const userInfo:User = reactive({ name: '胡歌', sex: '男', age: 20, wealth:5000, car: { brand: '奔驰', price: 230000 } }) //声明修改数据的方法actions const changeCar = () => { userInfo.car = Object.assign(userInfo.car, { brand: '宝马', price: 32000 }) } const changeName = (value:string) => { userInfo.name = value } const changeAge = () => { userInfo.age += 1 } const changeWealth = (value:number) => { userInfo.wealth += value } //如果想页面一刷新就先去看本地存储是否已经缓存了内容 //let list:类型= reactive(JSON.parse(sessionStorage.getItem('list') as string) || []) let list: Array<ListInter> = reactive([]) //声明操作数据方法,支持异步 const getList = async () => { let { data: { data } } = await axios.get('http://geek.itheima.net/v1_0/channels') list = Object.assign(list, data.channels) } const doubleMoney = computed(() => userInfo.wealth*3) return { userInfo, changeCar, changeName, changeAge, changeWealth, doubleMoney, getList, list } }, { /* 默认设置 persist: true, //所有变量存储在localStorage中 */ /* 统一设置存储规则 persist: { key: 'user', //自定义存储在本地的key storage: sessionStorage,// 可以选择存储方式,localStorage | sessionStorage paths: ['list']//选择state中那些变量需要持久化 } */ // 变量单独设置存储规则 persist: [ { key: 'info', storage: localStorage, paths: ['userInfo'] }, { key: 'list', storage: sessionStorage, paths: ['list'] } ] })
九、补充
1.【shallowRef和shallowReactive】
-
作用:创建一个浅层响应式数据,只会使数据的浅层(第一层)属性变成响应式的,数据内部的嵌套属性则不会变成响应式的
-
意义: 通过使用
shallowRef()和shallowReactive()来绕开深度响应。浅层式API创建的状态只在其浅层(第一层)是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
<template>
<div>
<h1>ref--shallowRef</h1>
<div>{{ user.name }}</div>
<div>{{ user.car }}</div>
<div>{{ user.car.brand }}</div>
<br>
<div>{{ user2.name }}</div>
<div>{{ user2.car }}</div>
<div>{{ user2.car.brand }}</div>
<button @click="changeUser">改变user信息</button>
<button @click="changeUser2">改变user2信息</button>
<button @click="changeUser3">同时改变user、user2信息</button>
</div>
</template>
<script lang='ts' setup>
import { ref,shallowRef,reactive,shallowReactive } from 'vue';
let user = ref({
name: '胡歌',
age: 20,
car: {
brand: '宝马',
price:2000000
}
})
let user2 = shallowRef({
name: '胡歌2',
age: 20,
car: {
brand: '宝马2',
price: 2000000
}
})
function changeUser() {
//修改ref创造的数据,任何层的属性,视图都会更新
user.value.name = '陈瑶'
user.value.car.brand = '奔驰'
}
function changeUser2() {
//修改shallowRef创造的数据,修改浅层(第一层)属性,视图才会更新,简单理解只能修改.value=xx
user2.value={name:'胡汉三',age:50,car:{brand:'毛驴车',price:1000}}
//数据变了,但视图不会更新
/*
user2.value.name = "陈瑶2"
user2.value.car.brand = '奔驰2'
*/
}
function changeUser3() {
//因为修改了user,视图会更新
user.value.name = '陈瑶'
user.value.car.brand = '奔驰'
/*
修改shallowRef生成的数据的深层数据,虽然不会更新视图,
但是因为user会更新视图,所以user2的数据也被动的在视图上被更新了
*/
user2.value.name = "陈瑶2"
user2.value.car.brand = '奔驰2'
}
</script>
2.【readonly和shallowReadonly 】
2-1.readonly
-
作用及特点:
- 对象的所有嵌套属性都将变为只读。
- 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
-
用法:
const original = reactive({ ... }); //readOnlyCopy的全部属性变为只读 const readOnlyCopy = readonly(original); let user = reactive({ name: '胡歌', age: 20, car: { brand: '宝马', price:2000000 } }) let user2 = readonly(user) // user2的任何属性都不可以修改,但user数据修改,user2也会同步更新 -
应用场景:
- 创建不可变的状态快照。
- 保护全局状态或配置不被修改。
2-2.shallowReadonly
-
作用:与
readonly类似,但只作用于对象的浅层属性。 -
用法:
const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original); let user2 = shallowReadonly({ name: '胡歌2', age: 20, car: { brand: '宝马2', price: 2000000 } }) function changeUser() { //user2.name = '陈瑶' //浅层属性不允许修改 // user2.car = {xxx} //浅层属性不允许修改 user2.car.brand = '奔驰' //深层属性可以修改 } -
特点:
-
只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
-
适用于只需保护对象顶层属性的场景。
-
3.【toRaw 与 markRaw】
3-1.toRaw
作用:用于获取一个响应式对象的原始对象,
toRaw返回的对象不再是响应式的,不会触发视图更新。在需要将响应式对象传递给非Vue的库或外部系统时,使用toRaw可以确保它们收到的是普通对象。
- 写法
注意:只能用于reactive生成的数据
/* toRaw */
// 响应式对象
let person = reactive({name:'tony',age:18})
// 原始对象
let rawPerson = toRaw(person)
- 案例
<template>
<div>
<div>{{ user.name }}</div>
<div>{{ user.age }}</div>
<div>{{ user.car }}</div>
<button @click="change">修改user2</button>
<button @click="changeUser">修改user</button>
</div>
</template>
<script lang='ts' setup>
import { ref,toRaw,reactive} from 'vue';
//响应式对象
let user = reactive({
name: '胡歌',
age: 20,
car: {
brand: '宝马',
price:2000000
}
})
//原始对象
let user2 = toRaw(user)
//修改原始对象的属性
function change() {
user2.name = '白居易'
console.log(user)//其实user中的name也被修改了,但是没有更新视图
}
//更新user中的其他属性,引起视图更新,也会更新视图中的name的数据
function changeUser() {
user.age=23
}
</script>
3-2.markRaw
- 作用:标记一个对象,使其永远不会变成响应式的。
- 写法:
import { markRaw} from 'vue';
/* markRaw */
let citys = markRaw([
{id:'asdda01',name:'北京'},
{id:'asdda02',name:'上海'},
{id:'asdda03',name:'天津'},
{id:'asdda04',name:'重庆'}
])
// 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
let citys2 = reactive(citys)
4.【customRef】
- 作用:创建一个自定义的
ref,并对其依赖项跟踪和更新触发进行逻辑控制。 - 写法:
<template> <div> <h2>{{ name }}</h2> <input v-model="name"> </div> </template> <script lang='ts' setup> import { ref,customRef} from 'vue'; //使用ref定义的响应式数据,一改变,视图立马更新 // let name =ref("胡歌") //使用customRef定义的响应式数据 //track(跟踪) ,trigger(触发) let initName="胡歌" let name = customRef((track,trigger) => { return { //get调用--数据被读取时 get() { track()//先执行track(),告诉vue持续关注数据,一但变化就更新 return initName }, //set调用--数据被修改时 set(value) { initName = value trigger() //数据修改后执行trigger(),通知vue数据变化了 } } }) </script> - 实现防抖效果:
<template> <div> <h2>{{ name }}</h2> <input v-model="name"> </div> </template> <script lang='ts' setup> import {customRef} from 'vue'; //使用customRef定义的响应式数据,实现修改数据,1s后视图上<h2>元素内容才更新 let initName="胡歌" let timer:number let name = customRef((track,trigger) => { return { get() { track() return initName }, set(value) { clearTimeout(timer) timer= setTimeout(() => { initName = value trigger() }, 1000); } } }) </script>
4.【新组件:Teleport 】
Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。
<!-- teleport组件 to='放置在某个元素内' -->
<teleport to='body'>
<div class="modal" v-show="isShow">
<h2>我是一个弹窗</h2>
<p>我是弹窗中的一些内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
5.【新组件:Suspense 】
Suspense 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
子组件<template> <div>{{ num }}</div> </template> <script lang='ts' setup> import { ref } from 'vue'; //模拟3s后获得数据,然后展示 //在setup函数中,使用了await,将组件变了从异步组件 let result= await new Promise((resolve, reject) => { setTimeout(() => { resolve(1) }, 3000) }) let num = ref(result) </script>
父组件<template> <h2>我是父组件</h2> <Suspense> <!-- v-slot:default 真正要展示的组件--> <template v-slot:default> <Child/> </template> <!-- v-slot:fallback 等待时间中的提示组件--> <template v-slot:fallback> <h3>加载中.....</h3> </template> </Suspense> </template> <script lang='ts' setup> //传统写法 // import { Suspense, defineAsyncComponent } from 'vue' // const Child = defineAsyncComponent(() => import('./ChildTwo.vue')) //语法糖简写 import Child from './ChildTwo.vue' import { Suspense } from 'vue' </script>
6.【全局API转移到应用对象】
app.component:全局注册组件
import { createApp } from 'vue'
import App from './App.vue'
import Hello from '@/views/Child/ChildTwo.vue'
const app = createApp(App)
//app.component(组件名,引入的组件)
app.component('Hello',Hello)
app.mount('#app')
app.config:配置全局属性
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
//app.config.globalProperties.xxx==内容
app.config.globalProperties.fruit = "苹果"
declare module 'vue' {
interface ComponentCustomProperties {
//ts要判断类型
fruit:string
}
}
app.mount('#app')
app.directive:全局指令
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) //app.directive(指令名,(元素,{传入的值})=>{ xxx }) app.directive('beauty', (element, { value}) => { element.innerText += value element.style.color='green' element.style.background='yellow' }) app.mount('#app')<template> <!-- v-指令名=value --> <h2 v-beauty="'苹果'">我最喜欢的水果--</h2> </template>
-
app.mount:挂载 -
app.unmount:卸载 -
app.use:安装插件
十、vue3非兼容性改变
-
过渡类名
v-enter修改为v-enter-from、过渡类名v-leave修改为v-leave-from。 -
keyCode作为v-on修饰符的支持。 -
v-model指令在组件上的使用已经被重新设计,替换掉了v-bind.sync。 -
v-if和v-for在同一个元素身上使用时的优先级发生了变化。 -
移除了
$on、$off和$once实例方法。 -
移除了过滤器
filter。 -
移除了
$children实例propert。