认识前端路由
路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:
-
后端路由阶段。
-
前后端分离阶段。
-
单页面富应用(SPA)。 下面我们就来介绍一下这些阶段
后端路由阶段
早期的网站开发整个HTML页面是由服务器来渲染的。服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示。
但是, 一个网站, 这么多页面服务器如何处理呢?
每个页面都有对应的一个网址(url),用户在浏览器中输入对应的url,服务器将返回对应的完整页面。这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化。
后端路由的缺点:
-
一种情况是整个页面的模块由后端人员来编写和维护的。
-
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码。
-
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情。
前后端分离阶段
这个阶段就是后端人员写数据接口,前端人员写页面。拿到数据接口渲染对应的页面。
前端渲染的理解:
每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染。需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件。同时可以看到,和之前的后端路由不同,这时后端只是负责提供API了。
前后端分离阶段:
随着Ajax的出现, 有了前后端分离的开发模式。后端只提供API来返回数据,前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中。这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上。并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可。目前比较少的网站采用这种模式开发(jQuery开发模式)。
SPA(single page application)单页面应用阶段
一个应用有多个页面,需要切换展示时,我们前端自己维护一个路由映射表。我们通过改变hash或者通过HTML5提供的history模式。这两种模式修改url。不会向服务器请求资源。就可以映射到不同的页面了。
改变hash,实现前端路由
通过hashchange
监听hash的变化(location.href来改变),来改变占位内容。
了解更多location的内容,请访问mdn:developer.mozilla.org/zh-CN/docs/…
<div id="app">
<a href="#/home">home</a>
<a href="#/about">about</a>
<div id="content">Default</div>
</div>
<script>
const contentEl = document.getElementById("content")
window.addEventListener("hashchange", () => {
switch(location.hash) {
case "#/home":
contentEl.innerHTML = "Home";
break;
case "#/about":
contentEl.innerHTML = "About";
break;
default:
contentEl.innerHTML = "Default";
}
})
</script>
hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
history模式实现前端路由
history接口是HTML5新增的, 它有六种模式改变URL而不刷新页面:
-
replaceState:替换原来的路径。
-
pushState:使用新的路径。
-
popState:路径的回退。
-
go:向前或向后改变路径。
-
forward:向前改变路径。
-
back:向后改变路径。 下面就来简单实现以下
<a href="/zh">zh</a>
<a href="/llm">llm</a>
<div id="container">我是默认页面</div>
<script>
const container = document.getElementById("container");
const aEls = document.getElementsByTagName("a");
const changeContent = () => {
switch (location.pathname) {
case "/zh":
container.innerHTML = "我是zh页面"
break;
case "/llm":
container.innerHTML = "我是llm页面"
break;
default:
container.innerHTML = "我是其他页面"
break;
}
}
for (let a of aEls) {
a.addEventListener("click", (e) => {
e.preventDefault();
const href = a.getAttribute("href");
history.pushState({}, "", href)
changeContent()
})
}
addEventListener("popstate", changeContent)
vue-router
通过上面的介绍,我们现在来介绍一下vue-router吧。
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得非常容易。目前Vue路由最新的版本是4.x版本。
vue-router是基于路由和组件的。路由用于设定访问路径, 将路径和组件映射起来。在vue-router的单页面应用中, 页面的路径的改变就是组件的切换。
安装Vue Router:npm install vue-router@4
。
路由的基本使用步骤
-
创建路由组件的组件。
-
配置路由映射: 组件和路径映射关系的routes数组。
-
通过
createRouter
创建路由对象,并且传入routes和history模式。 -
使用路由通过
<router-link>
(切换到指定的组件) 和<router-view>
(切换路径,他就自动匹配到对应的组件)。 -
并将router对象在app.use中注册。 下面我们来介绍一下routes(路由组件映射表)
路由的映射,只会匹配到第一个准确的路径,当匹配到多个路径时,只会展示第一个匹配到的路径的组件
-
path
: 配置路由路径 -
redirect
: 重定向。参数重定向的路由路径。
{
path: "/",
redirect: "/home"
}
-
children
: 配置子路由。一个数组。每个数组元素都是一个route。 -
alias
: 配置路由别名。string | string[]
。可以简化牵头路由的路径。
{
path: "/home",
name: "home",
component: () => import("../pages/Home.vue"),
children: [
{
path: "message",
component: () => import("../pages/HomeMessage.vue"),
alias: ['/zh', 'zh']
}
}
}
-
name
: 该路由匹配的名称。(唯一) -
beforeEnter
: 当匹配到该路由时,调用的钩子。在进入特定于此记录的守卫之前。注意如果记录有重定向
属性,则beforeEnter
无效。 -
props
: 传入一个Boolean, 用来给由router-view
渲染的组件传递动态路由值。以至于不需要使用$route.params.参数
。
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]
替换成
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }]
-
meta
: 在记录上附加自定义数据。
const routes = [
{
path: '/user/:id',
component: User,
meta: {name: 'zh'}
}
]
一个简单的路由映射
import { createRouter, createWebHistory } from "vue-router"
import About from '../components/About.vue'
const Home = () => import("../components/Home.vue")
const routes = [
{ path: "/", redirect: "/home" },
{
path: "/home",
name: "home",
component: Home
},
{
path: "/about",
name: "about",
component: About
}
]
export default router = createRouter({
history: createWebHistory(),
routes: routes
})
两种路由模式
-
hash模式
{
history: createWebHashHistory()
}
-
history模式
{
history: createWebHistory()
}
内置组件
router-link
: 切换到指定的组件。
下面介绍一下他的常用属性
-
to
属性:是一个字符串,或者是一个对象
<!-- 字符串 -->
<router-link to="/home">Home</router-link>
<!-- 渲染结果 -->
<a href="/home">Home</a>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link :to="'/home'">Home</router-link>
<!-- 同上 -->
<router-link :to="{ path: '/home' }">Home</router-link>
<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: '123' }}">User</router-link>
<!-- 带查询参数,下面的结果为 `/register?plan=private` -->
<router-link :to="{ path: '/register', query: { plan: 'private' }}">
Register
</router-link>
-
replace
属性:设置 replace 属性的话,当点击时,会调用 router.replace(),而不是 router.push()。 -
active-class
属性:设置激活a元素后应用的class,默认是router-link-active。如果父路径相同就会添加给a元素这个class。 -
exact-active-class
属性:链接精准激活时,应用于渲染的<a>
的 class,默认是router-link-exact-active。只有父子路径都相同时,才会添加这个class。 -
custom
属性: 如果设置为ture, 他表示将当前router-link
包裹的元素外的a标签去除。在使用v-slot
创建自定义 RouterLink 时很有用。我们可以自定义跳转逻辑。 -
v-slot
指令: 接收作用域插槽传递的props。用于自定义router-link。 在vue-router3.x的时候,router-link有一个tag属性,可以决定router-link到底渲染成什么元素。但是在vue-router4.x开始,该属性被移除了。而给我们提供了更加具有灵活性的v-slot的方式来定制渲染的内容。
v-slot
如何使用呢?
-
首先,在
router-link
上设置custom
属性,表示我们整个元素要自定义。如果不写,那么自定义的内容会被包裹在一个 a 元素中。 - 其次,我们使用v-slot来作用域插槽来获取内部传给我们的值:
-
href:解析后的 URL。
-
route:解析后的规范化的route对象。
-
navigate:触发导航的函数。相当于
router-link
中的to
属性做的事情。跳转页面到指定路径。 -
isActive:是否匹配的状态。相当于
router-link
中的active-class
属性,如果匹配到了自动添加router-link-active
class属性。我们可以通过isActive的值来自定义active-class值。 -
isExactActive:是否是精准匹配的状态。相当于
router-link
中的exact-active-class
属性,如果匹配到了自动添加router-link-exact-active
class属性。我们可以通过isExactActive的值来自定义exact-active-class值。
-
<router-link
to="/home"
v-slot="props"
custom
>
<button @click="props.navigate">{{props.href}} 跳转到home</button>
<button @click="props.navigate">跳转到home, 可以将props.navigate添加到多个元素上</button>
<span :class="{'active': props.isActive}">{{props.isActive}}</span>
<span :class="{'active': props.isActive}">{{props.isExactActive}}</span>
<!-- <p>{{props.route}}</p> -->
</router-link>
router-view
: 切换路径,他就自动匹配到对应的组件。
-
name
: 当我们需要多个视图,而不是嵌套展示。我们就可以通过name属性来匹配到route中提供的components
组件。 -
v-slot
指令: 接收作用域插槽传递的props。可以用于 和 组件来包裹你的路由组件。
其中提供两个props:
-
Component:要渲染的组件。即当前路由匹配到的组件。
<router-view v-slot="{ Component, route }">
<transition>
<keep-alive>
<component
:is="Component"
/>
</keep-alive>
</transition>
</router-view>
-
route:解析出的标准化路由对象。即当前的路由对象。
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。也可以提高首屏的渲染效率。
其实路由懒加载就是给component属性传入一个工厂函数,并且返回一个promise对象。
component: () => import("../pages/home.vue")
动态路由
记得当时学习vue2的时候,了解到动态路由一头雾水。其实我们可以把它理解为为url传递参数,并在页面中获取到该参数,做一些事情。
很多时候我们需要将给定匹配模式的路由映射到同一个组件:例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但是用户的ID是不同的。在Vue Router中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数。
那么在User中如何获取到对应的值呢?
-
在template中,直接通过 $route.params获取值。
-
在created中,通过 this.$route.params获取值。
-
在setup中,我们要使用 vue-router库给我们提供的一个hook
useRoute
。该Hook会返回一个Route对象,对象中保存着当前路由相关的值。我们也可以在同一个路由中设置有多个 _路径参数_,它们会映射到
$route.params
上的相应字段。$route.params
上保存的就是动态参数对象。
错误页面处理
当我们访问没有路由映射的页面时,我们需要给用户一些提示。
对于哪些没有匹配到的路由,我们通常会匹配到固定的某个页面。比如NotFound的错误页面中,这个时候我们可编写一个根路径下的动态路由用于匹配所有的页面。我们可以通过 $route.params.pathMatch
获取到传入的参数。
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
{ path: '/:pathMatch(.*)', name: 'NotFound', component: NotFound }
嵌套路由
有时候,一个页面也可能出现多个组件间相互切换,我们就可以定义子路由来匹配到这个组件。注意:我们也需要在对应的位置设置router-view
来渲染子路由匹配到的组件。
如何来定义子路由呢? 我们只需要在路由映射对象中定义一个children属性然后来定义子路由的映射即可。但是有一些注意事项:
-
path
属性不需要加/
, 如果加上了/
,它将映射的是一级路由。 -
重定向传入的路由是完整的
href
。
{
path: "/home",
name: "home",
component: () => import("../pages/Home.vue"),
children: [
{
path: "",
redirect: "/home/message"
},
{
path: "message",
component: () => import("../pages/HomeMessage.vue")
},
{
path: "goods",
component: () => import("../pages/HomeGoods.vue")
}
]
}
路由跳转时参数传递
- 通过params, 即上面讲到的动态路由。
-
通过
$route.params
获取
-
- 通过query, 当我们跳转路由时,不管是通过
router-link
还是通过$router.push()
等API, 我们都可以传入一个query对象,并且在query对象中设置传递的参数。-
通过
$route.query
获取
-
- 通过meta, 在我们定义路由映射表的时候,我们可以在meta字段中定义一些数据。
-
通过
$route.meta
获取
-
-
通过props,在我们定义路由映射表时,我们可以传递一些props给组件。这个就可以作为组件的props。
const routes = [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
编程式导航
我们知道,切换路由我们可以通过router-link
组件来完成。但是我们大多数还是通过书写代码来切换路由的。
-
在options API中,我们可以通过vue-router提供的
this.$router
对象调用其API来切换路由。 -
在setup中,我们可以通过vue-router提供的hook
useRouter
来调用其API切换路由。
setup() {
const router = useRouter()
const routerSkip = () => {
router.push('/about')
router.replace('/about')
}
}
切换路由的API
-
push
: 这个方法会向 history 栈添加一个新的记录,可以点击回退上一个路径。 -
replace
: 它在导航时不会向 history 添加新记录。取代当前的路径。 -
go
: 该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步。 -
back
: 在历史堆栈中后退一步。相当于go(-1)
。 -
forward
: 在历史堆栈中前进一步。相当于go(1)
。router.push
,router.replace
和router.go
是window.history.pushState
、window.history.replaceState
和window.history.go
的翻版,它们确实模仿了window.history
的 API。
注意:前两个API传入的参数和router-link
中to属性是一样的。当同时传入path
和params
,这会忽略params
动态添加路由
某些情况下我们可能需要动态的来添加路由。比如根据用户不同的权限,注册不同的路由。这个时候我们可以使用一个方法 addRoute
-
添加一级路由。我们只需要向
addRoute
中添加一个参数。 如果路由有一个name
字段,并且已经有一个与之名字相同的路由,它会先删除之前的路由。
const categoryRoute = {
path: "/category",
component: () => import("../pages/Category.vue")
}
router.addRoute(categoryRoute);
-
添加二级路由。我们需要传入当前路由(即路由的name字段)和添加的子路由。
router.addRoute("home", {
path: "moment",
component: () => import("../pages/HomeMoment.vue")
})
动态删除路由
与动态添加路由对应。
删除路由有以下三种方式:
导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。在跳转之前,之后等状态时,做一些逻辑处理。类似于Express, Koa中的中间件。
下面我们来讲一讲全局前置守卫router.beforeEach
。其他的守卫用法一致,只是调用时机不同而已。
它有三个参数:
-
to
:即将进入的路由Route对象。 -
from
:即将离开的路由Route对象。 -
next
(可选):允许路由正常跳转。在vue-router3.x中,他是必传的,但是在4.x官方已不再建议使用。当不满足条件时,我们可以通过返回一个路径字符串或者一个对象来指定跳转。满足条件时,他会自动跳转。
它有返回值:
-
false:取消当前导航跳转。
-
不返回或者undefined:进行默认导航跳转。
-
返回一个路由地址。可以是一个string类型的路径,也可以是一个对象,对象中包含path、query、params等信息。 下面这个例子,就是当用户未登录的时候,不管访问任何页面,我们都让其跳转到login页面。
router.beforeEach((to, from) => {
if (to.path !== "/login") {
const token = window.localStorage.getItem("token");
if (!token) {
return "/login"
}
}
})
其他的导航守卫,请访问官网:next.router.vuejs.org/zh/guide/ad…
完整的导航解析流程
-
导航被触发。
-
在失活的组件里调用
beforeRouteLeave
守卫。 -
调用全局的
beforeEach
守卫。 -
在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 -
在路由配置里调用
beforeEnter
。 -
解析异步路由组件。
-
在被激活的组件里调用
beforeRouteEnter
。 -
调用全局的
beforeResolve
守卫(2.5+)。 -
导航被确认。
-
调用全局的
afterEach
钩子。 -
触发 DOM 更新。
-
调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。