vue3路由的使用
一、路由的概要
1.1、什么是路由?
-
路由就是一组映射关系,根据不同的 url 地址展示不同的内容或页面(key-value);
-
key为路径,value可能是function或component
-
-
路由 ( 英文: router ) 就是对应关系
- 通俗易懂的概念:Hash 地址与组件之间的对应关系
-
SPA与前端路由
-
SPA指的是一个web 网站只有唯一的一个HTML页面,所有组件的展示与切换都在这唯一的一个页面内完成。
-
此时,不同组件之间的切换需要通过前端路由来实现
-
1.2、SPA应用:
-
单页Web应用(single page web application,SPA)
-
整个应用只有一个完整的页面
-
点击页面中的导航链接不会刷新页面,只会在页面局部更新
-
数据需要通过ajax请求获取
1.3、路由的分类
1.3.1、后端路由
理解: value是function,用于处理客户提交的请求
工作过程: 浏览器在地址栏切换url时,会向服务器发送请求,服务器响应后,会根据请求路径找到匹配的函数来处理请求,返回响应数据(页面)。
优点: 分担了前端的压力,html和数据的拼接都是由服务器完成。
缺点: 浏览器会刷新页面,且前后端不分离,当项目十分庞大时,加大了服务器端的压力,同时在浏览器端不能输入制定的url路径进行指定模块的访问。另外一个就是如果当前网速过慢,那将会延迟页面的加载,对用户体验不是很友好。
1.3.2、前端路由
理解: value是component,用于展示页面内容
工作过程: 当浏览器的路径改变时,对应的组件就会显示
优点:
-
用户体验好,和后台网速没有关系,不需要每次都从服务器全部获取,快速展现给用户
-
可以再浏览器中输入指定想要访问的url路径地址
-
实现了前后端的分离,方便开发。有很多框架都带有路由功能模块。
缺点:
-
使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存
-
单页面无法记住之前滚动的位置,无法在前进、后退的时候记住滚动的位置
路由操作:
-
① 用户点击了页面上的路由链接
-
② 导致了 URL 地址栏中的 Hash 值发生了变化
-
③ 前端路由监听了到 Hash 地址的变化
-
④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中
结论:前端路由,指的是 Hash 地址与组件之间的对应关系!
1.4、模拟路由
1.4.1、定义三个子组件
子组件A:
<template lang=""> <div class="cls1"> <h2>这是首页 - home</h2> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1{ width: 500px; height: 350px; border: 2px solid #666; border-radius: 20px; padding: 15px; margin: 10px; background-color: blueviolet; } </style>
子组件B:
<template lang=""> <div class="cls1"> <h2>这是电影频道 - movie</h2> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1{ width: 500px; height: 350px; border: 2px solid #666; border-radius: 20px; padding: 15px; margin: 10px; background-color: #dfe; } </style>
子组件C:
<template lang=""> <div class="cls1"> <h2>这是关于我们页面 - about</h2> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1{ width: 500px; height: 350px; border: 2px solid #666; border-radius: 20px; padding: 15px; margin: 10px; background-color: #fde; } </style>
父组件:
<template> <div class="main"> <nav> <a href="#/Home">首页</a> | <a href="#/Movie">电影</a> | <a href="#/About">关于</a> </nav> <section> <KeepAlive> <component :is="coms[comName]"></component> </KeepAlive> </section> </div> </template> <script lang="ts" setup> import Home from './components/home.vue'; import Movie from './components/movie.vue'; import About from './components/about.vue'; import { ref, KeepAlive, onMounted } from 'vue'; let coms={ Home, Movie, About }; let comName = ref("Home"); // 当页面挂载成功时的钩子 onMounted(() => { // 当hash值(#号后面的/就是hash值)发生变化的事件 window.addEventListener("hashchange",event=>{ // 获取路径名称,把#/home从第二位开始拿 let path = location.hash.substring(2); // 更换组件名称 comName.value = path; },false) }) </script> <style> .main a{ color: #00f; text-decoration: none; font-size: 16px; } .main a:hover{ color: orangered; } .main nav{ border-bottom: 2px solid #999; height: 46px; line-height: 46px; } </style>
注意:如果 :is="coms[comName]" 报错了,就在 tsconfig.json 文件中的 "sourceMap": true 下面添加 "noImplicitAny": false 意思是:当 noImplicitAny 标志是 false (默认值) 时, 如果编译器无法根据变量的用途推断出变量的类型,它就会悄悄的把变量类型默认为 any
1.5、vue-router 使用方法:
1.5.1、安装 vue-router 包
npm i vue-router@next // @next:下载最新版的
1.5.2、定义三个子组件
子组件A:
<template lang=""> <div class="cls1"> <h2>这是首页 - home</h2> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1 { background-color: blueviolet; min-height: 500px; } .cls1 h2 { padding: 0; margin: 0; } </style>
子组件B:
<template lang=""> <div class="cls1"> <h2>这是电影频道 - movie</h2> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1 { min-height: 500px; background-color: #dfe; } .cls1 h2 { padding: 0; margin: 0; } </style>
子组件C:
<template lang=""> <div class="cls1"> <h2>这是关于我们页面 - about</h2> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1 { background-color: #fde; min-height: 500px; } .cls1 h2{ padding: 0; margin: 0; } </style>
1.5.3、在 src 源代码目录下,新建router / index.ts 路由模块,并初始化如下的代码:
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; // 自定义的组件文件 import Home from "../views/Home.vue"; import About from "../views/About.vue"; import Movie from "../views/Movie.vue"; // routes 是一个数组,作用:定义 'hash 地址' 与 '组件之间的对应关系' let routes: RouteRecordRaw[] = [ { path: "/", component: Home }, { path: "/movie", component: Movie }, { path: "/about", component: About }, ]; // 创建路由对象 let router = createRouter({ history: createWebHashHistory(), // 指定路由模式为hash模式(兼容性好,但带#)
routes, });
// 向外共享路由的实例对象
export default router;
1.5.4、在 src / main.ts 入口文件中,导入并挂载路由模块
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index' // 导入路由规则
let app = createApp(App);
app.use(router); // 挂载路由中间件
app.mount('#app');
1.5.5、声明路由链接和占位符,使用路由
父组件(App.vue文件)
<template> <div class="main"> <nav> <!-- 当安装了vue-router后,可以使用 router-link 来替代普通的a链接 --> <router-link to="/home">首页</router-link> | <router-link to="/movie">电影</router-link> | <router-link to="/about">关于</router-link> | </nav> <section> <!-- 只要在项目中安装和配置了 vue-router,就可以使用 router-view 这个组件 --> <!-- 它的作用很单纯:占位符,相当于一个容器 --> <router-view></router-view> </section> </div> </template> <script lang="ts" setup> </script> <style> .main a { color: #00f; text-decoration: none; font-size: 16px; } .main a:hover { color: orangered; } .main nav { border-bottom: 2px solid #999; height: 46px; line-height: 46px; } section{ min-height: 500px; margin: 0; } </style>
1.6、路由 - 重定向
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。
通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; // 自定义的组件文件 import Home from "../components/home.vue"; import About from "../components/about.vue"; import Movie from "../components/movie.vue"; // routes 是一个数组,作用:定义 'hash 地址' 与 '组件之间的对应关系' let routes: RouteRecordRaw[] = [ { path: "/", component: Home }, { path: "/movie", component: Movie }, { path: "/about", component: About }, { path: "/film", redirect:"/movie" }, // redirect:重定向 ]; // 创建路由对象 let router = createRouter({ history: createWebHashHistory(), // 指定路由模式为hash模式(兼容性好,但带#) routes, }); // 向外共享路由的实例对象 export default router;
App组件:
<template> <div class="main"> <nav> <!-- 当安装了vue-router后,可以使用 router-link 来替代普通的a链接 --> <router-link to="/home">首页</router-link> | <router-link to="/movie">电影</router-link> | <router-link to="/about">关于</router-link> | <router-link to="/film">影视</router-link> </nav> <section> <!-- 只要在项目中安装和配置了 vue-router,就可以使用 router-view 这个组件 --> <!-- 它的作用很单纯:占位符,相当于一个容器 --> <router-view></router-view> </section> </div> </template> <script lang="ts" setup> </script> <style> .main a { color: #00f; text-decoration: none; font-size: 16px; } .main a:hover { color: orangered; } .main nav { border-bottom: 2px solid #999; height: 46px; line-height: 46px; } section{ min-height: 500px; margin: 0; } </style>
1.7、嵌套路由
1.7.1、概要
概念:通过路由实现组件的嵌套展示,叫做嵌套路由。嵌套路由也称之为子路由,就是在被切换的组件中又切换其他子组件。我认为嵌套路由就是:子组件里面又包含着组件(孙子组件)
例如:
点击父级路由链接显示模板内容:
① 模板内容中又有子级路由链接
② 点击子级路由链接显示子级模板内容
1.7.2、定义两个孙组件
孙组件A:components/culture:
<template lang=""> <div class="cls1"> 企业环境 企业环境是指企业的性质、企业的经营方向、外部环境、企业的社会形象、与外界的联系等方面。它往往决定企业的行为。 价值观 价值观是指企业内成员对某个事件或某种行为好与坏、善与恶、正确与错误、是否值得仿效的一致认识。价值观是企业文化的核心,统一的价值观使企业内成员在判断自己行为时具有统一的标准,并以此来决定自己的行为。 英雄人物 英雄人物是指企业文化的核心人物或企业文化的人格化,其作用在于作为一种活的样板,给企业中其他员工提供可供学习的榜样,对企业文化的形成和强化起着极为重要的作用。 文化仪式 文化仪式是指企业内的各种表彰、奖励活动、聚会以及文娱活动等,它可以把企业中发生的某些事情戏剧化和形象化,来生动地宣传和体现企业的价值观,使人们通过这些生动活泼的活动来领会企业文化的内涵,使企业文化“寓教于乐”之中。 文化网络 文化网络是指非正式的信息传递渠道,主要是传播文化信息。它是由某种非正式的组织和人群所组成,它所传递出的信息往往能反映出职工的愿望和心态。 </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1 { background-color: #fde; min-height: 500px; padding: 20px; margin: 20px; } </style>
孙组件B:components/introduce:
<template lang=""> <div class="cls1"> 企业简介是指通过文字和图片资料向社会公众介绍企业基本情况和经营战略的文案。主要介绍企业名称、企业的法律性质、开办地址、设立时间、所属行业、注册资金、建筑面积、技术装备条件;生产经营范围、主要产品结构;企业发展战略、经营理念等情况。 </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1 { background-color: #fde; min-height: 500px; padding: 20px; margin: 20px; } </style>
1.7.3、配置路由规则,使用 children 配置项:router / index.ts文件
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; // 自定义的组件文件 import Home from "../components/home.vue"; import About from "../components/about.vue"; import Movie from "../components/movie.vue"; import Culture from "../components/culture.vue"; import Introduce from "../components/introduce.vue"; // routes 是一个数组,作用:定义 'hash 地址' 与 '组件之间的对应关系' let routes: RouteRecordRaw[] = [ { path: "/", component: Home }, { path: "/home", component: Home }, { path: "/movie", component: Movie }, { path: "/about", component: About, redirect:"/about/culture", // 重定向,默认路由 默认页面 children:[ // 子路由,不用以 / 开始 {path:"culture",component: Culture}, {path:"introduce",component: Introduce} ] }, { path: "/film", redirect:"/movie" }, // redirect:重定向 ]; // 创建路由对象 let router = createRouter({ history: createWebHashHistory(), // 指定路由模式为hash模式(兼容性好,但带#) routes, }); // 向外共享路由的实例对象 export default router;
1.7.4、about 子组件文件,跳转(要写完整路径)
<template lang=""> <div class="cls1"> <h2>这是关于我们页面 - about</h2> <nav> <router-link to="/about/culture">企业文化</router-link> | <router-link to="/about/introduce">企业介绍</router-link> </nav> <router-view></router-view> </div> </template> <script lang="ts" setup> </script> <style scoped> .cls1 { background-color: #fde; min-height: 500px; } .cls1 h2{ padding: 0; margin: 0; } nav{ margin: 10px 10px 10px 30px; } </style>
1.7.5、可以在App.vue直接调用 about 子组件文件
<template> <div class="main"> <nav> <router-link to="/about">关于</router-link> </nav> <section> <router-view></router-view> </section> </div> </template> <script lang="ts" setup> </script> <style> .main a { color: #00f; text-decoration: none; font-size: 16px; } .main a:hover { color: orangered; } .main nav { border-bottom: 2px solid #999; height: 46px; line-height: 46px; } section{ min-height: 500px; margin: 0; } </style>
1.8、带参数的动态路由匹配
获取路径参数的三种方法: param、query 与 hash
1.8.1、通过 param 获取(路由传参,用于带 / 号的)
1、传递参数,在路径后面再加一个/:id ,通过在地址栏输入完整路径(xxx/movie/任意一个数),在页面中显示该值出来
{ path: "/movie/:id", component: Movie }, //:xxx是占位符,用来声明接收params参数
2.接收参数(App文件中)
<div class="cls1"> <h2>这是电影频道 - movie</h2> <h3>当前电影编号:{{ $route.params.id }}</h3> </div>
3. 如图所示:
1.8.2、通过 query获取(用于带 ?号的)
1.8.2.1、直接在页面上传递参数的方式
1. 直接在页面中,接收参数
<h4>取出查询(query,问号后的参数)价钱:{{ $route.query.money }},姓名:{{ $route.query.name }}</h4>
2. 如图所示
1.8.2.2、在父组件传值给子组件的方式
1. 父组件
<!-- 本组件用于显示所有的图书列表 --> <template lang=""> <div class="main" v-for="(item,i) in list.hot" @click="clicklist(item)"> // 通过点击事件,把该行的参数传递过去 <div class="main_left"> <img :src="item.tp" alt="" > </div> <div class="main_right" > <p style="font-size: 18px;">{{item.name}}</p> <p style="color: #aaa;font-size: 14px;">{{item.zz}}</p> <p style="color: red;font-size: 16px;">¥ {{item.price}}</p> </div> </div> </template> <script lang="ts"> // 导入路由文件 import { useRouter } from 'vue-router' import { reactive, toRefs } from 'vue'; export default { setup() { const s: any = reactive({ list: [], list2: [] }); s.list.hot = [ { name: "web 前端开发", zz: "南方IT学院", price: "62.4", tp: "https://ts1.cn.mm.bing.net/th/id/R-C.5c5ec1769389901eac83e9cf75a5bd33?rik=GXnLovzVPOcEUg&riu=http%3a%2f%2fwww.tup.tsinghua.edu.cn%2fupload%2fbigbookimg3%2f080900-01.jpg&ehk=4efizNnDT%2fRsmCZVq5H2UkONQ%2bBT2oOu6%2br%2bP511Lv0%3d&risl=&pid=ImgRaw&r=0" }, { name: "JavaScript语言精粹", zz: "清华大学出版社", price: "88.9", tp: "https://pic2.zhimg.com/v2-a4265bf1750016b9a83abdc60a8471c7_b.jpg" }, { name: "HTML5权威指南", zz: "北京大学出版社", price: "108.0", tp: "https://codesheep.oss-cn-hangzhou.aliyuncs.com/blog/20200916233828.png" }, { name: "ES6标准入门", zz: "清华大学出版社", price: "35.8", tp: "https://pic2.zhimg.com/v2-17c68027e62866178d5159374318ad7d_b.jpg" }, { name: "图解css3", zz: "南方it学院", price: "11.2", tp: "https://pic2.zhimg.com/v2-64c56a356fa4777037ffd15233417585_b.jpg" }, { name: "node.js", zz: "人民大学出版社", price: "77.9", tp: "https://ts1.cn.mm.bing.net/th/id/R-C.a6bfd782f3aa77ca473a208514a65e18?rik=r07N6f2SwTOl1w&riu=http%3a%2f%2fwww.fairysoftware.com%2fad_images%2fqian_duan_kai_fa_06.jpg&ehk=6nr%2fdlTyGl4EtlcAJu3KDeRyRLAH9VvZC8Ae58EB1y0%3d&risl=&pid=ImgRaw&r=0" }, ] // 路由跳转 const $router = useRouter(); // 该方法实现点击每一个图书时,跳转到该图书的详情页上,通过query传递点击的图书信息参数过去 function clicklist(item:any) { // path是路由路径 $router.push({ path:'info', query:item }) console.log(item); } // 暴露出去页面上 return { ...toRefs(s),clicklist } } } </script> <style> /* 身体 */ .main { width: 90%; height: 100px; margin: auto; /* border: 1px solid magenta; */ margin-top: 20px; display: flex; } .main_left { width: 35%; height: 100%; } .main_left img { width: 100%; height: 100%; object-fit: contain; } .main_right { width: 65%; height: 100%; padding-bottom: 10px; border-bottom: 1px solid #aaa; } .main_right p { text-align: left; line-height: 20px; margin: 10px; } </style>
2. 子组件
<div class="picture"> <!-- 用传过来的参数,直接渲染到页面 --> <img :src="route.query.tp" alt=""> </div> <div class="text"> <!-- 这部分也是获取路由跳转时传过来的信息,直接渲染到页面上 --> <p>{{route.query.name}}</p> <P style="color:#aaa;font-size: 16px;">{{route.query.zz}}</P> <P style="color:red;font-size: 16px;">¥ <span style="font-size: 20px;">{{route.query.price}}</span> </P> </div> <script lang="ts"> // 需要导入这个路由 import { useRoute } from 'vue-router'; export default { setup() { // 接收路由跳转时传过来的数据 const route = useRoute() // 通过route.query,打印传递过来的数据 console.log(route.query); return { route } } } </script>
1.8.3、通过 hash 获取(# 后面的值)
1. 在页面上操作
<h2>hash获取参数:</h2> <h4>取出hash值:{{$route.hash}}</h4>
2. 如图所示
1.9、使用 props 获取参数
作用:让路由组件更方便的收到参数
使用场景:路由组件读取参数时太麻烦

解决方法:在路由的配置文件中找到需要接收参数的路由组件,为其增加props配置项,即 在路由规则中开启 props 传参
{
name:"movie",
path:"/movie/:id/:type",
component:Movie,
props:true // 是否允许将参数映射到组件的props中
},
第一种写法: props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
//index.js
{
name:'xuexi',
path:'detail/:id',
component:Detail,
//不常用,因为数据是写死的
props:{a:900}
}
//Detail.vue <template> <div>读取a{{a}}</div> </template> <script> export default{ name:'Detail', props:['a'], } </script>
第二种写法: props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
//index.js
{
name:'xuexi',
path:'detail/:id/:title',
component:Detail,
//不推荐,只能接收params参数
props:true,
}
//Detail.vue <template> <ul> <li>消息编号:{{id}}</li> <li>消息标题:{{title}}</li> </ul> </template> <script> export default{ name:'Detail', props:['id','title'], } </script>
第三种写法(常用): props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
//index.js
{
name:'xuexi',
path:'detail/:id',
component:Detail,
props($route){
return{
id:$route.query.id,
title:$route.query.title
}
}
}
//Detail.vue <template> <ul> <li>消息编号:{{id}}</li> <li>消息标题:{{title}}</li> </ul> </template> <script> export default{ name:'Detail', props:['id','title'], } </script>
1.10、响应路由参数的变化
当用户从 /users/johnny
导航到 /users/jolyne
时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用
举例说明:如图所示
要对同一个组件中参数的变化做出响应的话,可以简单地 watch(监听) $route
对象上的任意属性
// 非语法糖
export default{
setup(props){
console.log(props);
},
props: ["id","type"],
created(this:any){
this.$watch(()=>this.$route.params.id,(toParam,fromParam)=>{
console.log("toParam="+toParam);
console.log("fromParam="+fromParam);
})
}
}
运行结果:
或者,使用 beforeRouteUpdate
导航守卫,它也可以取消导航:
const User = {
template: '...',
async beforeRouteUpdate(to, from) {
// 对路由变化做出响应...
this.userData = await fetchUser(to.params.id)
},
}
注意:控制台哪里有一个警告信息!!!!( [Violation]'setTimeout' handler took 189ms )
原因:看下图的地址栏信息,然后在对比我们路由的配置,根本就没有一个为 “ /movie ” 的路径,所以它报空路径警告,然后页面也没有显示出来!!
解决方法:在参数后加一个问号,让其变为可选参数
// 如果在参数后增加向号则参数变成可选参数,否则为必选参数,这样就不会报空路径的警告
// 问号的作用是:让参数变成可变参数
{ path: "/movie/:id?/:type?", component: Movie,props:true },
1.11、捕获所有路由或 404 Not found 路由(即对错误路径的页面进行处理)
1.11.1、概要与基本使用
常规参数只匹配 url 片段之间的字符,用 /
分隔。如果我们想匹配任意路径,我们可以使用自定义的 路径参数 正则表达式,在 路径参数 后面的括号中加入 正则表达式 :
pathMatch:参数名,自定义的
(.*)* :不限制长度和后面的 * 号代表允许重复
1. 使用正则表达式,定义路由
// 快速定义组件的方法 const NotFound = { // 组件模板 template: '<p>没有找到您要访问的内容!</p>' } { path:"/:anypath(.*)",component:NotFound }
2. 此时会报警告(不支持组件模板,所以不帮编译),所以需要在 vue.config.js 里添加 runtimeCompiler: true
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 运行时编译,缺点:打包时,包会变大(应用体积增大10KB),会消耗性能
runtimeCompiler: true
})
注意:更改了配置信息,一定要重启项目!!!
3. 运行结果:
1.11.2、获取路径信息
获取路径信息:输出 anypath 是什么
// 快速定义组件的方法 const NotFound = { // 组件模板 // $route.params:获取路径信息 template: '<p>没有找到您要访问的内容!</p> <p>{{ $route.params }}</p>' }
{ path:"/:anypath(.*)",component:NotFound } // 用正则表达式,匹配路径
运行结果:
请注意!!!
如果后面带星号的,会输出什么呢???
{ path:"/:anypath(.*)*",component:NotFound }
运行结果:
1.11.3、结合 props 使用
1. 开启 props
// 用正则表达式,匹配路径 { path:"/:anypath(.*)*",component:NotFound,props:true }
2. 使用
// 快速定义组件的方法 const NotFound = { // 组件模板 // $route.params:获取路径信息 template: '<p>没有找到您要访问的内容!</p> <p>{{ $route.params }}</p> <p>{{ anypath }}</p>', props:['anypath'] }
3. 输出结果
1.12、路由的匹配语法
1.12.1、在参数中自定义正则
用于简化路径
页面:
<h4>对象:{{ $route.params }}</h4>
路由配置:
{ path:"/:movieId(\\d+)", // 正则匹配/123路径,+:表示1个或多个数字 component:Movie },
运行结果:
1.13、声明式导航 & 编程式导航
1.13.1、介绍
- 在浏览器中,点击链接实现导航的方式,叫做声明式导航。例如:
-
- 普通网页中点击 <a> 链接, vue项目中点击 <router-link> 都属于声明式导航
- 在普通浏览器中,调用API方法实现导航的方式,叫做编程式导航。例如:
-
- 普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航
1.13.2、编程式方法的使用
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
<p> <button @click.prevent="pushHandle">router.push编程导航</button> </p> <script lang="ts" setup> // 注意:导入的是这个文件,而不是它自动生成的router文件 import { useRoute, useRouter } from "vue-router" // 调用这两个方法 const router = useRouter(); const route = useRoute(); function pushHandle() { // 使用 // 字符串路径 router.push('/about/introduce') // 导航到about下面的introduce组件中 // 带有路径的对象 router.push({ path: '/about/culture' }) // 带参数,让路由建立 url,还是要带name属性的,要不然不知道匹配的是哪个路由 router.push({ name: 'movie', params: { id: 12345,type:"action" } }) // 带查询参数,结果是 /movie?name=rose&money=58.5 router.push({ name: 'movie', query: { name:"rose",money:58.5 } }) // 带 hash,结果是 /movie#team router.push({ name: 'movie', hash: '#team' }) } </script>
注意:如果提供了 path
,params
会被忽略,上述例子中的 query
并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name
或手写完整的带有参数的 path
:
const username = 'eduardo' // 我们可以手动建立 url,但我们必须自己处理编码 router.push(`/user/${username}`) // -> /user/eduardo // 同样 router.push({ path: `/user/${username}` }) // -> /user/eduardo // 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益 router.push({ name: 'user', params: { username } }) // -> /user/eduardo // `params` 不能与 `path` 一起使用 router.push({ path: '/user', params: { username } }) // -> /user
当指定 params
时,可提供 string
或 number
参数(或者对于可重复的参数可提供一个数组)。任何其他类型(如 undefined
、false
等)都将被自动字符串化。对于可选参数,你可以提供一个空字符串(""
)来跳过它。
由于属性 to
与 router.push
接受的对象种类相同,所以两者的规则完全相同。
router.push
和所有其他导航方法都会返回一个 Promise,让我们可以等到导航完成后才知道是成功还是失败。
1.13.3、<router-link>
的 replace 属性
1.作用:控制路由跳转时操作浏览器历史记录的模式
2.浏览器的历史纪录有两种写入方式:分别为 push
和 replace
-
1.this.$router.push('hash地址')
- 跳转到指定 hash 地址,并增加一条历史记录
-
2.this.$router.replace('hash地址')
-
跳转到指定的 hash 地址,并替换当前的历史记录
-
3.开启 replace
模式:<router-link replace...>News</router-link>
1.13.4、横跨历史
该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步,类似于 window.history.go(n)
。
this.$router.go(数值):
调用 this.$router.go() 方法,可以在浏览历史中前进或后退。实例代码如下
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与 router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
举例说明:
<button @click="forward">前进</button> <button @click="back">后退</button> <button @click="goN(2)">go(n)</button> function forward(){ // 向前访问一条历史记录 router.go(1); } function back(){ // 向后访问一条历史记录 router.go(-1); } function goN(n:number){ // 移动指定步数 router.go(n) }
运行结果:
$router.go 的简化用法:
-
1.$router.back();
- 在历史记录中,后退到上一个页面
-
2.$router.forward();
-
在历史记录中,前进到下一个页面
-
1.14、命名路由
除了 path
之外,你还可以为任何路由提供 name
。这有以下优点:
-
没有硬编码的 URL
-
params
的自动编码/解码。 -
防止你在 url 中出现打字错误。
-
绕过路径排序(如显示一个)
要链接到一个命名的路由,可以向 router-link
组件的 to
属性传递一个对象:
<router-link :to="{ name: 'user', params: { username: 'erina' }}"> User </router-link>
这跟代码调用 router.push()
是一回事:
router.push({ name: 'user', params: { username: 'erina' } })
在这两种情况下,路由将导航到路径 /user/erina
1.15、命名视图(多视图)
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar
(侧导航) 和 main
(主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view
没有设置名字,那么默认为 default
。只允许在app.vue根路由中,否则无效
举例说明:
1.App组件定义三个容器
<section> <div class="views"> <!-- 只要在项目中安装和配置了 vue-router,就可以使用 router-view 这个组件 --> <!-- 它的作用很单纯:占位符,相当于一个容器 --> <router-view class="view left" name="leftView"></router-view> <router-view class="view center"></router-view> <router-view class="view right" name="rightView"></router-view> </div> </section>
2.定义多个路由
{
path: "/about",
name:"about",
// 定义多个路由,注意components是有s结尾的
components: {
leftView:Culture,
default:About,
rightView:Introduce
},
}
3. 运行结果:
1.16、路由别名
重定向是指当用户访问 /home
时,URL 会被 /
替换,然后匹配成 /
。那么什么是别名呢?
将 /
别名为 /home
,意味着当用户访问 /home
时,URL 仍然是 /home
,但会被匹配为用户正在访问 /
。
// alias:别名,用 [] 可以取多个
const routes = [{ path: '/', component: Homepage, alias: '/home' }]
1.17、链接样式
1.17.1、active-class
-
类型:
string
-
默认值:
"router-link-active"
(或者全局linkActiveClass
) -
详细内容:
-
链接激活时,应用于渲染的
<a>
的 class。
-
1.17.2、linkActiveClass(全局)
用于激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-active
。
1.17.3、exact-active-class
-
类型:
string
-
默认值:
"router-link-exact-active"
(或者全局linkExactActiveClass
) -
详细内容:
-
链接精准激活时,应用于渲染的
<a>
的 class。
-
1.17.4、linkExactActiveClass(全局)
用于精准激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-exact-active
。
1.17.5、用途
如图所示:
代码实现:给两个类添加样式
/* 用于激活的 RouterLink 的默认类 */ .router-link-active{ border-bottom: 2px solid orangered; } /* 链接精准激活时,应用于渲染的 <a> 的 class。 */ .router-link-exact-active{ background: khaki; }
如果想修改类名的话,需要在 index.ts 添加两个属性,如下所示:
// 创建路由对象 let router = createRouter({ history: createWebHashHistory(), // 指定路由模式为hash模式(兼容性好,但带#) routes, // 以下两个属性就是修改链接样式类的名称 linkActiveClass:"active", linkExactActiveClass:"exact" }); // 向外共享路由的实例对象 export default router;
App.vue 引用:
/* 用于激活的 RouterLink 的默认类 */ .router-link-active,.active{ border-bottom: 2px solid orangered; } /* 链接精准激活时,应用于渲染的 <a> 的 class。 */ .router-link-exact-active,.exact{ background: khaki; }
运行结果:
1.17、导航守卫
导航守卫可以控制路由的访问记录权限
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
1.17.1、全局前置守卫
可以使用 router.beforeEach
注册一个全局前置守卫:
每次发生路由的导航跳转的时,都会触发全局前置守卫。因此,在全局前置守卫中,程序员可以对每个路由进行访问权限的控制
const router = createRouter({ ... }) router.beforeEach((to, from) => { // ... // 返回 false 以取消导航 return false })
2.守卫方法的3个形参
router.beforeEach(()=>{
// to 是将要访问的路由的信息对象
// from 是将要离开的路由的信息对象
// next 是一个函数,调用 next () 表示放行,允许这次路由导航
})
3.next函数的3种调用方式
-
1.当前用户拥有后台主页的访问权限,直接放行:next()
-
2.当前用户没有后台主页的访问权限,强制其跳转到登陆页面:next('/longin')
-
3.当前用户没有后台主页的访问权限,不允许跳转到后台页面:next(false)
4. 不使用 next 实现前置守卫的模拟登录栗子
(1)在路由配置文件里面,定义前置守卫规则
// 添加导航守卫 // 每一个导航的开始都必须经过该函数,类似于钩子函数 router.beforeEach((to,from)=>{ console.log("开始导航了beforeEach...."); console.log("目标位置:" + to); console.log("原来位置:" + from); // 1、如果返回值为true或者无返回值,没有next参数,则正常访问默认路径 // return true; // 2、如果返回值为false则阻塞访问,拒绝访问 // return false; // 3、返回路径对象,则导航到指定路径 if (to.name!=="login" && !authentication) { // 如果当前路由不是login则跳转到login return {name: "login"} } return true; // 放行 }) // 判断用户是否登录 function authentication(){ let username = localStorage.getItem("USERNAME"); // 获取本地存储的值 return !!username; // !!转布尔类型的 } // 向外共享路由的实例对象 export default router;
(2)在页面中实现登录功能
<template > <h2>用户登录</h2> <fieldset> <legend>用户信息</legend> <p> <label for="">账号:</label> <input type="text" v-model="user.username"> </p> <p> <label for="">密码:</label> <input type="text" v-model="user.password"> </p> <p> <button @click.prevent="login" >登录</button> </p> </fieldset> </template> <script lang="ts" setup> let user = {username:"",password:""}; function login(){ if (user.username === "admin" && user.password === "123456") { localStorage.setItem("USERNAME",user.username) // 设置本地存储的值 } } </script> <style scoped> </style>
5. 使用 next 实现前置守卫的模拟登录栗子
(1)在路由配置文件里面,定义前置守卫规则
// 添加导航守卫 // 每一个导航的开始都必须经过该函数,类似于钩子函数 router.beforeEach((to,from,next)=>{ // 1、如果不调用next则导航阻塞 // 2、如果直接调用next不带参数则表示放行 // next(); // 3、可以在next中指定路径对象 // 判断不等于登录页,而且验证不通过的话,还是一样在登录页 if (to.name!=="login" && !authentication()) { next({name:"login"}); } // 否则其他页面,直接放行 next(); }) // 判断用户是否登录 function authentication(){ let username = localStorage.getItem("USERNAME"); // 获取本地存储的值 return !!username; // !!转布尔类型的 } // 向外共享路由的实例对象 export default router;
(2)在页面中实现登录功能
<template > <div> <h2>用户登录</h2> <fieldset> <legend>用户信息</legend> <p> <label for="">账号:</label> <input type="text" v-model="user.username"> </p> <p> <label for="">密码:</label> <input type="text" v-model="user.password"> </p> <p> <button @click.prevent="login">登录</button> </p> </fieldset> </div> </template> <script lang="ts" setup> let user = { username: "", password: "" }; function login() { if (user.username === "admin" && user.password === "123456") { localStorage.setItem("USERNAME", user.username) // 设置本地存储的值 } } </script> <style scoped> </style>
6. 运行结果:不登录的话,就点击其他的页面是没有效果的,还是一样回到登录页,输入账号密码成功后,就可以点击其他页面了
1.17.2、全局后置守卫
也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next
函数也不会改变导航本身:
// 添加后置导航守卫 router.afterEach((to,from)=>{ console.log("结束导航了afterEach...."); })
它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用!
1.17.3、路由独享的守卫
如果是设置单个值的:
{ name:"movie", path: "/movie/:id?/:type?", component: Movie, props:true, alias:["/film","/m"], beforeEnter: function(to,from){ if (Object.keys(to.params).length) { // 如果有参数 // 去掉参数,把params的值设为空 //return { name:to.name,path:to.path,query:to.query,hash:to.hash,params:{} } to.params={}; return to; } return true; } },
设置多个值:
{ name:"movie", path: "/movie/:id?/:type?", component: Movie, props:true, alias:["/film","/m"], beforeEnter: [RemoveParam,RemoveHash] // 定义一个数组 }, // 移除导航中的参数 function RemoveParam(to){ if (Object.keys(to.params).length) { // 如果有参数 // 去掉参数,把params的值设为空 //return { name:to.name,path:to.path,query:to.query,hash:to.hash,params:{} } to.params={}; return to; } return true; } // 移除导航中的hash function RemoveHash(to){ if (Object.keys(to.hash).length) { // 如果有hash值 // 去掉参数,把hash的值设为空 to.hash=undefined; return to; } return true; }
1.17.4、组件内的守卫
最后,你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)
你可以为路由组件添加以下配置:
-
beforeRouteEnter
- 在渲染该组件的对应路由被验证前调用
- 不能获取组件实例 `this` !
- 因为当守卫执行时,组件实例还没被创建!
-
beforeRouteUpdate
- 在当前路由改变,但是该组件被复用时调用
- 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
- 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
- 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
-
beforeRouteLeave
-
在导航离开渲染该组件的对应路由时调用
- 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
如果你正在使用组合 API 和 setup
函数来编写组件,你可以通过 onBeforeRouteUpdate
和 onBeforeRouteLeave
分别添加 update 和 leave 守卫。 请参考组合 API 部分以获得更多细节。
举例如下:
login.vue 文件代码如下:
<template> <div> <h2>用户登录</h2> <fieldset> <legend>用户信息</legend> <p> <label>帐号:</label> <input v-model="user.username" /> </p> <p> <label>密码:</label> <input v-model="user.password" type="password" /> </p> <p> <button @click.prevent="login">登录</button> </p> </fieldset> </div> </template> <script setup lang="ts"> import { useRoute, useRouter } from 'vue-router'; let route = useRoute(); let router = useRouter(); console.log(route); console.log(router); let user = { username: '', password: '' }; function login() { if (user.username === 'admin' && user.password === '123456') { localStorage.setItem('USERNAME', user.username); // 设置本地存储的值 //获取上一个请求的路径,返回路径,如果没有则直接进入main let returnUrl = route.query.returnUrl || '/main'; router.replace({ path: returnUrl + '' }); } } </script> <style scoped> </style>
main.vue 文件代码如下:
<template > <div class="sdsds"> <h2>用户后台</h2> <p> <a href="#">修改密码</a> | <a href="#">安全退出</a> </p> </div> </template> <script lang="ts" setup> </script> <style scoped> .sdsds { display: flex; /* 以列的方式 */ flex-direction: column; } </style>
movie.vue 文件代码如下:
beforeRouteEnter(to,from){
console.log("---- beforeRouteEnter组件内开始导航事件 ----");
},
beforeRouteUpdate(to,from){
console.log("---- beforeRouteUpdate组件内导航更新事件 ----");
},
beforeRouteLeave(to,from){
console.log("---- beforeRouteLeave组件内离开导航事件 ----");
},
运行结果就是:没点击之前,如果是还没有登录的情况下,点了电影这个组件,需要登录,登录成功后会自动跳到电影这个组件!!!
1.17.5、完整的导航解析流程
-
导航被触发。
-
在失活的组件里调用
beforeRouteLeave
守卫。 -
调用全局的
beforeEach
守卫。 -
在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 -
在路由配置里调用
beforeEnter
。 -
解析异步路由组件。
-
在被激活的组件里调用
beforeRouteEnter
。 -
调用全局的
beforeResolve
守卫(2.5+)。 -
导航被确认。
-
调用全局的
afterEach
钩子。 -
触发 DOM 更新。
-
调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
1.18、路由元信息
有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta
属性来实现,并且它可以在路由地址和导航守卫上都被访问到。(简单来说,就是限制哪个组件没有登录之前,也能点击)定义路由的时候你可以这样配置 meta
字段:
const routes = [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// 只有经过身份验证的用户才能创建帖子
meta: { requiresAuth: true }
},
{
path: ':id',
component: PostsDetail
// 任何人都可以阅读文章
meta: { requiresAuth: false }
}
]
}
]
那么如何访问这个 meta
字段呢?
首先,我们称呼 routes
配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,它可能匹配多个路由记录。
例如,根据上面的路由配置,/posts/new
这个 URL 将会匹配父路由记录 (path: '/posts'
) 以及子路由记录 (path: 'new'
)。
一个路由匹配到的所有路由记录会暴露为 $route
对象(还有在导航守卫中的路由对象)的$route.matched
数组。我们需要遍历这个数组来检查路由记录中的 meta
字段,但是 Vue Router 还为你提供了一个 $route.meta
方法,它是一个非递归合并所有 meta
字段的(从父字段到子字段)的方法。这意味着你可以简单地写
router.beforeEach((to, from) => {
// 而不是去检查每条路由记录
// to.matched.some(record => record.meta.requiresAuth)
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: '/login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
}
}
})
1.19、路由懒加载
1.19.1、什么是路由懒加载?
整个网页默认是刚打开就去加载所有页面,路由懒加载就是只加载你当前点击的那个模块。
按需去加载路由对应的资源,提高首屏加载速度(tip:首页不用设置懒加载,而且一个页面加载过后再次访问不会重复加载)。
实现原理:将路由相关的组件,不再直接导入了,而是改写成异步组件的写法,只有当函数被调用的时候,才去加载对应的组件内容。
1.19.2.传统路由配置
import Vue from 'vue' import VueRouter from 'vue-router' import Login from '@/views/login/index.vue' import Home from '@/views/home/home.vue' Vue.use(VueRouter) const router = new VueRouter({ routes: [ { path: '/login', component: Login }, { path: '/home', component: Home } ] export default router
1.19.3、路由懒加载写法
import Vue from 'vue' import VueRouter from 'vue-router' //const Login = ()=> { // return import('@/views/login/index.vue') //} //const Home = ()=> { // return import('@/views/home/home.vue') //} //有return且函数体只有一行,所以省略后为 const Login = ()=> import('@/views/login/index.vue') const Home = ()=> import('@/views/home/home.vue') Vue.use(VueRouter) const router = new VueRouter({ routes: [ { path: '/login', component: Login }, { path: '/home', component: Home } ] export default router
再次简化(省去定义变量):
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const router = new VueRouter({ routes: [ { path: '/login', component: () => import('@/views/login/index.vue') }, { path: '/home', component: () => import('@/views/home/home.vue') } ] export default router
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
// 将 // import UserDetails from './views/UserDetails.vue' // 替换成 const UserDetails = () => import('./views/UserDetails.vue') const router = createRouter({ // ... routes: [{ path: '/users/:id', component: UserDetails }], })
component
(和 components
) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise :
const UserDetails = () => Promise.resolve({ /* 组件定义 */ })
一般来说,对所有的路由都使用动态导入是个好主意。
注意
不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。
如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。
如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。
添加动态导入的方法:
// 动态导入方法一 const Home=()=>import('../components/home.vue'); // 动态导入方法二 { name:"login", path:"/login", component: import("../components/login.vue"), meta:{ "requiredAuth":false } }
1.19.4、把组件按组分块
const UserDetails = () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') const UserDashboard = () => import(/* webpackChunkName: "group-user" */ './UserDashboard.vue') const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
因为指定了chunk的名称,所有Home与Login被打包在一起了。
https://cli.vuejs.org/zh/
需要注意的是:/* webpackChunkName: "group-user" */ 魔术注释只是指定了[name],可以在配置文件中指定新的名称,修改vue.config.js文件如下:
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, runtimeCompiler:true, //开启运行时编译 configureWebpack: { output: { filename: `[name].[hash].js`, chunkFilename: `js/[name].[chunkhash].js` } } })
运行效果如下:
此处可以发现使用了文件的hash值,可以解决客户端缓存的问题!