阅读 461

Vue3+VueRouter4.x 学习总结

一、初始化 Vue3 项目

使用vite方式:

npm init vite-app vue3-demo 复制代码

二、setup 函数

1、setup 使用

src/App.vue文件:

<template>   <div>{{ readersNumber }} {{ book.title }}</div> </template> <script>   import { ref, reactive } from 'vue'   export default {     setup() {       const readersNumber = ref(0)       const book = reactive({ title: 'Vue 3 Guide' })       // expose to template       return {         readersNumber,         book       }     }   } </script> 复制代码

setup 存在的意义,就是为了让你能够使用新增的组合 API。并且这些组合 API 只能在 setup 函数内使用,

setup 调用的时机是创建组件实例,然后初始化 props,紧接着就是调用 setup 函数。新的 setup 组件选项在创建组件之前执行,一旦 props 被解析,并充当合成 API 的入口点。

从生命周期钩子的角度来看,它会在 beforeCreate 钩子之前被调用,由于在执行 setup 时尚未创建组件实例,因此在 setup 选项中没有 this ,所以 setup 内是拿不到 this 上下文的,这就意味着,除了 props 之外,将在setup无法访问组件中声明的任何属性——本地状态计算属性方法

Vue2.0 和 Vue3.0 生命周期对比

Vue 2.0Vue 3.0
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
activatedonActivated
deactivatedonDeactivated
errorCapturedonErrorCaptured

除去 beforeCreatecreated 之外,在我们的 setup 方法中,有 9 个旧的生命周期钩子,我们可以在 setup 方法中访问。

1、setup():开始创建组件之前,在 beforeCreatecreated 之前执行。创建的是datamethod

2、onBeforeMount(): 组件挂载到节点上之前执行的函数。

3、onMounted():组件挂载完成后执行的函数。

4、onBeforeUpdate():组件更新之前执行的函数。

5、onUpdated():组件更新完成之后执行的函数。

6、onBeforeUnmount():组件卸载之前执行的函数。

7、onUnmounted():组件卸载完成后执行的函数。

8、onActivated():被 keep-alive 缓存的组件激活时调用。

9、onDeactivated():被 keep-alive 缓存的组件停用时调用,比如从 A 组件,切换到 B 组件,A 组件消失时执行。

10、onErrorCaptured():当捕获一个来自子孙组件的异常时激活钩子函数。

src/App.vue文件:

<template>   <div>{{ readersNumber }} {{ book.title }}</div> </template> <script>   import { ref, reactive } from 'vue'   export default {     setup() {       const readersNumber = ref(0)       const book = reactive({ title: 'Vue 3 Guide' })       // expose to template       return {         readersNumber,         book       }     }   } </script> 复制代码

通过 refreactive 方法包裹的变量,会被赋予响应式的能力,也就是说你在 setup 函数内改变他们的值,模板内也会响应展示改变后的值,如下所示:

<template>   <div>{{ readersNumber }} {{ book.title }}</div> </template> <script> import { ref, reactive } from 'vue' export default {   setup() {     const readersNumber = ref(0)     const book = reactive({ title: 'Vue 3 Guide' })     setTimeout(() => {       // 通过 ref 包裹的变量,需要通过 .value 的形式修改变量,在模板中使用无需使用 .value ,因为在 setup 内 return 的时候,已经帮你接开。       readersNumber.value = 1       book.title = 'Vue3 Book'     }, 2000)     // 暴露给模板 expose to template     return {       readersNumber,       book,     }   }, } </script> 复制代码

reactive定义复杂的数据类型的数据,ref推荐定义基本数据类型,所以如果要使用reactive定义基本数据类型的话,我们需要在reactive中将数据包装一下

2、setup 中的渲染函数 (h)/JSX 方法

渲染函数 h 是 createElement 的别名

注意:这里必须写成 h,不能写成其它名称

<template>   <div>{{ readersNumber }} {{ book.title }}</div> </template> <script> import { ref, reactive, h } from 'vue' export default {   setup() {     const readersNumber = ref(0)     const book = reactive({ title: 'Vue 3 Guide' })     setTimeout(() => {       // 通过 ref 包裹的变量,需要通过 .value 的形式修改变量,在模板中使用无需使用 .value ,因为在 setup 内 return 的时候,已经帮你接开。       readersNumber.value = 1       book.title = 'Vue3 Book'     }, 2000)     // 暴露给模板 expose to template     // return {     //   readersNumber,     //   book,     // }     return ()=> h('h1',readersNumber,book.title)   }, } </script> 复制代码

3、setup 中的参数

setup 函数接收两个参数,第一个是 props 对象,第二个是 context 上下文

3.1 props 对象

src/App.vue文件:

<template>   <HelloWorld :count1="count" /> </template> <script> import { ref, reactive } from 'vue' import HelloWorld from './components/HelloWorld.vue' export default {   components: {     HelloWorld,   },   setup() {     const count = ref(0)     // 暴露给模板 expose to template     return { count }   }, } </script> </script> 复制代码

components/HelloWorld.vue文件:

<template>   <h2>{{ count1 }}</h2> </template> <script> export default {   name: 'HelloWorld',   props: {     count1: Number   },   // data() {   //   return {   //     count: 0   //   }   // } setup(props) { console.log('props',props); console.log(props.count1); } } </script> 复制代码

在页面控制台中打印出的 props对象,可以发现它被 Proxy 代理过,这是 Vue 3.0 实现响应式的核心 API,也就是说从父亲组件传到子组件的 count1 变量,已经是响应式数据。

image-20210723211441446

props 对象是响应式的——即在传入新的 props 时会对其进行更新,并且可以通过使用 watchEffectwatch 进行观测和响应:

<template>   <h2>{{ count1 }}</h2> </template> <script> import { watchEffect } from 'vue' export default {   name: 'HelloWorld',   props: {     count1: Number,   },   // data() {   //   return {   //     count: 0   //   }   // }   setup(props) {     // console.log('props',props);     // console.log(props.count1);     watchEffect(() => {       console.log(`props.count1:`, props.count1)     })   }, } </script> 复制代码

请不要解构 props 对象,因为它会失去响应式:

  • 如 setup(...props),这样会让 props 失去响应式。

  • export default {   name: 'HelloWorld',   props: {     count1: Number,   },   setup({ count1 }) {     watchEffect(() => {       console.log(`count1:`, count1) // 没有响应式     })   }, } 复制代码

3.2 context 上下文

传递给 setup 函数的第二个参数是 contextcontext 是一个普通的 JavaScript 对象,从原来的 Vue 2.0 中的 this 选择性的暴露一些属性。context为我们提供了三个属性,分别是:

  • attrs

  • slots

  • emit

export default {   setup(props, context) {     // Attribute (非响应式对象)     console.log(context.attrs)     // 插槽 (非响应式对象)     console.log(context.slots)     // 触发事件 (方法)     console.log(context.emit)   }, } 复制代码

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

export default {   setup(props, { attrs, slots, emit }) {     ...   } } 复制代码

attrs属性

attrs为我们提供了最新传入的数据,在使用 attrs 时候,同一个变量名,如果在 props 中声明了,则变量会在 props 中,attrs 中就取不到这个变量,类似下面这样:

components/HelloWorld.vue文件:

<template>   <h2>{{ count1 }}</h2> </template> <script> import { watchEffect } from 'vue' export default {   name: 'HelloWorld',   // 这里的props中的变量,不要跟context中的attrs中的变量冲突   /* props: {     count1: Number   }, */   setup(props, context) {     // console.log(context.attrs.count1)     return {       count1: context.attrs.count1,     }   }, } </script> </script> 复制代码

App.vue文件:

<template>   <HelloWorld :count1="count" /> </template> <script> import { ref } from 'vue' import HelloWorld from './components/HelloWorld.vue' export default {   components: {     HelloWorld,   },   setup() {     const count = ref(100)     return { count }   }, } </script> 复制代码

在控制台中,我们可以通过 $vm.ctx.count1 拿到 count1 的值(ctx 就是 context 上下文)。如下图所示:

image-20210805200008000

emit属性

子组件可以 通过 ctx.emit 把一个方法传给父组件,父组件可以在子组件上通过事件,接收子组件传入的参数

components/HelloWorld.vue文件:

<template>   <div>     <h2>{{ count1 }}</h2>     <button @click="addHanle">addHanle方法</button>   </div> </template> <script> export default {   name: 'HelloWorld',   props: {     count1: Number,   },   setup(props, ctx) {     const addHanle = () => {       ctx.emit('addClick', 50, 1)     }     return {       addHanle,     }   }, } </script> 复制代码

App.vue文件:

<template>   <HelloWorld :count1="count" @addClick="add" /> </template> <script> import { ref } from 'vue' import HelloWorld from './components/HelloWorld.vue' export default {   components: {     HelloWorld,   },   setup() {     const count = ref(0)     // num1、 num2 就是从子组件(HelloWorld组件)中 ctx.emit 传入的 50,1     const add = (num1, num2) => {       // console.log('num1',num1); // 50       // console.log('num2',num2); // 1       count.value += num1 + num2     }     return {       count,       add,     }   }, } </script> 复制代码

代码执行流程:

image-20210806210234392

这里需要注意的是,子组件中 steup 中的 props 虽然没有用到,但是必须要写到第一个参数的位置,如果不写,只写 ctx,那么 ctx 就成为了第一个参数,这就不对了(steup 函数中,第一个参数必须是 props,第二个参数是 ctx)。

image-20210806211249404

最终效果如下图所示:

Untitled

三、响应式 API

响应式 API 就是 Vue3 新特性中是如何去实现 Vue 的响应式功能的 API,介绍六个常见响应式 API:reactiverefcomputedreadonlywatchEffectwatch 的使用方法。

1、 响应性基础 APIs

1.1 reactive

reactiveVue 3.0 中提供的实现响应式数据的方法。在 Vue 2.0 中实现响应式数据是通过 Object 的 defineProPerty 属性来实现的,而在 Vue 3.0 中的响应式是通过 ES2015 的 Proxy 来实现。

reactive 方法的参数必须是对象(JSON、Array)

<template>   <div>     <h1>书名:{{ book.bookName }}</h1>     <h2>作者:{{ book.authorName }}</h2>   </div> </template> <script> import { reactive } from 'vue' export default {   setup() {     const book = reactive({       bookName: '《Vue3基础入门》',       authorName: '筱竹',     })     return {       book,     }   }, } </script> 复制代码

显示效果:

image-20210806214156924

reactive 包裹的对象,已经通过 Proxy 进行响应式赋能,所以我们可以通过如下形式修改值,会直接体现在 template 模板上。

<template>   <div>     <h1>书名:{{ book.bookName }}</h1>     <h2>作者:{{ book.authorName }}</h2>   </div> </template> <script> import { reactive } from 'vue' export default {   setup() {     const book = reactive({       bookName: '《Vue3基础入门》',       authorName: '筱竹',     })     setTimeout(() => {       book.bookName = 'Hello Vue3'       book.authorName = 'xiaozhu'     }, 1000)     return {       book,     }   }, } </script> 复制代码

1 秒后你将会在浏览器上看到“《Vue3 基础入门》”变成了“Hello Vue3”,筱竹变成了 xiaozhu。

image-20210806214532701

响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的代理是等于原始对象。建议只使用响应式代理,避免依赖原始对象。

上述是官方文档描述 reactive 返回的代理后的对象,内部的二级三级属性,都会被赋予响应式的能力,所以官方建议,使用 reactive 返回的值,而不要去使用原始值。

1.2 ref

refreactive 一样,同样是实现响应式数据的方法。在业务开发中,我们可以使用它来定义一些简单数据(基本数据类型),如下所示:

<template>   <div>     {{ count }}   </div> </template> <script> import { ref } from 'vue' export default {   setup() {     const count = ref(100)     return { count }   }, } </script> 复制代码

修改数据:

<template>   <div>     {{ count }}   </div> </template> <script> import { ref } from 'vue' export default {   setup() {     const count = ref(100)     // 修改count的值     count.value = 10     return { count }   }, } </script> 复制代码

修改数据,可以通过 count.value = 1 类似这样的语法去修改。但是为什么它需要这样去修改变量,而 reactive 返回的对象可以直接修改如 state.title = Vue3

原因是 Vue 3.0 内部将 ref 悄悄的转化为 reactive,如上述代码会被这样转换:

ref(0) => reactive({ value: 0 }) 复制代码

所以 count 相当于 reactive 返回的一个值,根据 reactive 修改值的方式,就可以理解为什么 ref 返回的值是通过 .value 的形式修改值了。

ref 作为渲染上下文的属性返回(即在 setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value。之所以会自动解套,是因为 template 模板在被解析的时候,Vue 3.0 内部通过判断模板内的变量是否是 ref 类型。如果是,那就加上 .value,如果不是则为 reactive 创建的响应集代理数据。

我们不妨打印一下 ref 创建的对象 console.log(count),浏览器控制台如下图所示:

image-20210809202718364

没错,就是通过上图 __v_isRef 变量去判断,模板内的变量是否为 ref 类型。判断类型也可以通过 isRef 方法,如下:

<template>   <div>     {{ count }}   </div> </template> <script> import { isRef, ref } from 'vue' export default {   setup() {     const count = ref(100)     // 修改count的值     count.value = 10     console.log('isRef(count)', isRef(count)) // true     return { count }   }, } </script> 复制代码

Vue 2.0 中,我们可以通过给元素添加 ref="xxx" 属性,然后在逻辑代码中通过 this.$refs.xxx 获取到对应的元素。

到了 Vue 3.0 后,setup 函数内没有 this 上下文,因此我们可以通过 ref 方法去获取,并且还需要在页面挂载以后才能拿到元素。

<template>   <div ref="xiaozhuRef">筱竹</div> </template> <script> import { onMounted, ref } from 'vue' export default {   setup() {     const xiaozhuRef = ref(null)     console.log(xiaozhuRef.value) // null     // onMounted 页面挂载完成后执行的函数, xiaozhuRef.value 需要在页面在挂载之后,才能拿到     onMounted(() => {       console.log(xiaozhuRef.value) // <div>筱竹</div>     })     return { xiaozhuRef }   }, } </script> 复制代码

1.3 toRef

toRef 用于为源响应式对象上的属性新建一个ref,从而保持对其源对象属性的响应式连接。接收两个参数:源响应式对象和属性名,返回一个 ref 数据。例如使用父组件传递的props数据时,要引用props的某个属性且要保持响应式连接时就很有用。

  • 获取数据值的时候需要加.value

  • toRef 后的ref数据不是原始数据的拷贝,而是引用,改变结果数据的值也会同时改变原始数据

<template>   <div class="hello">     <h1>{{ obj.name }}</h1>     <h1>{{ newObj }}</h1>     <button @click="changeName">改变msg</button>   </div> </template> <script> import { toRef } from 'vue' export default {   name: 'HelloWorld',   setup() {     let obj = { name: '筱竹', age: 18 }     const newObj = toRef(obj, 'name')     const changeName = () => {       newObj.value = 'xiaozhu'       console.log('obj', obj)       console.log('newObj', newObj)     }     return {       obj,       newObj,       changeName,     }   }, } </script> 复制代码

image-20210916205200879


从以上代码可以看出,当触发changeName函数时,通过改变newObj结果数据,同时也改变obj的原始数据,但是需要注意,如果修改通过toRef创建的响应式数据,并不会触发 UI 界面的更新。所以,toRef的本质是引用,与原始数据是有关联的。

ref示例代码(和toRef做比较):

<template>   <div class="hello">     <h1>{{ obj.name }}</h1>     <h1>{{ newObj }}</h1>     <button @click="changeName">改变msg</button>   </div> </template> <script> import { ref } from 'vue' export default {   name: 'HelloWorld',   setup() {     let obj = { name: '筱竹', age: 18 }     const newObj = ref(obj.name)     const changeName = () => {       newObj.value = 'xiaozhu'       console.log('obj', obj)       console.log('newObj', newObj)     }     return {       obj,       newObj,       changeName,     }   }, } </script> 复制代码

image-20210916205615660

上述代码,当触发changeName函数时,newObj响应式数据发生改变,而原始数据obj并不会改变,会触发 UI 界面的更新 原因在于,ref 的本质是拷贝,与原始数据没有引用关系。

reftoRef的区别 (1). ref本质是拷贝,修改响应式数据不会影响原始数据;toRef的本质是引用关系,修改响应式数据会影响原始数据 (2). ref数据发生改变,界面会自动更新;toRef当数据发生改变是,界面不会自动更新 (3). toRef传参与ref不同;toRef接收两个参数,第一个参数是哪个对象,第二个参数是对象的哪个属性

所以如果想让响应式数据和以前的数据关联起来,并且想在更新响应式数据的时候不更新 UI,那么就使用 toRef

1.4 toRefs

toRefs()函数可以将 reactive()创建出来的响应式对象,转换为普通对象,只不过这个对象上的每个属性节点,都是 ref()类型的响应式数据

有的时候,我们希望将对象的多个属性都变成响应式数据,并且要求响应式数据和原始数据关联,并且更新响应式数据的时候不更新界面,就可以使用 toRefs,用于批量设置多个数据为响应式数据。(toRef 一次仅能设置一个数据)

<template>   <div class="hello">     <h1>{{ obj.name }}</h1>     <h1>{{ newObj.name.value }}</h1>     <button @click="changeName">改变msg</button>   </div> </template> <script> import { toRefs } from 'vue' export default {   name: 'HelloWorld',   setup() {     let obj = { name: '筱竹', age: 18 }     const newObj = toRefs(obj)     const changeName = () => {       newObj.name.value = 'xiaozhu'       console.log('obj', obj)       console.log('newObj', newObj)     }     return {       obj,       newObj,       changeName,     }   }, } </script> 复制代码

image-20210916211207428

同时 toRefs也可以结合 reactive 进行使用,如下所示:

<template>   <div class="hello">     <!-- <h1>{{ data.msg }}</h1>     <button @click="data.changeMsg">改变msg</button> -->     <h1>{{  msg }}</h1>     <button @click="changeMsg">改变msg</button>   </div> </template> <script> // import { ref } from 'vue'; import { reactive,toRefs } from 'vue'; export default {   name: 'HelloWorld',   setup(){     // const msg = ref('hello vue3');     // const changeMsg = () =>{     //   msg.value = '您好 Vue3';     // }     // return{     //   msg,     //   changeMsg     // }     const data = reactive({       msg:'hello Vue3',       changeMsg:() => {         data.msg = '您好 Vue3'       },     });     return {       // data       ...toRefs(data) // 对 data进行解构     }   } } </script> 复制代码

1.5 computed

Vue 2.0+版本中 ,computed 是一个 选项式(options)API,而到了 Vue 3.0+ 版本中,它将会以钩子函数的形式出现。我们先来修改 App.vue 下的代码,如下所示:

<template> <div>{{ show }}</div> </template> <script> import { reactive, computed } from 'vue' export default {   setup() {     const obj = reactive({       name:'筱竹',       age:18     })     const show = computed(() => {       console.log(obj); // Proxy {name: "筱竹", age: 18}       return obj.name + obj.age     })     setTimeout(() =>{       obj.age = 16     },2000)     return {       show     }   } } </script> 复制代码

上述代码通过 computed 函数将 nameage 变量拼接,返回 show 渲染在模板上。

这里要注意的是 2 秒后,age 变量将会被重新赋值,那么 computed 函数内带有 obj.age,所以会被动态计算,重新返回 show 值,浏览器会有如下变化:

Untitled


若是将 computed 方法内的函数做如下改动:

<template> <div>{{ show }}</div> </template> <script> import { reactive, computed } from 'vue' export default {   setup() {     const obj = reactive({       name:'筱竹',       age:18     })     const show = computed(() => {       console.log(obj); // Proxy {name: "筱竹", age: 18}       return obj.name     })     setTimeout(() =>{       obj.age = 16     },2000)     return {       show     }   } } </script> 复制代码

若是将 computed 方法内的函数做如下改动:

<template>   <div>{{ show }}</div> </template> <script> import { reactive, computed } from 'vue' export default {   setup() {     const obj = reactive({       name: '筱竹',       age: 18,     })     const show = computed(() => {       console.log(obj) // Proxy {name: "筱竹", age: 18}       return obj.name     })     setTimeout(() => {       obj.age = 16     }, 2000)     return {       show,     }   }, } </script> 复制代码

少了 obj.name,定时器 2 秒后,computed中的console.log(obj)不会再次打印,因为函数内不会被检测执行。

述形式 computed 返回的值是不可修改的,通过 getset 的形式返回的值是可修改的,如下所示:

<template>   <div class="main">     <div>       <input type="text" v-model="obj.name" />     </div>     <h1>计算属性</h1>     <div>       <input type="text" v-model="show" />     </div>   </div> </template> <script> import { computed, reactive } from "vue"; export default {   setup() {     const obj = reactive({       name: "筱竹",     });     // vue3中计算属性的函数中如果传入一个对象,表示的是get和set     const show = computed({       get() {         return obj.name;       },       set(val) { // 监视当前属性值的变化,当属性值发生变化时执行,更新相关的属性数据         // val就是obj.name的最新属性值         console.log(val);         let nameVal = val.split("");         obj.name = val;       },     });     return {       obj,       show,     };   }, }; </script> 复制代码

getset 的形式返回值工作中使用场景很少,以上了解即可。

1.4 readonly

readonly 顾名思义,用于创建一个只读的数据,并且所有的内容都是只读,不可修改。我们看如下代码:

<template>   <div>{{ obj.name }}</div> <button @click="handle">改变</button> </template> <script> import { computed, readonly } from "vue"; export default {   setup() {     const obj = readonly({       name: "筱竹", age: 18     })     const handle = () => {       console.log('我被点击了');       obj.name = 'xiaozhu'       obj.age = 16     }      return{ obj, handle}    } }; </script> 复制代码

image-20210812215509237

当点击 改变按钮,无法修改 obj.nameobj.age属性值,同时控制台进行了警告 ⚠️,设置属性值失败,obj 这个对象是一个只读数据。

1.5 watchEffect

watchEffect会追踪响应式数据的变化,并且还会在第一次渲染的时候立即执行,如下所示:

<template>   <div>{{ obj.age }}</div>   <button @click="change">改变年龄</button> </template> <script> import { reactive, watchEffect } from 'vue' export default {   setup() {     const obj = reactive({       age: 18,     })     watchEffect(() => {       console.log(obj.age)     })     const change = () => {       obj.age = 16     }     return { obj, change }   }, } </script> 复制代码

watchEffect 接受一个回调函数作为参数,当执行 change 方法改变 obj.age 变量时,回调函数会被执行,如下所示:

Untitled

watchEffect 函数返回的是一个新的函数,我们可以通过执行这个函数或者当组件被卸载的时候,来停止监听行为,如下所示:

<template>   <div>{{ obj.age }}</div>   <button @click="change">改变年龄</button> </template> <script> import { reactive, watchEffect } from 'vue' export default {   setup() {     const obj = reactive({       age: 18,     })     const stop = watchEffect(() => {       console.log(obj.age)     })     setTimeout(() => {       stop()     }, 1000)     const change = () => {       obj.age = 16     }     return { obj, change }   }, } </script> 复制代码

当 1 秒后,stop 方法被执行,停止监听后,控制台不再打印数据。如下图所示:

Untitled

watchEffect 它接收一个回调函数也接收一个函数 onInvalidate作为参数, 该函数用于清除副作用,用来清除失效时的回调,onInvalidate将会在 watchEffect 监听的变量改变之前执行。

<template>   <div>{{ obj.age }}</div>   <button @click="change">改变年龄</button> </template> <script> import { reactive, watchEffect } from 'vue' export default {   setup() {     const obj = reactive({       age: 18,     })     const stop = watchEffect((onInvalidate) => {       console.log(obj.age)       onInvalidate(() => {         console.log('onInvalid执行了')       })     })     const change = () => {       obj.age = 16     }     return { obj, change }   }, } </script> 复制代码

Untitled

<template>   <div>{{ obj.time }}</div>   <button @click="nowTime">查看现在时间</button> </template> <script> import { reactive, watchEffect } from 'vue' export default {   setup() {     let timer = null     const obj = reactive({       time: Date.now(),     })     watchEffect((onInvalidate) => {       console.log(obj.time)       timer = setTimeout(() => {         console.log('模拟异步请求,2秒后返回内容')       }, 2000)       onInvalidate(() => {         console.log('清除定时器')         clearInterval(timer)       })     })     const nowTime = () => {       obj.time = Date.now()     }     return { obj, nowTime }   }, } </script> 复制代码

watchEffect 回调函数内,我用 setTimeout 的形式去模拟响应时间为 2 秒的异步请求,上面代码可以理解为 2 秒之内如果你不去改变 time 变量,那么页面就成功返回接口数据,如果在 2 秒之内你再次点击按钮改变了 time 变量,onInvalidate 将会被触发,从而清理掉上一次的接口请求,然后根据新的 time 变量去执行新的请求。效果如下图所示:

Untitled

1.7 watch

Vue 3.0 的watch 的功能和之前的 Vue 2.0 的 watch 是完全一样的。和 watchEffect 相比较,区别在 watch 必须侦听一个特定的变量,并且不会默认执行回调函数,而是等到侦听的变量发生改变了才会执行(所以是惰性的)。并且你可以拿到改变前和改变后的值,代码如下:

<template>   <div>{{ obj.time }}</div>   <button @click="handleChange">查看现在时间</button> </template> <script> import { reactive, watch } from 'vue' export default {   setup() {     let timer = null     const obj = reactive({       time: Date.now(),     })     watch(       () => {         return obj.time       },       (beforeData, afterData) => {         console.log(`beforeData`, beforeData)         console.log(`afterData`, afterData)       }     )     const handleChange = () => {       obj.time = Date.now()     }     return { obj, handleChange }   }, } </script> <template>   <div>{{ obj.time }}</div>   <button @click="handleChange">查看现在时间</button> </template> <script> import { reactive, watch } from 'vue' export default {   setup() {     let timer = null     const obj = reactive({       time: Date.now(),     })     watch(       () => {         return obj.time       },       (beforeData, afterData) => {         console.log(`beforeData`, beforeData)         console.log(`afterData`, afterData)       }     )     const handleChange = () => {       obj.time = Date.now()     }     return { obj, handleChange }   }, } </script> 复制代码

Untitled

四、生命周期钩子函数、Provide(提供) / Inject (注入)

1、Vue2.0 生命周期钩子函数回顾

image-20210816215148687

标记 1: new Vue() 初始化实例;

标记 2: 初始化事件和组件生命周期,此时会执行beforeCreate钩子函数,这是在组件创建之前执行的;

标记 3: 初始化注入和响应式,在这个时候,data 数据已经创建完成,此时会执行created钩子函数;

标记 4: 判断是否有el选项,如果有就进入到标记 5,如果没有,就通过 vm.$mount(el) 的形式去手动挂载(和 el 没有区别);

new Vue({   el: '#app', }) 复制代码

标记 5: 判断是否有template模板如果有 template选项,就会进入标记 6中;

new Vue({   el: '#app',   template: '<div>筱竹</div>', }) 复制代码

标记 6: 标记 5中如果有 template选项,将会把 template模板编译为 render 函数;

标记 7: 标记 5中如果没有template选项,就会获取 elouterHTML 作为模板进行编译;

标记 8: beforeMount 钩子函数被触发,将模板转化为 AST 树,再将 AST 树转成 render 函数,最后转化为虚拟 DOM 挂在到真实 DOM 节点上;

标记 9: 代表组件已经加载完了,在组件内部更新数据时候的生命周期,更新前和更新后各自触发的钩子函数;

标记 10: 代表组件被卸载,包括监听器也会被卸载,你可以在这里做一些组件销毁后的事情;

2、Vue3.0 生命周期钩子函数

2.1、Vue2.0 选项式 API 的生命周期钩子和 Vue3.0 组合式 API 之间写法对比

  • beforeCreate -> 使用 setup()

  • created -> 使用 setup()

  • beforeMount -> onBeforeMount

  • mounted -> onMounted

  • beforeUpdate -> onBeforeUpdate

  • updated -> onUpdated

  • beforeDestroy -> onBeforeUnmount

  • destroyed -> onUnmounted

  • errorCaptured -> onErrorCaptured

App.vue中写入 Vue3.0 生命周期钩子函数如下所示:

<template>   <div>     <h1>Vue3的生命周期{{ count }}</h1>     <div v-if="isShow">       <HelloWorld />     </div>   </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' import {   onBeforeMount,   onErrorCaptured,   onMounted,   onUnmounted,   onBeforeUpdate,   onUpdated,   onBeforeUnmount,   reactive,   toRefs, } from 'vue' export default {   components: {     HelloWorld,   },   setup() {     const data = reactive({       count: 0,       isShow: true,     })     setTimeout(() => {       data.count = 10       data.isShow = false     }, 2000)     onBeforeMount(() => {       console.log('onBeforeMount')     })     onMounted(() => {       console.log('onMounted')     })     onBeforeUpdate(() => {       console.log('onBeforeUpdate')     })     onUpdated(() => {       console.log('onUpdate')     })     onBeforeUnmount(() => {       console.log('onBeforeUnmount')     })     onUnmounted(() => {       console.log('onUnmounted')     })     onErrorCaptured(() => {       console.log('onErrorCaptured')     })     // let { count, isShow} = toRefs(data)     return {       ...toRefs(data), // 把data通过 toRefs 变成响应式的数据,并进行解构     }   }, } </script> <template>   <div>     <h1>Vue3的生命周期{{ count }}</h1>     <div v-if="isShow">       <HelloWorld />     </div>   </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' import {   onBeforeMount,   onErrorCaptured,   onMounted,   onUnmounted,   onBeforeUpdate,   onUpdated,   onBeforeUnmount,   reactive,   toRefs, } from 'vue' export default {   components: {     HelloWorld,   },   setup() {     const data = reactive({       count: 0,       isShow: true,     })     setTimeout(() => {       data.count = 10       data.isShow = false     }, 2000)     onBeforeMount(() => {       console.log('onBeforeMount')     })     onMounted(() => {       console.log('onMounted')     })     onBeforeUpdate(() => {       console.log('onBeforeUpdate')     })     onUpdated(() => {       console.log('onUpdate')     })     onBeforeUnmount(() => {       console.log('onBeforeUnmount')     })     onUnmounted(() => {       console.log('onUnmounted')     })     onErrorCaptured(() => {       console.log('onErrorCaptured')     })     // let { count, isShow} = toRefs(data)     return {       ...toRefs(data), // 把data通过 toRefs 变成响应式的数据,并进行解构     }   }, } </script> 复制代码

HelloWorld.vue子组件:

<template>   <div>page:子组件</div> </template> <script> import { onBeforeUnmount, onUnmounted } from 'vue' export default {   name: 'HelloWorld',   setup() {     onBeforeUnmount(() => {       console.log('子组件中的 onBeforeUnmount')     })     onUnmounted(() => {       console.log('子组件中的 onUnmounted')     })   }, } </script> 复制代码

以下是运行的结果:

image-20210923195643151

首先在页面渲染之前执行onBeforeMount,然后执行onMounted。当组件中有变量更新导致页面变化的时候,先执行onBeforUpadate,但是没有立即执行onUpdate,而是先执行了子组件中的onBeforeUnmountonUnmounted,这是因为子组件在父组件中渲染,在页面变化没有完全结束前,是不会执行父组件中的onUpdate生命周期钩子函数的。

2.2、Provide(提供) / Inject (注入)

常用的父子组件通信方式都是父组件绑定要传递给子组件的数据,子组件通过props属性接收,一旦组件层级变多时,采用这种方式一级一级传递值非常麻烦,而且代码可读性不高,不便后期维护。

vue提供了provideinject帮助我们解决多层次嵌套嵌套通信问题。在provide中提供要传递给子孙组件的数据,子孙组件通过inject注入祖父组件传递过来的数据。

如下场景所示:在一个祖先组件中引入和使用了个父组件,在父组件中引入和使用了一个儿子组件,当我们需要把祖先组件的数据传递给儿子组件时,我们就可以直接使用Provide(提供) / Inject (注入),不用再通过祖先组件传递给父组件,父组件再传递给儿子组件这样一层层的传递数据了,我们现在只需要在祖先组件中声明provide,然后在儿子组件中声明inject拿到数据即可。

image-20210923203612468

image-20210923210728509


Vue2.0 代码写法:

App.vue文件

<template>   <div>     <h1>provide和inject</h1>     <Father />   </div> </template> <script> import Father from './components/Father.vue' export default {   components: {     Father,   },   provide: {     name: '筱竹',     age: 18,   }, } </script> 复制代码

Father.vue文件

<template>   <h2>page:父组件</h2>   <Son /> </template> <script> import Son from './Son.vue' export default {   name: 'Father',   components: {     Son,   }, } </script> 复制代码

Son.vue文件

<template>   <div>     page:子组件,这是从祖先组件中传递过来的值:姓名:{{ name }} ,年龄:{{       age     }}   </div> </template> <script> export default {   name: 'Son',   inject: ['name', 'age'], } </script> 复制代码

浏览器中的效果如下所示:

image-20210923210351213Vue3.0 代码写法:

setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 来定义每个 property。

provide 函数允许你通过两个参数定义 property:

  1. name (<String> 类型)

  2. value

setup() 中使用 inject 时,也需要从 vue 显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。

inject 函数有两个参数:

  1. 要 inject 的 property 的 name

  2. 默认值 (可选)

App.vue文件

<template>   <div>     <h1>provide和inject</h1>     <Father />   </div> </template> <script> import { provide } from 'vue' import Father from './components/Father.vue' export default {   components: {     Father,   },   setup() {     provide('name', '筱竹') // 单个提供的形式     provide('status', {       age: 18,       hobby: '工作',     }) // 多个提供的形式   }, } </script> <template>   <div>     <h1>provide和inject</h1>     <Father />   </div> </template> <script> import { provide } from 'vue' import Father from './components/Father.vue' export default {   components: {     Father,   },   setup() {     provide('name', '筱竹') // 单个提供的形式     provide('status', {       age: 18,       hobby: '工作',     }) // 多个提供的形式   }, } </script> 复制代码

Father.vue文件

<template>   <h2>page:父组件</h2>   <Son /> </template> <script> import Son from './Son.vue' export default {   name: 'Father',   components: {     Son,   }, } </script> 复制代码

Son.vue文件

<template>   <div>     page:子组件,这是从祖先组件中传递过来的值:姓名:{{ name }} ,年龄:{{       age     }}     爱好:{{ hobby }}   </div> </template> <script> import { inject, toRefs } from 'vue' export default {   name: 'Son',   setup() {     const name = inject('name', 'xiaozhu')     const status = inject('status')     return {       name,       ...toRefs(status),     }   }, } </script> 复制代码

浏览器中的效果如下所示:

image-20210923212215435

**通过阅读 Vue3 的官网得知,**当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。这句话的意思是建议我们在声明provide 的组件中去修改数据,根据上面的例子,所以我们应该在App.vue中去修改数据,而不是Son.vue组件中去修改数据。

App.vue文件

<template>   <div>     <h1>provide和inject</h1>     <Father />   </div> </template> <script> import { provide, reactive, ref } from 'vue' import Father from './components/Father.vue' export default {   components: {     Father,   },   setup() {     const name = ref('筱竹')     const status = reactive({       age: 18,       hobby: '工作',     })     provide('name', name)     provide('status', status)     const changeStates = () => {       name.value = 'xiaozhu'       status.age = 19       console.log(status.age)     }     provide('changeStates', changeStates)   }, } </script> 复制代码

Father.vue文件

<template>   <h2>page:父组件</h2>   <Son /> </template> <script> import Son from './Son.vue' export default {   name: 'Father',   components: {     Son,   }, } </script> 复制代码

Son.vue文件

<template>   <div>     page:子组件,这是从祖先组件中传递过来的值:姓名:{{ name }} ,年龄:{{       age     }}     爱好:{{ hobby }}   </div>   <button @click="changeStates">修改信息</button> </template> <script> import { inject, toRefs } from 'vue' export default {   name: 'Son',   setup() {     const name = inject('name', 'xiaozhu')     const status = inject('status')     const changeStates = inject('changeStates')     return {       name,       ...toRefs(status),       changeStates,     }   }, } </script> 复制代码

change

最后,为了确保通过 provide 传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly

说明:在 Son.vue 组件中,是可以直接修改 inject 传进来的 值,虽然可以直接修改 inject 传进来的值,但非常不建议这样去操作,因为数据本身就在App.vue文件声明的,结果在其它(Son.vue)组件中被修改了,就会造成数据紊乱,没有规则,页面组件变得难以维护,所以一定要控制好数据,保证单一的数据流。

App.vue文件

<template>   <div>     <h1>provide和inject</h1>     <Father />   </div> </template> <script> import { provide, reactive, ref, readonly } from 'vue' import Father from './components/Father.vue' export default {   components: {     Father,   },   setup() {     const name = ref('筱竹')     const status = reactive({       age: 18,       hobby: '工作',     })     provide('name', readonly(name))     provide('status', readonly(status))     const changeStates = () => {       name.value = 'xiaozhu'       status.age = 19       console.log(status.age)     }     provide('changeStates', changeStates)   }, } </script> 复制代码

五、Vue-Router

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。

通过 npm install vue-router@next 下载最新的路由插件

或者通过 npm install vue-router@4下载当前最新的路由插件

以上两个安装方法都可以安装目前最新的 vue-router4.X 版本的路由

1、router-link 和 router-view 组件

src目录下新建viewsrouter目录,然后在views目录中新建Home.vueAbout.vue文件,在router目录中新建index.js文件。

Home.vue文件

<template>   <h2>     <router-link to="/about">Home Page</router-link>   </h2> </template> <script> export default {   name: 'Home', } </script> <style scope></style> 复制代码

About.vue文件

<template>   <h2>About page</h2> </template> <script> export default {   name: 'About', } </script> <style scope></style> 复制代码

App.vue文件

<template>   <div>     <router-view></router-view>   </div> </template> 复制代码

router/index.js文件

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) import Home from '../views/Home.vue' import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     path: '/',     component: Home,   },   {     path: '/about',     component: About,   }, ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({   history: createWebHashHistory(),   routes, }) // 导出路由实例 export default router 复制代码

mian.js文件

import { createApp } from 'vue' import App from './App.vue' import './index.css' // 导入路由文件 import router from '../src/router' // 创建 App实例 const app = createApp(App) // 让整个App应用支持路由 app.use(router) // 将实例挂载到 #app节点上 app.mount('#app') 复制代码

效果如下所示:

1

注意上图,浏览器并没有因为点击跳转而刷新页面,这就是路由带来的单页面切换组件能力,在不刷新页面的情况下,改变可视区域的组件。

之所以路由能够不刷新页面,能够带来可视区域不同内容展示,原因如下图所示:

image-20211013221349958

以下通过一张图来展示通过前端路由实现单页面应用无刷新页面的过程。

image-20211013223158597

如我们将 router-link 换成普通的 a 标签 href 跳转,也能实现页面组件之间的切换,但是这样就会导致浏览器页面的刷新,这并不是我们的初衷。

2、编程式导航

2.1、无参数形式

Home.vue文件修改如下,其它文件保持不变。

<template>   <div>     <h2>Home Page</h2>     <button @click="linkTo">to About Page</button>   </div> </template> <script> // 因为我们在 setup 里面没有 this,所以我们不能再直接访问 this.$router 或 this.$route。作为替代,需要使用 useRouter 函数 import { useRouter } from 'vue-router' export default {   name: 'Home',   setup() {     const router = useRouter()     console.log('router', router) // 打印 router 查看内部的一些属性方法     const linkTo = () => {       // 字符串路径       // router.push('/about')       // 带有路径的对象       router.push({ path: '/about' })     }     return {       linkTo,     }   }, } </script> <style scope></style> 复制代码

通过 useRouter 生成路由实例 router,其内部都是路由相关的方法,如跳转方法、路由守卫、返回方法等等。可以通过打印 router 查看内部的一些属性方法,如下图所示;

image-20211015204508032

2.2、命名路由 + params(带参数) 形式

有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。

router/index.js文件修改如下:

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) import Home from '../views/Home.vue' import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     name: 'home',     path: '/',     component: Home,   },   {     name: 'about',     path: '/about',     component: About,   }, ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({   history: createWebHashHistory(),   routes, }) export default router 复制代码

Home.vue文件修改如下

<template>   <div>     <h2>Home Page</h2>     <button @click="linkTo">to About Page</button>   </div> </template> <script> // 因为我们在 setup 里面没有 this,所以我们不能再直接访问 this.$router 或 this.$route。作为替代,需要使用 useRouter 函数 import { useRouter } from 'vue-router' export default {   name: 'Home',   setup() {     const router = useRouter()     // console.log('router',router);     const linkTo = () => {       // 字符串路径       // router.push('/about')       // 带有路径的对象       // router.push({path:'/about'})       // 命名路由,并加上参数       router.push({ name: 'about', params: { id: 123 } })     }     return {       linkTo,     }   }, } </script> <style scope></style> 复制代码

注意params 不能与 path 一起使用,如果提供了 pathparams 会被忽略。

// `params` 不能与 `path` 一起使用 const my = 'my' router.push({ path: '/about', params: { my } }) // 能够跳转到 about 页面, 不能跳转到 my 页面 复制代码

About.vue文件接收Home.vue文件传入的params 修改如下:

<template>   <h2>About page</h2> </template> <script> // 因为我们在 setup 里面没有 this,所以我们不能再直接访问 this.$route。作为替代,需要使用 useRoute 函数来获取参数 import { useRoute } from 'vue-router' export default {   name: 'About',   setup() {     const route = useRoute()     const { id } = route.params     console.log('id', id) // 123   }, } </script> <style scope></style> 复制代码

1

值得注意的是,如果使用 params 的形式传参,当在 About Page 页面再次手动刷新时,就获取不到 params 参数,如下所示:

image-20211015214851295

解决办法 1:params 传参时在路由定义里设置 url 参数

router/index.js文件修改如下:

// …… // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     name: 'home',     path: '/',     component: Home,   },   {     name: 'about',     path: '/about/:id', // 这设置params     component: About,   }, ] // …… 复制代码

此时当在 About Page 页面再次刷新浏览器,依然能够在 获取到params 参数,同时浏览器地址栏中会有http://localhost:3000/#/about/123

3

解决办法 2:使用 query(带查询参数)形式 ( 详见如下 3、 query(带查询参数)形式)

2.3、 query(带查询参数)形式

Home.vue文件修改如下

<template>   <div>     <h2>Home Page</h2>     <button @click="linkTo">to About Page</button>   </div> </template> <script> // 因为我们在 setup 里面没有 this,所以我们不能再直接访问 this.$router 或 this.$route。作为替代,需要使用 useRouter 函数 import { useRouter } from 'vue-router' export default {   name: 'Home',   setup() {     const router = useRouter()     // console.log('router',router);     const linkTo = () => {       // 字符串路径       // router.push('/about')       // 带有路径的对象       // router.push({path:'/about'})       // 命名的路由,并加上参数,       // router.push({ name: 'about', params: { id: 123 } })       // 带查询参数,结果是 /about?id=123       router.push({ path: '/about', query: { id: 123 } })     }     return {       linkTo,     }   }, } </script> <style scope></style> 复制代码

About.vue文件接收Home.vue文件传入的params 修改如下:

<template>   <h2>About page</h2> </template> <script> // 因为我们在 setup 里面没有 this,所以我们不能再直接访问 this.$route。作为替代,需要使用 useRoute 函数来获取参数 import { useRoute } from 'vue-router' export default {   name: 'About',   setup() {     const route = useRoute()     const { id } = route.query     console.log('id', id) // 123   }, } </script> <style scope></style> 复制代码

router/index.js文件修改如下:

// …… // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     name: 'home',     path: '/',     component: Home,   },   {     name: 'about',     path: '/about', // 这里记得去掉/:id     component: About,   }, ] // …… 复制代码

此时当在 About Page 页面再次刷新浏览器,依然能够在 获取到query 参数,同时浏览器地址栏中会有http://localhost:3000/#/about?id=123

2

2.4、 replace(替换当前位置)形式

Home.vue文件修改如下:

<template>   <div>     <h2>Home Page</h2>     <button @click="linkTo">to About Page</button>   </div> </template> <script> // 因为我们在 setup 里面没有 this,所以我们不能再直接访问 this.$router 或 this.$route。作为替代,需要使用 useRouter 函数 import { useRouter } from 'vue-router' export default {   name: 'Home',   setup() {     const router = useRouter()     // console.log('router',router);     const linkTo = () => {       // 字符串路径       // router.push('/about')       // 带有路径的对象       // router.push({path:'/about'})       // 命名的路由,并加上参数,       // router.push({ name: 'about', params: { id: 123 } })       // 带查询参数,结果是 /about?id=123       // router.push({ path: '/about', query: { id: 123 } })       // replace,它的作用类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录       // router.replace({ path: '/about' })       // 上面写法也可以改成以下写法       router.push({ path: '/about', replace: true })     }     return {       linkTo,     }   }, } </script> <style scope></style> 复制代码

About.vue文件修改如下:

<template>   <h2>About page</h2> </template> <script> export default {   name: 'About', } </script> <style scope></style> 复制代码

4

这里说下 vue-router 中的router.pushrouter.replace的区别:

router.push

描述:跳转到不同的 url,但这个方法会向 history 栈添加一个记录,点击后退会返回到上一个页面。

router.replace

描述:同样是跳转到指定的 url,但是这个方法不会向 history 里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。

2.5、路由懒加载 + 嵌套路由

为了避免我们今后打包后的 Vue 项目一个 js 文件打包过大,影响页面的加载速度,我们可以把不同路由对应的最贱分割成不同的代码块,然后只有当路由被访问到的时候才加载对应组件,这样就会减少打包体积和页面加载速度,从而更加高效。

之前router/index.js文件中的路由写法并不是路由懒加载写法,以下的路由对应的组件,不管在路由是否被访问到,路由所对应的组件都会先加载,影响页面访问速度和打包体积。

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) import Home from '../views/Home.vue' import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     name: 'home',     path: '/',     component: Home,   },   {     name: 'about',     path: '/about',     component: About,   }, ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({   history: createWebHashHistory(),   routes, }) export default router 复制代码

以上修改router/index.js为路由懒加载写法

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) - import Home from '../views/Home.vue' - import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [{ name: 'home', path: '/', + component: () => import('../views/Home.vue') }, { name: 'about', path: '/about', + component: () => import('../views/About.vue') } ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({ history: createWebHashHistory(), routes, }) export default router; 复制代码

有些时候,我们需要在页面中通过触发某个事件在当前页面另外一个组件,这时候,我们可以使用嵌套路由来实现,嵌套路由写法如下:

router/index.js

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) // import Home from '../views/Home.vue' // import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     name: 'home',     path: '/',     component: () => import('../views/Home.vue'),   },   {     name: 'about',     path: '/about',     component: () => import('../views/About.vue'),     children: [       {         name: 'type',         path: 'type',         component: () => import('../views/Type.vue'),       },     ],   }, ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({   history: createWebHashHistory(),   routes, }) export default router 复制代码

views目录中新建Type.vue文件,写法如下所示:

<template>   <div>     <h2>Type Page</h2>     <p>我是About页面的子页面</p>   </div> </template> <script> import { useRoute } from 'vue-router' export default {   name: 'Type',   setup() {     const route = useRoute()     const id = route.query     console.log(id)   }, } </script> <style scope></style> 复制代码

About.vue文件修改如下:

<template>   <div>     <h2>About page</h2>     <button @click="linkShow">展示 type组件</button>     <router-view></router-view>   </div> </template> <script> import { useRouter } from 'vue-router' export default {   name: 'About',   setup() {     const router = useRouter()     const linkShow = () => {       router.push({ path: '/about/type', query: { id: 955 } })     }     return { linkShow }   }, } </script> <style scope></style> 复制代码

效果如下:

5

2.6、重定向和别名

重定向

“重定向”的意思是,比如:当用户访问 /index时,URL 将会被替换成 /,然后匹配路由为 /

router/index.js文件中 routes 中,添加 redirect属性,重定向的写法有三种形式,如下所示:

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) // import Home from '../views/Home.vue' // import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     path: '/index',     // 写法 1     // redirect: '/'     // 写法 2     // redirect: {     //  name: 'home'     // }     // 写法 3     redirect: (to) => {       return {         path: '/',       }     },   },   {     name: 'home',     path: '/',     component: () => import('../views/Home.vue'),   },   {     name: 'about',     path: '/about',     component: () => import('../views/About.vue'),     children: [       {         name: 'type',         path: 'type',         component: () => import('../views/Type.vue'),       },     ],   }, ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({   history: createWebHashHistory(),   routes, }) export default router 复制代码

效果如下所示:

6

别名

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

import {   createRouter,   createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) // import Home from '../views/Home.vue' // import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [{         path: '/index',         // 写法 1         // redirect: '/'         // 写法 2         // redirect: {         //  name: 'home'         // }         // 写法 3         redirect: to => {                 return {                         path: '/'                 }         }     }, {         name: 'home',         path: '/', +       alias: '/home',         component: () => import('../views/Home.vue')     },     {         name: 'about',         path: '/about',         component: () => import('../views/About.vue'),         children: [{                 name: 'type',                 path: 'type',                 component: () => import('../views/Type.vue')         }]     } ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({     history: createWebHashHistory(),     routes, }) export default router; 复制代码

效果如下:

7

3、导航守卫

很多时候你需要监听路由的变化,从而来实现一个业务逻辑,比如:

1、跳转到一个网站的页面时,要判断该用户是否登录了,如果没有登录,就跳转到登录页面,如果已经登录了,就可以跳转到网站的其它页面。

2、在做后台管理系统权限控制的时候,如果用户没有权限访问一个页面,那么就会做一些相应的逻辑处理。

以上这些都是通过导航守卫可以实现的。

3.1、全局守卫(路由独享守卫)

beforeEachafterEach 方法接收一个回调函数,回调函数内可以接收三个参数**tofromnext**,

  • to: 即将要进入的目标;

  • from: 当前导航正要离开的路由

  • next: 要做的一些事情,执行效果依赖 next 方法的调用参数:

    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。

    • next(error): 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError()注册过的回调。

所以通过以上参数,可以监听到路由的变化,我们通过修改 App.vue 如下所示:

<template>   <div>     <router-view></router-view>   </div> </template> <script> import { useRouter } from 'vue-router' export default {   name: 'App',   setup() {     const router = useRouter()     const HAS_LOGIN = false // 登录标识,这里一般调用登录接口     // router.beforeEach:全局前置守卫     router.beforeEach((to, from, next) => {       console.log('to', to)       console.log('from', from)       //  to: 到哪里去,即将要进入的目标 路由对象       // from 从哪里来,当前导航正要离开的路由       // next 继续做一些事情       // 如果跳转的到的页面不是登录页面       if (to.name !== 'login') {         // 如果已经登录,就继续执行         if (HAS_LOGIN) {           next()         } else {           // 如果没有登录,跳转到登录页面           next({ name: 'login' })         }         // 如果跳转的是登录页面       } else {         // 如果已经登录,就跳转到首页         if (HAS_LOGIN) {           next({ name: 'home' })         } else {           // 如果没有登录,就继续执行           next()         }       }     })     // router.afterEach:全局后置钩子     router.afterEach((to, from) => {       // loading = false // 让loading  取消     })   }, } </script> 复制代码

router/index.js文件中添加login路由配置

import { createRouter, createWebHashHistory } from 'vue-router' // 1、定义路由组件(以下是从其他文件导入的路由组件) // import Home from '../views/Home.vue' // import About from '../views/About.vue' // 2、定义路由,让路由映射到对应的组件中 const routes = [   {     path: '/index',     // 写法 1     // redirect: '/'     // 写法 2     // redirect: {     //  name: 'home'     // }     // 写法 3     redirect: (to) => {       return {         path: '/',       }     },   },   {     name: 'home',     path: '/',     alias: '/home',     component: () => import('../views/Home.vue'),   },   {     name: 'about',     path: '/about',     component: () => import('../views/About.vue'),     children: [       {         name: 'type',         path: 'type',         component: () => import('../views/Type.vue'),       },     ],   },   {     name: 'login',     path: '/login',     component: () => import('../views/Login.vue'),   }, ] // 3、创建路由实例,把定义的路由挂载到路由实例中 const router = createRouter({   history: createWebHashHistory(),   routes, }) export default router 复制代码

views目录中添加 Login.vue文件,如下所示:

<template>   <div>     <h2>Login Page</h2>   </div> </template> <script> export default {} </script> <style scope></style> 复制代码

效果如下所示:

8

补充:在 beforeEach 和 afterEach 方法的回调函数中,回调函数内也可以通过 router.currentRoute 拿到当前的路径参数。

App.vue

<template>   <div>     <router-view></router-view>   </div> </template> <script> import { useRouter } from 'vue-router' export default {   name: 'App',   setup() {     const router = useRouter()     const HAS_LOGIN = false // 登录标识,这里一般调用登录接口     // router.beforeEach:全局前置守卫     router.beforeEach((to, from, next) => {       // console.log('to', to)       // console.log('from', from) + console.log(router.currentRoute)       //  to: 到哪里去,即将要进入的目标 路由对象       // from 从哪里来,当前导航正要离开的路由       // next 继续做一些事情        // ………     })     // ………   }, } </script> 复制代码

console.log(router.currentRoute)在浏览器控制台打印如下所示:

image-20211017153628592

3.2、组件内守卫

我们也可以在路由组件内直接定义路由导航守卫,需要注意的是,beforeRouteEnter目前在Vue Router 4.x版本中不在支持,setup 函数里面可以使用 onBeforeRouteLeave 和 onBeforeRouteUpdate 两个新增的 API 代替。

先把 App.vue 中的登录标识修改为true,方便后续调试

const HAS_LOGIN = true // 登录标识,这里一般调用登录接口 复制代码

About.vue文件修改如下:

<template>   <div>     <h2>About page</h2>     <div>{{ userData }}</div>     <button @click="linkShow">展示 type组件</button>     <router-view></router-view>   </div> </template> <script> import { useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' import { ref } from 'vue' export default {   name: 'About',   setup() {     const userData = ref(0)     const router = useRouter()     const linkShow = () => {       router.push({ path: '/about/type', query: { id: 955 } })     }     // 与 beforeRouteLeave 相同,无法访问 `this`     onBeforeRouteUpdate((to, from) => {       userData.value = to.query.id     })     // 与 beforeRouteLeave 相同,无法访问 `this`     onBeforeRouteLeave((to, from) => {       const answer = window.confirm('你确认要离开吗?当前还没有保存信息!')       // 取消导航并停留在同一页面上       if (!answer) return false     })     return {       linkShow,       userData,     }   }, } </script> <style scope></style> 复制代码

效果如下:

9


作者:筱竹
链接:https://juejin.cn/post/7020067926686302222


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