奥奥の探索日记
ShaoAo
文章18
标签19
分类6

文章分类

Vue3学习笔记

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()里面,那么就默认是响应式数据。

  1. 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
    1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
    2. watch:要明确指出监视的数据
    3. 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的生命周期

  1. beforeCreate(创建前)

    • 你可以在这个阶段做些准备工作,但是此时还没有真正创建 Vue 实例。也就是说,Vue 实例还没有被初始化。
  2. created(创建后)

    • Vue 实例被创建好了,但是此时还没有开始创建真正的 DOM。你可以在这个时候做一些数据的初始化工作,或者请求一些初始数据。
  3. beforeMount(挂载前)

    • 这个阶段是在 Vue 开始把你的页面内容放到 DOM 中之前调用的。你可以在这里做一些DOM准备工作。
  4. mounted(挂载后)

    • 这时候 Vue 实例已经挂载到了页面上,你的页面已经显示了 Vue 组件的内容。通常你可以在这里做一些需要操作 DOM 的工作,比如初始化页面后的一些动画效果。
  5. beforeUpdate(更新前)

    • 在数据更新但页面尚未重新渲染时调用。你可以在这里做一些在更新前需要的操作,比如对比更新前后的数据。
  6. updated(更新后)

    • 数据已经更新,页面也已经重新渲染完毕。你可以在这个阶段做一些需要最新数据的操作,比如更新一些在页面上显示的内容。
  7. beforeDestroy(销毁前)

    • 当你要销毁 Vue 实例之前调用。你可以在这里做一些清理工作,比如取消定时器、移除绑定的事件等。
  8. 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-ifv-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来命名,比如useOrderuseSum

具体就是新建一个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. 导航区、展示区
  2. 请来路由器

安装路由

1
npm i vue-router

涉及到路由,就必须有router这个文件夹,因为我们使用标准文件划分,并且指定制定路由事要想好路由器的工作模式。

Vue Router 提供了两种路由模式:historyhash。这两种模式主要区别在于 URL 的显示方式和浏览器历史记录的管理方式。

  1. **Hash 模式(哈希模式)**:在这种模式下,URL 中会包含一个 # 符号,例如 http://example.com/#/home。这个 # 后面的部分(hash)不会发送到服务器,它只是客户端用来跟踪应用状态的一种方式。当用户点击浏览器的后退按钮时,应用会根据 URL 中的 hash 值来显示相应的视图。createWebHashHistory

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

两个注意点

  1. 路由组件通常存放在pagesviews文件夹,一般组件通常存放在components文件夹。

    路由组件:靠路由规则渲染出来的,比如以下这是三个,一般是放在pagesviews文件夹里(视图)。

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/>
  1. 通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

components组合成为view,一个页面(page)由多个view组合(例如导航栏、显示区等)而成。

路由器工作模式

  1. history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL

    缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

    history:createWebHistory()

  2. 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.vueto地址的同时传入参数,写法如下

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
			]
		}
	]
})
  1. 传递参数

这里的跳转只可以使用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. 接收参数:
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

geetersstateactions都是 兄弟,所以他们都是同级,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的组合式写法

我们之前写的,属于是选项式写法,比如actionsstate都是写在了一个对象里,我们把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

当你定义函数的时候,有一个默认参数,这个参数就是事件对象,里边包含了很多事件,比如鼠标移动之类的坐标。

  1. 概述:自定义事件常用于:子 传 父。

  2. 注意区分好:原生事件、自定义事件。

在定义@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)
}
  1. 原生事件:

    • 事件名是特定的(clickmosueenter等等)
    • 事件对象$event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  2. 自定义事件:

    • 事件名是任意名称
    • 事件对象$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 提供了四个主要方法:

  1. on(event, handler)
    • 绑定事件
  2. off(event, handler)
    • 解绑事件
  3. emit(event, …args)
    • 触发事件
  4. 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

  1. 概述:$attrs用于实现当前组件的父组件,向当前组件的子组件通信(祖→孙)。
    可以理解父组件通过子组件来传递给孙组件。

  2. 具体说明:$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

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

    属性 说明
    $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 很好用

  1. 概述:实现祖孙组件直接通信,不打扰子组件。
  2. 具体使用:
    • 在祖先组件中通过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 插槽

一般是同时呈现多个组件,但是组件里面的内容只有些许的不一样,这样就不用写多个组件,只需要用一个组件来完成。

默认插槽

img

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>里面传递过来的gamesax三个参数、即为所有参数。

对于命名还是和之前一样,比如:v-slot:s="params",子组件:name='s'

数据的维护什么都在子组件中,但是根据这些数据所生成的结构是父组件决定的。

简单理解:压岁钱在孩子那,但是根据压岁钱买的东西,却由父亲来决定。

其他API

shallowRef 与 shallowReactive

需要从vue中引用

shallowRef

  1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。
  2. 用法:
1
let myVar = shallowRef(initialValue);
  1. 特点:只跟踪引用值的变化,不关心值内部的属性变化。

简单理解:我可以对person.value进行修改值,并且有响应式处理,但是我对person.value.name就没有响应式处理了,就是只可以.一次。

如果你关注的是数据的整体修改,那么就使用shallowRef

shallowReactive 同理。

作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的。

通过使用 shallowRef()shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。

readonly 与 shallowReadonly

都是需要通过vue来引用的,然后直接使用。

readonly

  1. 作用:用于创建一个对象的深只读副本,限制所有层次。
  2. 用法:
    1
    2
    const original = reactive({ ... });  
    const readOnlyCopy = readonly(original);

当你在修改original的时候,那么readOnlyCopy的值也会跟着响应式进行变化,是有关联关系的,但是如果直接修改readOnlyCopy则无法修改,会提示警告.

相当于复制了一份只能用来使用但是不能修改的数据。

  1. 特点:
    • 对象的所有嵌套属性都将变为只读,并且readonly的参数必须是响应式数据(ref,reactive)。
    • 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。
  2. 应用场景:
    • 创建不可变的状态快照。
    • 保护全局状态或配置不被修改。

shallowReadonly

readonly 类似,但只作用于对象的顶层属性,就是只限制第一层,更深层次的就不限制了。

1
2
const original = reactive({ ... });  
const shallowReadOnlyCopy = shallowReadonly(original);

toRaw 与 markRaw

需要从vue中引用。

toRaw 函数用于获取由 refreactivereadonly 等创建的响应式对象的原始未代理的对象。当你需要访问到对象的原始数据,而不是其响应式代理时,就可以使用 toRawtoRaw 返回的对象不再是响应式的,不会触发视图更新。

语法: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(触发)。

Vue3新组件


本文作者:ShaoAo
本文链接:https://sawr.gitee.io/2024/03/04/vue/vue3study/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议许可,仅作个人记录,转载请说明出处!