路由
路由的发展历程
后端路由阶段
前后端分离阶段
SPA(single page app) 单页面应用
之前的前后端分离,后端依然保留了大量模板代码,前端依然需要将这些模板代码请求下来,然后通过 ajax 请求将数据和模板拼成了一个完整网页。
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。不用后端,换句话说不用发送请求,前端就能完成页面的切换。
此时需要一个页面与路径的映射关系,也就是路由。因为这个映射关系维护在前端,所以叫前端路由。
前端路由的原理
无非是让 URI 发生变化,然后监听到变化,进行相应的页面展示,形成 url 和组件的隐射关系。
其中让 URI 变化有两种实现方式。
URL 的 hash
URL 的 hash 也就是锚点#
后面的字符,点击锚点链接会把#hash
添加到 url 后面,本质上是修改了window.location
的href
属性。锚点链接可以让 url 变化,并且不会刷新页面,这很适合做前端路由。
- 我们可以通过
location.hash
来获取或者修改hash
。
hash 的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径
<div id="app">
<!-- 点击链接,就会在 url 后面添加上 #/home -->
<a href="#/home">home</a>
<a href="#/about">about</a>
<div class="content">Default</div>
</div>
<script>
const contentEl = document.querySelector('.content');
// 对 url 的变化进行监听
window.addEventListener("hashchange", () => {
// 形成映射关系
switch(location.hash) {
case "#/home":
console.log(location.hash); // #/home
console.log(location.href); // http://127.0.0.1:5500/index.html#/home
contentEl.innerHTML = "Home";
break;
case "#/about":
contentEl.innerHTML = "About";
break;
default:
contentEl.innerHTML = "Default";
}
})
</script>
HTML5 的 History
history 接口是 HTML5 新增的, 它有六个方法改变 URL 而不刷新页面:
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forward:向前改变路径;
- back:向后改变路径;
history 会维护一个栈结构来记录当前的浏览记录。
replaceState 方法相当于替换掉栈顶记录,所以如果只使用 replaceState 来改变路径,则不能进行网页后退,因为栈里始终只有一条记录。
pushState 和 popState 相当于压栈和出栈,所以能前进后退网页。另外 hash 模式下网页也能前进后退。
后面三个方法就是在栈里面像个指针一样跳来跳去。
<div id="app">
<!-- 正常使用超链接,页面会发送请求并跳转 -->
<a href="/home">home</a>
<a href="/about">about</a>
<div class="content">Default</div>
</div>
<script>
const contentEl = document.querySelector('.content');
const changeContent = () => {
// pathname 也能拿到 a 标签添加的路径
switch(location.pathname) {
case "/home":
contentEl.innerHTML = "Home";
break;
case "/about":
contentEl.innerHTML = "About";
break;
default:
contentEl.innerHTML = "Default";
}
}
const aEls = document.getElementsByTagName("a");
for (let aEl of aEls) {
aEl.addEventListener("click", e => {
// 遍历监听所有 a 标签,清除他们的默认行为
e.preventDefault();
// 拿到 a 标签的 href,通过 history api 将 href 路径添加到 url 上
const href = aEl.getAttribute("href");
history.pushState({}, "", href);
// history.replaceState({}, "", href);
// 根据 url 的改变,进行页面映射
changeContent();
})
}
// 因为用的 pushState,能后退,所以要监听 popstate 事件,点击后退时能进行内容和 url 映射
window.addEventListener("popstate", changeContent)
</script>
vue-router
使用 vue-router
- 第一步:创建路由组件的配置文件;
- 第二步:配置路由映射: 组件和路径映射关系的 routes 数组;
- 第三步:通过
createRouter()
创建路由对象,并且传入 routes 并配置路由模式: history 或 hash 模式; - 第四步:在 mian.js 中注册 router 插件
- 第五步:使用路由: 通过
<router-link>
修改 url 和使用<router-view>
占位; ```javascript import { createRouter, createWebHashHistory } from “vue-router”;
import Home from ‘../views/Home.vue’ import About from ‘../views/About.vue’
// 配置路由关系 const routes = [ { path: ‘/home’, component: Home}, { path: ‘/about’, component: About} ]
// 创建路由对象 const router = createRouter({ routes: routes, history: createWebHashHistory() // 使用 history 模式路由,默认为 hash 路由 })
export default router
```html
<template>
<div>
<router-link to="/home">home</router-link>
<router-link to="/about">about</router-link>
<router-view></router-view>
</div>
</template>
路由的默认路径
默认情况下, 进入网站的首页, 我们希望/
配置一个映射。
const routes = [ { path: '/', component: Home} ]
我们一般不会给根路径直接配置一个组件,一般是选择重定向redirect
是重定向, 也就是我们将根路径重定向到 /home 的路径下, 这样就可以得到我们想要的结果了.
const routes = [
{ path: '/', redirect: '/home'},
{ path: '/home', component: Home},
{ path: '/about', component: About}
]
router-link
router-link事实上有很多属性可以配置:
to
属性:是一个字符串,或者是一个对象replace
属性:设置 replace 属性的话,当点击时,会调用 router.replace(),默认是调用 router.push();
router.replace() 其实是 history.replaceState() 方法的封装,替换栈顶记录,所以网页不能后退。router.push 就是压栈。
<router-link to="/home" replace>home</router-link>
<router-link to="/about" replace>about</router-link>
active-class
属性:设置激活 a 元素后应用的 class
router-link 是内置组件,转为真实 dom 后,为 a 标签。当该 a 标签被选中后,会默认添加一个 class 类:router-link-active
。所以我可以在这个类中添加标签的选中样式。
也可以通过 active-class 属性显示指定该 a 标签被选中后,自动添加到 a 标签的 class 类。
<router-link to="/home" active-class="hhh" >home</router-link>
exact-active-class
属性:链接精准激活时,应用于渲染的 的 class,默认是 router-link-exact-active;
精准激活是对于存在子路由的情况的,exact-active-class 类会填添加到完全匹配的 path 路径上。
比如 /home/subhhome
,此时点击链接从组件 home 中渲染出了子组件 subhome,那么 exact-active-class 只会添加到 subhome 组件上,精确匹配了完整路径。而 active-class 类不仅添加到 subhome,还会添加到 home 组件上,只要是匹配上的组件都会添加。
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载:
- 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效;也可以提高首屏的渲染效率;
- 这种按需加载就是路由懒加载。
其实这里还是我们前面讲到过的 webpack 的分包知识, Vue Router 默认就支持动态来导入组件:
- 这是因为 component 属性可以传入一个组件,也可以接收一个函数,该函数 需要放回一个Promise;
- 而 import 函数就是返回一个 Promise,并且是异步的。
webpack 也可以通过魔法注释给异步加载的包自定义包名:``
const routes = [
{ path: '/', redirect: '/home'},
{ path: '/home', component: () => {/*webpackChunkName: "home-chunk" */ import('../views/Home.vue')}},
{ path: '/about', component: About}
]
路由的其他属性
routes 数组中的每一个对象 route ,还有很多其他的属性,比如 name:表示该路由的名称;meta:表示路由中自定义的其他属性。
const routes = [
// 这是一个 route 对象
{
path: "/home",
name: "home",
//component: () => {
// return import(/* webpackChunkName: "home-chunk" */'../views/Home.vue')}
//},
// 简写
component: () => import(/* webpackChunkName: "home-chunk" */"../views/Home.vue"),
meta: {
name: "why",
age: 18,
height: 1.88
}
}
]
动态路由
很多时候我们需要将多个 url 映射到同一个组件:
例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但是用户的 ID 是不同的,路径可能是 /user/zs、/user/ls 。这些路径应该都映射到 User 组件。
在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数;
const routes = [
{
path: "/user/:id", // id 为参数
component: () => import("../pages/User.vue"),
},
{
path: "/info/:name/aaa/:id" // 设置多个参数
...
}
]
<!-- 123, 456 就是实参 -->
<router-link to="/user/123" >user</router-link>
<router-link to="/user/456" >user</router-link>
获取动态路由的值
动态路由映射的组件还能拿到动态路由的实参。实参会被保存在全局变量$route
对象中的params
属性。options api 可以直接this.$route.params
访问,模板中就不用 this,直接访问。
在 setup 中,我们要使用 vue-router 库给我们提供的一个hook useRoute
;该Hook会返回一个Route对象,对象中保存着当前路由相关的值;
<template>
<div>
<h2> About </h2>
<h3> {{ $route.params.id }}</h3>
</div>
</template>
<script>
import { useRoute } from "vue-router"
export default {
created() {
console.log(this.$route.params.id) // options api 中获取
},
setup() {
const route = useRoute()
console.log(route.params.id); // composition api 中通过 hook 获取
}
}
</script>
404 页面
对于哪些没有匹配到的路由,我们通常会匹配到固定的某个页面,比如 NotFound 的错误页面中,这个时候我们可编写一个动态路由用于匹配所有的页面。这是个固定写法了。
{
// : 绑定 pathMatch(正则) 函数;正则:. 表示任意内容,* 表示任意多个
path: '/:pathMatch(.*)',
component: () => import('../views/NotFind.vue')
}
我们可以通过 $route.params.pathMatch
获取到传入的参数,它将 url 作为字符串输出。
如果在/:pathMatch(.*)
后面又加了一个 ,变成`/:pathMatch(.)*,那么将会对
/`进行解析,将每个下划线分割的内容放入数组中。
路由的嵌套
什么是路由的嵌套呢?
- 目前我们匹配的Home、About、User等都属于底层路由,我们在它们之间可以来回进行切换;
- 但是呢,我们Home页面本身,也可能会在多个组件之间来回切换:
- 比如Home中包括Product、Message,它们可以在Home内部来回切换;
- 这个时候我们就需要使用嵌套路由,在 Home 中也使用 router-view 来占位之后需要渲染的组件;
通过 route 对象中的 children 属性来配置子路由。注意子路由中重定向需要写完整的路径,因为重定向相当于在地址栏重新输入一次。
{
path: "/home",
name: "home",
component: () => import('../pages/Home.vue'),
// 子路由
children: [
{
path: "",
redirect: "/home/message" // 完整路径
},
{
path: "message", // 注意:不需要斜杆
component: () => import("../pages/HomeMessage.vue")
},
{
path: "shops",
component: () => import("../pages/HomeShops.vue")
}
]
}
<router-link to="/home/message">message</router-link>
<router-link to="/home/shops">shops</router-link>
<router-view></router-view>
编程式导航
有时候我们希望通过代码来完成页面的跳转,而不是通过封装好的 router-link。比如点击的是一个按钮。
- options api 中通过
$router
对象来实现,对象中有push()
和replace()
等封装好的方法。 setup 中则使用
useRouter
hook 来实现,其中也有replace()
等方法注意:这次的 hook 是
**useRouter**
,获取动态路由参数的是**useRoute**
```htmlHome
info
<a name="AQTdi"></a>
## query 方式的参数
url 中有 queryString 查询字符串,`this.$router`的方法中也可以传递对象,在 query 属性中对查询字符串进行配置。
```javascript
this.$router.push({
path: '/home/info',
query: { name: 'zs', age: 18 }
})
// url:#/home/info?name=zs&age=18
options api 通过$route.query
来获取参数,setup 也能继续通过 useRoute hook 获得,useRoute().query
<h1> {{ $route.query.name }}</h1>
<script>
import { useRoute } from "vue-router";
export default {
setup() {
const route = useRoute()
console.log(route.query.name); // zs
}
}
</script>
页面的前进后退
如果想要控制页面的前进和后退,useRouter() 返回的 router 对象中也封装了 history 对象中的 go、back、forward 方法。
router的go方法:
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与router.back()相同
router.go(-1)
//前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router也有back:
- 通过调用 history.back() 回溯历史。相当于 router.go(-1);
router也有forward:
- 通过调用 history.forward() 在历史中前进。相当于 router.go(1);
路由中的作用域插槽
router-link 的 v-slot
在 vue-router3.x 的时候,router-link 有一个tag属性,可以决定router-link到底渲染成什么元素:但是在vue-router4.x开始,该属性被移除了;并且给我们提供了更加具有灵活性的 v-slot 的方式来定制渲染的内容。
router-link 内置组件中默认提供了一个插槽,我们可以在其中自定义渲染的内容。router-link 默认是转成一个 a 元素,如果自定义内容不想被 a 标签包裹,可以添加custom
属性,表示我们整个元素要自定义,取消 a 元素包裹。
router-link 的默认插槽实际是个作用域插槽,它提供了很多值给我们使用。比如
href
:解析后的 URL;route
:解析后的规范化的route对象;navigate
:默认提供的触发导航的函数;- 添加 custom 属性后,router-link 内的内容不会被 a 元素包裹,就不能点击进行网页跳转了
- 问题不大,我们可以自己写函数进行编程式导航,比如自己写一个 click 事件的回调函数进行跳转
- 但其实不用自己写,navigate 就是默认提供的函数可以实现跳转
isActive
:是否匹配的状态;isExactActive
:是否是精准匹配的状态;
我们可以解构获取使用:
<template>
<div>
<h2> Home </h2>
<router-link to="/home/info" custom v-slot="{ href, route, isActive, isExactActive, navigate }">
<h3>{{ href }} </h3>
<h3>{{ route }} </h3>
<!-- 点击 hhh 跳转后,都为 true -->
<h3>{{ isActive }} </h3>
<h3>{{ isExactActive }} </h3>
<!-- 点击 hhh 实现跳转 -->
<p @click="navigate">hhh</p>
</router-link>
<router-view></router-view>
</div>
</template>
router-view 的 v-slot
router-view 也提供给我们一个作用域插槽,从而让我们在 router-view 中拿到实际渲染的组件。然后我们就可以给渲染的组件添加动画或者进行缓存。
router-view 作用域插槽传递过来的值,有个 component ,我们可以通过父向子传值一样的自定义属性对象 props拿到它。为了方便使用也可以直接解构。
拿到将要渲染的组件后,就可以将它放在动态组件中渲染,以实现
<router-view v-slot="hhh">
<!-- <router-view v-slot ='{ Component }'> -->
<transition name="ddd">
<keep-alive>
<component :is="hhh.Component"></component>
</keep-alive>
</transition>
</router-view>
<style>
.ddd-enter-from,
.ddd-leave-to {
opacity: 0;
}
.ddd-enter-to,
.ddd-leave-from {
opacity: 1;
}
.ddd-enter-active,
.ddd-leave-active {
transition: opacity 1s ease-in;
}
</style>
动态添加、删除路由
某些情况下我们可能需要动态的来添加路由,比如管理员和普通用户的菜单不一样。
一般有两种方式处理:前端根据后端返回的菜单在页面上进行展示和隐藏按钮。但是这种方式假如用户知道页面对应的路由,就可以自己在地址栏输入 url 来访问,这不合理。
第二种方式就是前端做判断,对不同角色或者菜单进行动态添加路由。普通用户哪怕知道管理员的 url ,因为没有添加该路由,所以还是会进入 404 页面。
router.addRoute()
方法添加路由,有两个属性
- parentName:父路由的名称,route.name
- route:路由
如果有第一个属性,则是添加子路由,没有就是添加根路由。注意添加子路由,path 没有斜杆。
if('管理员') {
router.addRoute('home', { // 给 home route 添加子路由
path: 'info',
component: () => import('./views/info.vue')
})
}
动态删除路由有以下三种方式:
方式一:添加一个 name 相同的路由,替换掉原来的路由
方式二:通过 removeRoute 方法,传入路由的名称进行删除路由;
方式三:通过 addRoute 方法的返回值回调;添加路由的方法返回值是一个函数,调用它就是删除该添加的路由
路由的其他方法补充:
- router.hasRoute():检查路由是否存在。
router.getRoutes():获取一个包含所有路由记录的数组。
路由导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。比如:截获路由,判断是否放行。
全局的前置守卫**beforeEach**
是在导航触发时会被回调的:
它有两个参数:to:即将进入的路由 Route 对象;
- from:即将离开的路由 Route 对象;
它有返回值:
- false:取消当前导航;
- 不返回或者 undefined:进行默认导航,相当于没有 beforeEach 函数存在一样
返回一个路由地址:
- 可以是一个 String 类型的路径;
- 可以是一个对象,对象中包含path、query、params等信息;
```javascript
// to: Route对象, 即将跳转到的Route对象
// from: Route对象,
/**
- 返回值问题:
- 1.false: 不进行导航
- 2.undefined或者不写返回值: 进行默认导航
- 3.字符串: 路径, 跳转到对应的路径中
- 4.对象: 类似于 router.push({path: “/login”, query: ….}) 中的参数对象 */ // 登录守卫 router.beforeEach((to, from) => {
// 判断是否有 token,否则跳转登录页面 if (to.path !== “/login”) { const token = window.localStorage.getItem(“token”); if (!token) {
return "/login"
} }
})
可选的第三个参数:next
- 在Vue2中我们是通过next函数来决定如何进行跳转的;
- 但是在Vue3中我们是通过返回值来控制的,不再推荐使用next函数,这是因为开发中很容易调用多次next;
<a name="yMdB9"></a>
## 其他导航守卫
Vue还提供了很多的其他守卫函数,目的都是在某一个时刻给予我们回调,让我们可以更好的控制程序的流程或者功能: [https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html](https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html)<br />我们一起来看一下完整的导航解析流程:
1. 导航被触发。
2. 在失活的组件里调用 beforeRouteLeave 守卫。
3. 调用全局的 beforeEach 守卫。
4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
5. 在路由配置里调用 beforeEnter。
6. 解析异步路由组件。
7. 在被激活的组件里调用 beforeRouteEnter。
8. 调用全局的 beforeResolve 守卫(2.5+)。
9. 导航被确认。
10. 调用全局的 afterEach 钩子。
11. 触发 DOM 更新。
12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
<a name="dBQpH"></a>
# historyApiFallback 属性
<a name="PZ6Ob"></a>
## 刷新页面的问题
通过前端路由我们进入一个页面,假设当前 url 为:http://localhost:8080/index/message<br />此时我们点击浏览器的刷新按钮,浏览器就会发送请求,向后端请求 /index/message 这个路径的静态资源。<br />这下问题就来了,这是前端路由的页面,nginx 怎么会有这个页面的资源呢?所以就会 404。显然这是不行的,所以 nginx 会进行配置,给出一个找资源的目录,让你去找,如果没找到就默认返回首页 index.html<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22919157/1652810002353-3655a051-ea35-4f2b-814a-8af40fad6186.png#clientId=u16a0bf65-b080-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=87&id=ufefb5987&margin=%5Bobject%20Object%5D&name=image.png&originHeight=136&originWidth=466&originalType=binary&ratio=1&rotation=0&showTitle=false&size=76887&status=done&style=none&taskId=ud2fe0362-9023-41a8-b44e-084b45d6be9&title=&width=298.24)<br />前端拿到首页,相当于重定向了一次到首页,又监听到 url 中的 /index/message,前端路由就发挥作用了,开始匹配路由规则,从而渲染出 /index/message 对应的组件。这样就避免了 404。<br />所以说,刷新了一下页面,相当于重进了一次该网站,并重进了该页面。
<a name="rJH3H"></a>
## webpack 配置 historyApiFallback
但是 vue-cli 启动本地服务器的时候,可没有 nginx 配置,为什么刷新的时候没有 404?<br />因为在 webpack 中设置了`historyApiFallback: true`,它能起到同样的效果。
`historyApiFallback`是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误。
- boolean值:默认是false
- 如果设置为true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容;
object类型的值,可以配置rewrites属性:
- 可以配置from来匹配路径,决定要跳转到哪一个页面;
事实上vue-cli devServer 中实现 historyApiFallback 功能是通过 connect-history-api-fallback 库。
我们可以吃饱了撑的手动去修改 vue-cli 的源码,设置 historyApiFallback 为 false,刷新就会 404 。也可以修改 vue 的默认配置文件`vue.config.js`,里面的配置将会和 vue 自己的配置进行融合。
```javascript
module.exports = {
configureWebpack: {
devServer: {
historyApiFallback: false
}
}
}