[TOC]

路由

image.png

路由的发展历程

后端路由阶段

image.png

前后端分离阶段

image.png

SPA(single page app) 单页面应用

之前的前后端分离,后端依然保留了大量模板代码,前端依然需要将这些模板代码请求下来,然后通过 ajax 请求将数据和模板拼成了一个完整网页。
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。不用后端,换句话说不用发送请求,前端就能完成页面的切换。
此时需要一个页面与路径的映射关系,也就是路由。因为这个映射关系维护在前端,所以叫前端路由。

前端路由的原理

无非是让 URI 发生变化,然后监听到变化,进行相应的页面展示,形成 url 和组件的隐射关系。
其中让 URI 变化有两种实现方式。

URL 的 hash

URL 的 hash 也就是锚点#后面的字符,点击锚点链接会把#hash添加到 url 后面,本质上是修改了window.locationhref属性。锚点链接可以让 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

image.png

使用 vue-router

  1. 第一步:创建路由组件的配置文件;
  2. 第二步:配置路由映射: 组件和路径映射关系的 routes 数组;
  3. 第三步:通过 createRouter() 创建路由对象,并且传入 routes 并配置路由模式: history 或 hash 模式;
  4. 第四步:在 mian.js 中注册 router 插件
  5. 第五步:使用路由: 通过<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 类会填添加到完全匹配的 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(.)*,那么将会对/`进行解析,将每个下划线分割的内容放入数组中。
image.png

路由的嵌套

什么是路由的嵌套呢?

  • 目前我们匹配的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** ```html

<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 方法的返回值回调;添加路由的方法返回值是一个函数,调用它就是删除该添加的路由
image.png
image.png
image.png
路由的其他方法补充:

  • 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
    }
  }
}