vue3学习 --- vue-router

353 阅读9分钟

前端路由

路由本质上就是一个映射表

这个映射表中维护了路径地址组件之间的映射关系

路由发展

后端路由

早期的网站开发整个HTML页面是由服务器来渲染

  • 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示

  • 此时每一个页面有自己对应的网址, 也就是URL

  • URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理

  • Controller进行各种处理, 最终生成HTML或者数据, 返回给前

上面的这种操作,当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端

此时这些路由维护的是URL页面之间的关系,所以被称之为后端路由

优点: 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化

缺点:

  1. 整个页面的各个模块都需要后端人员来编写和维护
  2. HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情
前后端分离

前端渲染

  • 随着Ajax的出现, 有了前后端分离的开发模式

  • 每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS

    然后在前端对这些请求回来的资源进行渲染

  • 客户端的每一次请求,都会从静态资源服务器请求文件

  • 和之前的后端路由不同, 此时的后端只负责提供API (只负责提供数据)

  • 前端通过Ajax获取数据,并且可以通过JavaScript将数据渲染到页面中

优点:

  1. 前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上
  2. 当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套API即可

缺点:

  1. 需要频繁操作DOM
  2. 不利于SEO
  3. 在整个过程中,后端存在一些页面的模板(模板引擎),前端会请求模板和数据,再将数据填充到模板上,展示给用户
  4. 这个过程中,路由依旧是后端控制的
单页面富应用
  • SPA (single page application 单页面富应用)整个过程中,只有一个页面

  • 此时的路由维护的是url组件之间的映射关系

  • 当路由发生改变的时候,前端会自动去加载对应的组件模块

    前端只需要向后端去请求所需要的数据即可,真正意义上做到了前后端分离

此时的路由完全由前端来进行控制,所以此时的路由被称之为前端路由

所谓前端路由本质上就是通过某种方式改变url路径,从而显示不同的内容,但是在url发生改变的过程中,页面不会发生刷新操作

