路由
在传统的 Web 开发过程中,当需要实现多个站内页面时,要写很多个 HTML 页面,然后通过 <a /> 标签来实现互相跳转。
在如今工程化模式下的前端开发,像 Vue 工程,可以轻松实现只用一个 HTML 文件就完成多个站内页面渲染、跳转的功能,这就是路由。
路由的目录结构
路由的管理需要放在src/router这个目录下:
src
│ # 路由目录
├─router
│ # 路由入口文件
├───index.ts
│ # 路由配置,如果路由很多,可以再拆分模块文件
├───routes.ts
│ # 项目入口文件
└─main.ts其中 index.ts 是路由的入口文件,如果路由很少,那么可以只维护在这个文件里,但对复杂项目来说,往往需要配置二级、三级路由,把逻辑和配置都放到一个文件的话太臃肿了。
所以如果项目稍微复杂一些,可以像上面这个结构一样拆出两个文件: index.ts 和 routes.ts ,在 routes.ts 里维护路由树的结构,在 index.ts 导入路由树结构并激活路由,同时可以在该文件里配置路由钩子。
如果项目更加复杂,例如做一个 Admin 后台,可以按照业务模块,再把routes拆分得更细,例如 game.ts / member.ts / order.ts 等业务模块,再统一导入到 index.ts 文件里。
路由模式
可通过createWebHashHistory和createWebHistory将路由配置为 hash 或者 history , Hash 模式是 http://abc.com/#/home 这种带 # 号的地址,支持所有浏览器, History 模式是 http://abc.com/home 这样不带 # 号的,不仅美观,而且体验更好,但需要服务端做一些配置支持,也只对主流浏览器提供支持。
路由配置
最基本的路由配置应该包含以下三个字段:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('@views/home.vue'),
},
]其中 path 是路由的访问路径,如果的域名是 https://test.com, 配置为 /home,那么访问路径就是 https://test.com/home
name 是路由的名称,非必填,但是一般都会配置上去,这样可以很方便地用 name 来代替 path 实现路由的跳转,例如有时候开发环境和生产环境的路径不一致,或者说路径变更,通过 name 无需调整,但如果通过 path,可能就要修改很多文件里面的链接跳转目标了。
component 是路由的模板文件,指向一个vue组件,用于指定路由在浏览器端的视图渲染,这里有两种方式来指定使用哪个组件:
1、同步方式
字段 component 接收一个变量,变量的值就是对应的模板组件。
在打包的时候,组件的所有代码都会被打包到一个文件里,对于大项目来说,这种方式的首屏加载是个灾难,要面对文件过大导致等待时间变长的问题。
import Home from '@views/home.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: Home,
},
]2、异步方式
字段 component 接收一个函数,在 return 的时候返回模板组件,同时组件里的代码在打包的时候都会生成独立的文件,并在访问到对应路由的时候按需引入。
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('@views/home.vue'),
},
]我们现在推荐使用这方式,可以实现 路由懒加载 。
路由懒加载
在上面提过,路由在配置同步方式的时候,构建出来的文件都集中在一起,大的项目的文件会变得非常大,影响页面加载。
所以我们更推荐使用异步方式,可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样按需载入,很方便地实现路由组件的懒加载。
在这一段配置里面:
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
component: () => import('@views/home.vue'),
},
]起到懒加载配置作用的就是 component 接收的值 () => import('@views/home.vue') ,其中 @views/home.vue 就是路由的组件。
在命令行运行 npm run build 打包构建后,会看到控制台输出的打包结果:
❯ npm run build
> vue-tsc --noEmit && vite build
vite v2.9.15 building for production...
✓ 42 modules transformed.
dist/index.html 0.42 KiB
dist/assets/home.03ad1823.js 0.65 KiB / gzip: 0.42 KiB
dist/assets/HelloWorld.1322d484.js 1.88 KiB / gzip: 0.96 KiB
dist/assets/about.c2af6d65.js 0.64 KiB / gzip: 0.41 KiB
dist/assets/login.e9d1d9f9.js 0.65 KiB / gzip: 0.42 KiB
dist/assets/index.60726771.css 0.47 KiB / gzip: 0.29 KiB
dist/assets/login.bef803dc.css 0.12 KiB / gzip: 0.10 KiB
dist/assets/HelloWorld.b2638077.css 0.38 KiB / gzip: 0.19 KiB
dist/assets/home.ea56cd55.css 0.12 KiB / gzip: 0.10 KiB
dist/assets/about.a0917080.css 0.12 KiB / gzip: 0.10 KiB
dist/assets/index.19d6fb3b.js 79.94 KiB / gzip: 31.71 KiB此时路由文件都会按照 views 目录下的路由组件和 components 目录下的组件命名,输出了对应的 JS 文件和 CSS 文件,项目部署后, Vue 只会根据当前路由加载需要的文件,其他文件只做预加载,对于大型项目的访问体验非常友好。
而如果不使用路由懒加载,打包出来的文件是这样的:
❯ npm run build
> vue-tsc --noEmit && vite build
vite v2.9.15 building for production...
✓ 41 modules transformed.
dist/index.html 0.42 KiB
dist/assets/index.67b1ee4f.css 1.22 KiB / gzip: 0.49 KiB
dist/assets/index.f758ee53.js 78.85 KiB / gzip: 31.05 KiB所有的组件都会被打包成了一个很大的 JS 文件和 CSS 文件,没有进行代码分割,对大型项目来说,这种方式打包出来的文件可能会有好几兆,首屏加载的速度可想而知。
路由渲染
编写好路由的配置文件之后,要在访问后进行渲染路由对应的组件,必须在父级组件里带有 <router-view /> 标签。
<router-view /> 在哪里,路由组件的代码就渲染在哪个节点上,一级路由的父级组件,就是 src/App.vue 这个根组件。
其中最基础的配置就是 <template /> 里面直接就是写一个 <router-view /> ,整个页面就是路由组件。
<template>
<router-view />
</template>如果站点带有全局公共组件,比如有全站统一的页头、页脚,只有中间区域才是路由,那么可以这样配置:
<template>
<!-- 全局页头 -->
<Header />
<!-- 路由 -->
<router-view />
<!-- 全局页脚 -->
<Footer />
</template>如果有一部分路由带公共组件,一部分没有,比如大部分页面都需要有侧边栏,但登录页、注册页不需要,就可以这么处理:
<template>
<!-- 登录 -->
<Login v-if="route.name === 'login'" />
<!-- 注册 -->
<Register v-else-if="route.name === 'register'" />
<!-- 带有侧边栏的其他路由 -->
<div v-else>
<!-- 固定在左侧的侧边栏 -->
<Sidebar />
<!-- 路由 -->
<router-view />
</div>
</template>路由跳转与传参
路由跳转
<template>
<div>我是第一页</div>
<button @click="onClick">去第二页</button>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router"; //先导入
const router = useRouter(); //创建一个实例
const onClick = () => {
router.push({
name: 'test2' //指定一个path也可以,但是更推荐使用name
})
}
</script>路由传参
路由传参有query和params两种方式。
query以?形式传参,如/user?id=1&name=tom&gender=maleparams通过:形式绑定参数传参,如/user/:id/:name/:gender--> /user/1/tom/male
路由传参用query或param都可以。采用params方式进行路由传参的url比较美观。
目标路由若想接收路由参数,需要导入useRoute
*import* { useRoute } *from* 'vue-router'
路由传参示例
定义路由
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/test1',
name: 'test1',
component: () => import('../views/class4/test1.vue')
},
//query跳转目标路由
{
path: '/test2',
name: 'test2',
component: () => import('../views/class4/test2.vue')
},
//params跳转目标路由
{
path: '/test3/:name',
name: 'test3',
component: () => import('../views/class4/test3.vue')
}
]
})跳转页
<template>
<div>我是第一页</div>
<button @click="onClick1">去第二页</button>
<button @click="onClick2">去第三页</button>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
const onClick1 = () => {
router.push({
name: 'test2',
query: {
name: 'lilei'
}
})
}
const onClick2 = () => {
router.push({
name: 'test3',
params: {
name: 'hanmeimei'
}
})
}
</script>query接收页
<template>
<div>我是第二页:{{ route.query.name }}</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
const route = useRoute();
</script>params接收页
<template>
<div>我是第三页:{{ route.params.name }}</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
const route = useRoute();
</script>导航守卫
导航守卫,也叫路由守卫。其实就是几个专属的钩子函数,先来看一下使用场景,大致理解一下基本概念和作用。
钩子的应用场景
对于导航守卫还不熟悉的开发者,可以从一些实际使用场景来加强印象,比如:
- 前面说的,在渲染的时候配置浏览器标题,由于 Vue 项目只有一个 HTML 文件,所以默认只有一个标题,但想在访问
/home的时候标题显示为 “首页”,访问/about的时候标题显示为 “关于” 。 - 带权限的页面,部分页面需要管理员才能访问,普通用户不允许进入到该路由页面。
- 当前系统需要登录,禁止未登录的时候访问网站的页面资源
| 常用钩子 | 含义 | 触发时机 |
|---|---|---|
| beforeEach | 全局前置守卫 | 在路由跳转前触发 |
| beforeResolve | 全局解析守卫 | 在导航被确认前,同时在组件内守卫和异步路由组件被解析后 |
| afterEach | 全局后置守卫 | 在路由跳转完成后触发 |
使用钩子
import { createRouter } from 'vue-router'
// 创建路由
const router = createRouter({ ... })
// 在这里调用导航守卫的钩子函数
router.beforeEach((to, from) => {
// ...
})
// 导出去
export default routerbeforeEach
全局前置守卫,这是导航守卫里面运用的最多的一个钩子函数,通常将其称为 “路由拦截”。
拦截这个词,顾名思义,就是在 XXX 目的达到之前,把它拦下来,所以路由的目的就是渲染指定的组件,路由拦截就是在组件被渲染之前,做一些拦截操作。
参数
| 参数 | 作用 |
|---|---|
| to | 即将要进入的路由对象 |
| from | 当前导航正要离开的路由 |
用法
比如在进入路由之前,判断是否需要登录:
router.beforeEach((to, from) => {
console.log(to, from);
const isNoLogin = xxx;//xxx代表判断是否已登录的逻辑
if (!isNoLogin) {
return '/login'
}
})beforeResolve
全局解析守卫,它会在每次导航时触发,但是在所有组件内守卫和异步路由组件被解析之后,将在确认导航之前被调用。
这个钩子用得比较少,因为它和 beforeEach 非常相似。
它通常会用在一些申请权限的环节,比如一些 H5 页面需要申请系统相机权限、一些微信活动需要申请微信的登录信息授权,获得权限之后才允许获取接口数据和给用户更多的操作,使用 beforeEach 时机太早,使用 afterEach 又有点晚,那么这个钩子的时机就刚刚好。
参数
| 参数 | 作用 |
|---|---|
| to | 即将要进入的路由对象 |
| from | 当前导航正要离开的路由 |
用法
用以前 Vue Router 官网的申请照相机权限的例子来举例:
// https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
router.beforeResolve(async (to) => {
// 如果路由配置了必须调用相机权限
if (to.meta.requiresCamera) {
// 正常流程,咨询是否允许使用照相机
try {
await askForCameraPermission()
} catch (error) {
// 容错
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 如果出现意外,则取消导航并抛出错误
throw error
}
}
}
})afterEach
全局后置守卫,这也是导航守卫里面用得比较多的一个钩子函数。
参数
| 参数 | 作用 |
|---|---|
| to | 即将要进入的路由对象 |
| from | 当前导航正要离开的路由 |
用法
假如我们有个需求每次切换路由都上报一次PV数据,类似这种每个路由都要执行一次,但又不必在渲染前操作的,都可以放到后置钩子里去执行。
router.afterEach((to, from) => {
// 上报流量的操作
// ...
})