Skip to content

路由

在传统的 Web 开发过程中,当需要实现多个站内页面时,要写很多个 HTML 页面,然后通过 <a /> 标签来实现互相跳转。

在如今工程化模式下的前端开发,像 Vue 工程,可以轻松实现只用一个 HTML 文件就完成多个站内页面渲染、跳转的功能,这就是路由。

路由的目录结构

路由的管理需要放在src/router这个目录下:

powershell
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 文件里。

路由模式

可通过createWebHashHistorycreateWebHistory将路由配置为 hash 或者 historyHash 模式是 http://abc.com/#/home 这种带 # 号的地址,支持所有浏览器, History 模式是 http://abc.com/home 这样不带 # 号的,不仅美观,而且体验更好,但需要服务端做一些配置支持,也只对主流浏览器提供支持。

路由配置

最基本的路由配置应该包含以下三个字段:

typescript
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 接收一个变量,变量的值就是对应的模板组件。

在打包的时候,组件的所有代码都会被打包到一个文件里,对于大项目来说,这种方式的首屏加载是个灾难,要面对文件过大导致等待时间变长的问题。

typescript
import Home from '@views/home.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: Home,
  },
]

2、异步方式

字段 component 接收一个函数,在 return 的时候返回模板组件,同时组件里的代码在打包的时候都会生成独立的文件,并在访问到对应路由的时候按需引入。

typescript
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: () => import('@views/home.vue'),
  },
]

我们现在推荐使用这方式,可以实现 路由懒加载

路由懒加载

在上面提过,路由在配置同步方式的时候,构建出来的文件都集中在一起,大的项目的文件会变得非常大,影响页面加载。

所以我们更推荐使用异步方式,可以把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样按需载入,很方便地实现路由组件的懒加载。

在这一段配置里面:

typescript
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: () => import('@views/home.vue'),
  },
]

起到懒加载配置作用的就是 component 接收的值 () => import('@views/home.vue') ,其中 @views/home.vue 就是路由的组件。

在命令行运行 npm run build 打包构建后,会看到控制台输出的打包结果:

powershell
❯ 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 只会根据当前路由加载需要的文件,其他文件只做预加载,对于大型项目的访问体验非常友好。

而如果不使用路由懒加载,打包出来的文件是这样的:

powershell
❯ 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 /> ,整个页面就是路由组件。

vue
<template>
  <router-view />
</template>

如果站点带有全局公共组件,比如有全站统一的页头、页脚,只有中间区域才是路由,那么可以这样配置:

vue
<template>
  <!-- 全局页头 -->
  <Header />

  <!-- 路由 -->
  <router-view />

  <!-- 全局页脚 -->
  <Footer />
</template>

如果有一部分路由带公共组件,一部分没有,比如大部分页面都需要有侧边栏,但登录页、注册页不需要,就可以这么处理:

vue
<template>
  <!-- 登录 -->
  <Login v-if="route.name === 'login'" />

  <!-- 注册 -->
  <Register v-else-if="route.name === 'register'" />

  <!-- 带有侧边栏的其他路由 -->
  <div v-else>
    <!-- 固定在左侧的侧边栏 -->
    <Sidebar />

    <!-- 路由 -->
    <router-view />
  </div>
</template>

路由跳转与传参

路由跳转

vue
<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>

路由传参

路由传参有queryparams两种方式。

  • query?形式传参,如 /user?id=1&name=tom&gender=male

  • params通过:形式绑定参数传参,如 /user/:id/:name/:gender--> /user/1/tom/male

路由传参用queryparam都可以。采用params方式进行路由传参的url比较美观。

目标路由若想接收路由参数,需要导入useRoute

*import* { useRoute } *from* 'vue-router'

路由传参示例

定义路由

typescript
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')
    }
  ]
})

跳转页

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接收页

vue
<template>
    <div>我是第二页:{{ route.query.name }}</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';

const route = useRoute();

</script>

params接收页

vue
<template>
    <div>我是第三页:{{ route.params.name }}</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';

const route = useRoute();

</script>

导航守卫

导航守卫,也叫路由守卫。其实就是几个专属的钩子函数,先来看一下使用场景,大致理解一下基本概念和作用。

钩子的应用场景

对于导航守卫还不熟悉的开发者,可以从一些实际使用场景来加强印象,比如:

  1. 前面说的,在渲染的时候配置浏览器标题,由于 Vue 项目只有一个 HTML 文件,所以默认只有一个标题,但想在访问 /home 的时候标题显示为 “首页”,访问 /about 的时候标题显示为 “关于” 。
  2. 带权限的页面,部分页面需要管理员才能访问,普通用户不允许进入到该路由页面。
  3. 当前系统需要登录,禁止未登录的时候访问网站的页面资源
常用钩子含义触发时机
beforeEach全局前置守卫在路由跳转前触发
beforeResolve全局解析守卫在导航被确认前,同时在组件内守卫和异步路由组件被解析后
afterEach全局后置守卫在路由跳转完成后触发

使用钩子

typescript
import { createRouter } from 'vue-router'

// 创建路由
const router = createRouter({ ... })

// 在这里调用导航守卫的钩子函数
router.beforeEach((to, from) => {
  // ...
})

// 导出去
export default router

beforeEach

全局前置守卫,这是导航守卫里面运用的最多的一个钩子函数,通常将其称为 “路由拦截”。

拦截这个词,顾名思义,就是在 XXX 目的达到之前,把它拦下来,所以路由的目的就是渲染指定的组件,路由拦截就是在组件被渲染之前,做一些拦截操作。

参数

参数作用
to即将要进入的路由对象
from当前导航正要离开的路由

用法

比如在进入路由之前,判断是否需要登录:

typescript
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 官网的申请照相机权限的例子来举例:

typescript
// 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数据,类似这种每个路由都要执行一次,但又不必在渲染前操作的,都可以放到后置钩子里去执行。

typescript
router.afterEach((to, from) => {
  // 上报流量的操作
  // ...
})