路由实现方法

  • URL的hash
    • URL的hash也就是锚点(#), 本质上是改变window.location的href属性

    • 可以通过直接赋值location.hash来改变href, 但是页面不发生刷新

    • hash的优势就是兼容性更好,在老版IE中都可以运行

    • 但是缺陷是URL中有一个#,显得不像一个真实的路径

  • html5的history
    • history接口是HTML5新增的
    • 使用history模式的页面,必须使用服务器打开,不支持file协议开头的URL
    • 它有六种模式改变URL而不刷新页面
方法说明
replaceState方法替换原来的路径,不会有新的浏览记录产生
pushState方法使用新的路径,相当于新增了一条新的浏览记录
popstate事件当除因history.pushState()或history.replaceState()方法触发的活动历史记录条目更改时,将触发popstate事件
go方法向前或向后改变路径, 参数为数字,可以跳转到任何的页面
如果参数是0,表示的是刷新当前页面
如果前进和回退的数值大于现有的历史记录的数值时,不会进行任何的操作,也不报错
forward方法向前改变路径,相当于go(1)
back方法向后改变路径,相当于go(-1)

hash

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>URL的hash</title>
</head>
<body>
  <div id="app">
    <!-- 路由 -->
    <a href="#/home">Home</a>
    <a href="#/about">About</a>

    <!-- 占位符 -->
    <div id="content">Home</div>
  </div>

  <script>
    const contentElem = document.getElementById('content')

    // 监听路由改变函数 --- 路由改变的时候,页面是不会刷新的
    window.addEventListener('hashchange', () => {
      // location.hash --- 可以获取到hash值,获取到的值是带#的
      switch(location.hash) {
        case '#/home':
          contentElem.innerHTML = 'Home'
          break

        case '#/about':
          contentElem.innerHTML = 'About'
          break
      }
    })
  </script>
</body>
</html>

history

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <!-- 路由 -->
    <a href="/home">Home</a>
    <a href="/about">About</a>

    <!-- 占位符 -->
    <div id="content">Home</div>
  </div>

  <script>
    const contentElem = document.getElementById('content')

    const aEls = document.getElementsByTagName('a')

    const changeRouter = () => {
      switch(location.pathname) {
        case '/home':
          contentElem.innerHTML = 'Home'
          break

        case '/about':
          contentElem.innerHTML = 'About'
          break
      }
    }

    for (const aEl of aEls) {
      aEl.addEventListener('click', e => {
        // 阻止默认行为
        e.preventDefault()

        // 设置历史记录,并修改url --- 不会发生跳转
        /*
        	有三个参数:
        	1. 一个对象,可以用于存储一些需要在多个页面中传递的数据,
        		 可以使用history.state来获取,初始值是null, 
        		 在使用pusState或replaceState后会变为所传入的值
        		 
        	2. 新页面的title,目前浏览器都不支持,保留属性
        	3. 新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址
                   (对应的拼接规则和URL的拼接规则一致)
        */
        history.pushState({}, '', aEl.getAttribute('href'))
        changeRouter()
      })
    }

    // 点击回退键和前进键的时候, 也需要重新渲染对应的内容
    /*
      1. 当活动历史记录条目更改时,将触发popstate事件
      2. 事件名都是小写的
      3. 这个事件是在window上的
    */
    window.addEventListener('popstate', changeRouter)
  </script>
</body>
</html>

vue-router

目前前端流行的三大框架, 都有自己的路由实现:

  • Angular的ngRouter
  • React的ReactRouter
  • Vue的vue-router

安装

# 安装 --- 目前默认安装的是3.x的版本
npm i vue-router

# 最新版的是4.X的版本
npm i vue-router@4

初体验

使用vue-router的步骤:

  • 第一步:创建路由组件的组件;
  • 第二步:配置路由映射: 组件和路径映射关系的routes数组;
  • 第三步:通过createRouter创建路由对象,并且传入routes和history模式;
  • 第四步:使用路由: 通过<router-link><router-view>

一般我们的路由都会存放于src/route文件夹中

一般我们的路由组件会存放于src/pages/src/views文件夹中

src/routes/index.js

import Home from '../pages/Home.vue'
import About from '../pages/About.vue'

import {
  createRouter,
  createWebHistory, // 如果需要使用history模式调用这个方法
  createWebHashHistory // 如果需要hash模式调用这个方法
} from 'vue-router'

// 定义映射表 --- 路由表  --- Route[]
const routes = [
  {
    path: '/home', // 路径
    component: Home // 组件
  },

  {
    path: '/about',
    component: About
  }
]

// 定义路由对象,用以存放路由表 -- 路由器
export default createRouter({
  routes, // 路由表
  history: createWebHistory()
})

src/main.js

import { createApp } from 'vue'
import App from './App.vue'

// 引入路由器 --- 可以将路由器看成vue的一个插件来进行使用
import router from './routes'

// use方法用于注册插件,返回值也是一个app对象
createApp(App).use(router).mount('#app')

App.vue

<template>
  <div>
    <!--
      router-link是vue-router提供的内置组件
      router-link可以用来进行路由的跳转
    -->
    <router-link to="/home">Home</router-link>
    <router-link to="/about">About</router-link>

    <!--
      router-view是vue-router提供的内置组件
      可以用来作为需要展示内容的占位符
    -->
    <router-view />
  </div>
</template>

默认路径

默认情况下, 进入网站的首页, 我们多希望<router-view>渲染首页的内容

实现方式1

const routes = [
  { // 主页面匹配显示的内容设置为Home组件
    path: '/',
    component: Home
  },

  {
    path: '/home',
    component: Home
  },

  {
    path: '/about',
    component: About
  }
]

实现方式2

const routes = [
  { // 如果是主页,自动跳转为/home
    //此时路径就为/home,所以可以匹配显示Home组件
    path: '/',
    redirect: '/home'
  },

  {
    path: '/home',
    component: Home
  },

  {
    path: '/about',
    component: About
  }
]

router-link的属性

属性说明
to跳转的路径
可以是一个字符串,或者是一个对象
replace设置 replace 属性的话,当点击时,会调用 router.replace(),而不是 router.push()
active-class设置激活a元素后应用的class
当路由部分配对上的时候,就可以存在router-link-active
默认是router-link-active
exact-active-class链接精准激活时,应用于渲染的 的 class
当路由完全配对上的时候,才会存在router-link-exact-active
默认是router-link-exact-active
<!-- replace的使用 -->
<router-link to="/home" replace>Home</router-link>
<router-link to="/about" replace>About</router-link>
<!--
  此时被激活的路由上有一个名为router-active的样式
  默认值为router-link-active
-->
<router-link to="/home" active-class="router-active">Home</router-link>
<router-link to="/about" active-class="router-active">About</router-link>

exact-active-classactive-class之间的区别

假设有以下路由配置

import {
  createRouter,
  createWebHistory,
} from 'vue-router'

const routes = [
  {
    path: '/about',
    component: () => import('../pages/About.vue'),

    children: [

      {
        path: 'info',
        component: () => import('../pages/Info.vue')
      }
    ]
  }
]

export default createRouter({
  routes,
  history: createWebHistory()
})

有以下2个router-link

<router-link to="/about">About</router-link>

<router-link to="/about/info">About</router-link>

当路由为/about

<!-- 上边存在的样式为 router-link-active 和 router-link-exact-active -->
<router-link to="/about">About</router-link>

<!-- 上边存在的样式为 空 -->
<router-link to="/about/info">About</router-link>

当路由为/about/info

<!-- 上边存在的样式为 router-link-active -->
<router-link to="/about">About</router-link>

<!-- 上边存在的样式为 router-link-active 和 router-link-exact-active -->
<router-link to="/about/info">About</router-link>

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载

我们需要能把不同路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应组件,这样就会更加高效,也可以提高首屏的渲染效率

此时就需要路由懒加载, Vue Router默认就支持动态来导入组件

{
  path: '/home',
  // component可以传入一个组件,也可以接收一个函数,该函数需要放回一个Promise
  // import函数本身返回的就是一个promise
  component: () => import('../pages/Home.vue')
},

{
  path: '/about',
  // 默认分包出来的文件夹名为chunk-[hash].js
  // 如果需要手动指定打包出来的chunk的名称,可以使用魔法注释手动指定(webpack会去识别并进行解析)
  component: () => import(/* webpackChunkName: "home-chunk" */'../pages/Home.vue')
}

route的其它属性

  {
    path: '/home',
    name: 'home', // 路由记录独一无二的名称
    meta: {}, // 自定义的数据
    component: () => import('../pages/Home.vue')
  }

动态路由

很多时候我们需要将给定匹配模式的路由映射到同一个组件

  • 我们可能有一个 User 组件,它应该对所有用户进行渲染,但是用户的ID是不同的
  • 在Vue Router中,我们可以在路径中使用一个动态字段(变量)来实现,我们称之为 路径参数(params参数)
  • 在一个路径中 路径参数可以是多个

路由

 {
    // 使用路径参数 --- 路径参数可以使用多个
    path: '/user/:name/id/:id',
    component: () => import('../pages/User.vue')
  }

router-link

<router-link to="/user/coderwxf/id/1810100?foo=baz">User</router-link>

user.vue

<template>
  <div>
    User
    <p>name: {{ $route.params.name }}</p>
    <p>id: {{ $route.params.id }}</p>
    <p>query: {{ $route.query.foo }}</p>
  </div>
</template>

<script>
// vue-router4.x 才有的方法
import { useRoute } from 'vue-router'

export default {
  name: 'User',

  created() {
    // 在options api中可以通过this.$route来获取 当前路由对象
    // this.$route.query --- 查询参数 --- 默认 {}
    // this.$route.params --- 路径参数 --- 默认 {}
    console.log(this.$route)
  },

  setup() {
    // useRoute返回route对象, 对象中保存着当前路由相关的值
    // 使用方法和options api中的$route完全一致
    const route = useRoute()
    console.log(route)
  }
}
</script>

NotFound

对于哪些没有匹配到的路由,我们通常会匹配到固定的某个页面, 比如NotFound的错误页面

// 当页面路径不匹配的时候,页面中对应的router-view显示404组件(notFound组件)
// (.*)表示前边pageMatch可以匹配的值是任意字符组成的字符串 ---- 在括号中书写正则
// pageMatch --- 只是一个形参 名称可以任意
{
  path: '/:pageMatch(.*)',
  component: () => import('../pages/NotFound.vue')
}

NotFound.vue

<template>
  <div>
    <h2>Not Found</h2>
    <!-- pageMatch这里的参数取决于路由中path中的路径参数所设置的名称 -->
    {{ $route.params.pageMatch }}
  </div>
</template>
// 最后一个*表示将获取到的查询字符串按照/来进行拆分
// 有以下一个例子
// 不加*的时候 --- home/config/user/info
// 加上了* --- [ "home", "config", "user", "info" ]
{
  path: '/:pageMatch(.*)*',
  component: () => import('../pages/NotFound.vue')
}

路由的嵌套

我们在页面中直接点击的路由,一般被称之为一级路由,例如\home

但是我们依据路由显示的组件内部其实依旧是可以使用我们的路由来进行显示不同的组件,这种路由被称之为二级路由,例如\home\config

router.js

  {
    path: '/home',
    component: () => import('../pages/Home.vue'),

    // 在children中可以配置我们的二级路由
    children: [
      {
        // 默认的/home 重定向到/home/config
        // 注意: 这里的路径需要写绝对路径 因为是 /home -> /home/config
        path: '',
        redirect: '/home/config'
      },

      {
        path: 'config',
        component: () => import('../pages/Config.vue')
      },

      {
        path: 'version',
        component: () => import('../pages/Version.vue')
      }
    ]
  }

Home.vue

<template>
  <div>
    <router-link to="/home/config">config</router-link>
    <router-link to="/home/version">version</router-link>

    <!-- 占位符 -->
    <router-view />
  </div>
</template>
路由的替换规则
{
    path: '/home',
    component: () => import('../pages/Home.vue'),

    children: [
      {
        // 路径会被转换为 /home/config
        path: 'config',
        component: () => import('../pages/Config.vue')
      },

      {
         // 路径会被转换为 /config
        path: '/config',
        component: () => import('../pages/Config.vue')
      },

     {
        // 路径会被转换为 /home/config
        path: '/home/config',
        component: () => import('../pages/Config.vue')
      },
    ]
  }
<!-- 假设当前网址为 http://www.example.com/home/config --> 

<!-- 当前路径会被替换为: http://www.example.com/home/version -->
<router-link to="version">version</router-link>

<!-- 当前路径会被替换为: http://www.example.com/version -->
<router-link to="/version">version</router-link>

<!-- 当前路径会被替换为: http://www.example.com/home/version -->
<router-link to="/home/version">version</router-link>

编程式路由跳转

有时候我们希望通过代码来完成页面的跳转,比如点击的是一个按钮

此时我们可以不通过router-link组件来帮助我们完成,而是通过js代码的方式来进行实现

<router-link>的方式定义的路由叫做 --- 声明式路由

使用js方式进行的路由跳转 --- 编程式路由

<template>
  <div>
    <button @click="goAbout">点我显示到About组件</button>
    <button @click="gotoAbout">点我显示到About组件</button>

    <router-view />
  </div>
</template>

<script>
import { useRouter } from 'vue-router'

export default {
  name: 'App',

  methods: {
    goAbout() {
      // options api
      this.$router.push('/about')
    }
  },

  setup() {
    // composition api

    // tips: useRouter和useRoute函数,必须在setup顶层作用域中被创建
    // 无法在函数中被创建
    
    // useRouter函数返回的router对象和options api中$router的用法是一模一样的
    const router = useRouter()

    const gotoAbout = () => {
      router.push('/about')
    }

    return {
      gotoAbout
    }
  }
}
</script>

参数补充

// 最终生成的跳转路径 ==> /about?name=Klaus&age=24
router.push({
  path: '/about',
  query: {
    name: 'Klaus',
    age: 24
  }
})

其实, router-link也是可以直接传递对象作为参数

<router-link :to="{
   path: '/about',
   query: {
     name: 'Klaus',
     age: 24
   }
}">
  About
</router-link>

无论是在hash模式下 还是 在 history模式下,

H5中的history对象上的所有方法,在router对象上都是可以被使用的

// replaceState
router.replace('/about') // 相对于 <router-link to="/about" replace> ... </router-link>

// go  --- 没有传递任何的参数的时候,默认参数为0
// 如果跳转的路径超出历史记录的调试的时候 --- 静默失败 --- 不报任何的错误且没有任何的效果
router.go(1)
router.go(0)
router.go(-1)

// back === go(-1)
router.back()

// forward === go(1)
router.forward()

Tips:

hash模式下 根路径会被转换为http://localhost:8080/#/

history模式下,根路径会被转换为http://localhost:8080/

推荐在开发中使用history模式,因为这样更贴近于普通的URL路径