Vue Router 4 的使用,一篇文章给你讲透彻

2,915 阅读7分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

Vue 3.X 使用Vue Router 4.x 进行路由配置,本文我们就来研究下如何使用Vue Router 4.x,本文中所有的使用方式都是使用 Composition API的方式。

本文通过一步步介绍Vue Router 4.x的使用,来搭建一个简单的博客系统,让你对新版的Vue Router 4.x有一个完整的认识,然后能够非常轻松滴将Vue Router 4.x应用在自己的项目中。

项目初始化

项目搭建

项目使用vite进行创建。

npm init vite@latest vue-router-blog
npm install
npm run dev

目前安装的是Vue 3.2.25

配置vite.config.js

我们配置@别名,这样就比较方便书写引入文件的路径

// 引入文件
const path = require("path");

export default defineConfig({
  // 添加 @
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  plugins: [vue()],
});

配置jsconfig.json

jsconfig.json可以让VSCode更加智能

{
  "include": [
    "./src/**/*",
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Vue Router 4初体验

安装Vue Router 4
npm i vue-router@4

目前安装的是Vue Router 4.0.12

创建两个页面Home.vueAbout.vue
<!-- Home.vue -->
<template>
  <div>
    主页
  </div>
</template>
<!-- About.vue -->
<template>
  <div>
    关于页
  </div>
</template>

这两个页面很简单,每个页面仅仅就是显示一行文字

创建router

我们在src目录下新建router目录,在router目录下创建index.js文件, 在里面进行路由的信息配置。

import { createRouter, createWebHistory } from "vue-router";

// 引入
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";

// 路由信息
let routes = [
  {
    path: "/",
    name: 'home',
    component: Home,
  },
  {
    path: "/about",
    name: 'about',
    component: About,
  },
];

// 路由器
const router = createRouter({
  history: createWebHistory(), // HTML5模式
  routes,
});

export default router;

安装router

将路由安装router安装到app上。

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

// 引入插件
import router from "@/store/index";
// 安装router插件
createApp(App).use(router).mount('#app')
使用 router-linkrouter-view

修改App.vue

<template>
  <img alt="Vue logo" src="./assets/logo.png" /><br />
  <div>
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link><br />
  </div>
  <router-view></router-view>
</template>

至此,我们的就实现了页面间的切换功能了。

效果

几个重要的概念

router-link组件和a标签的区别?

router-link组件底层也是渲染的a标签,但是router-link的页面切换只是更新了页面的部分内容,不会进行整个页面的刷新,而a标签跳转(例如:<a href="/about">调到Home标签</a><br>)是对整个页面进行刷新。

底层原理是router-link劫持了元素的点击事件,添加了处理页面更新的逻辑。

Hash模式和HTML5模式的区别?

Hash模式的URL中有一个#号,eg:http://localhost:3000/#/about, #号后面的就是Hash地址,这个模式以前是SPA的常用模式,但是链接有一个#号比较丑。

HTML5模式和正常的链接地址一样的,eg:http://localhost:3000/about, 这个地址很优雅,但是有一个问题,需要服务器支持。 原因是浏览器中输入http://localhost:3000/about支持,服务器以为要访问根路劲下的about目录的HTML文件,而不是访问根路劲下的HTML文件。

webpackvite启动的服务器是支持HTML5模式的,所以开发环境使用HTML5模式没有问题。

router-link组件和router-view组件为什么能直接使用?

安装router插件的时候注册了这两个全局组件,所以能直接使用。

install(app: App) {
    app.component('RouterLink', RouterLink)
    app.component('RouterView', RouterView)
}

路由懒加载

上面写法有一个严重的问题,router中所有的组件都会被一次加载。我们的例子中就是 HomeAbout组件,即使有时候不会用到About组件, 也要加载,这对首页的显示会有很大的影响。

改造如下:

<!--// 删除 import Home from "@/views/Home.vue";-->
<!--// 删除 import About from "@/views/About.vue";-->

let routes = [
  {
    path: "/",
    name: 'home',
    <!--// 改成如下的写法-->
    component: () => import("@/views/Home.vue"),
  },
  {
    path: "/about",
    name: 'about',
    <!--// 改成如下的写法-->
    component: () => import("@/views/About.vue"),
  },
];

这样在开发环境中只有使用到组件才会加载进来,在生产环境中异步组件会分开文件进行打包。

修改代码(创建博客的框架)

为了方便介绍其他内容,我们修改一下代码内容:

新建模拟博客列表数据
[
  {
    "id": 1,
    "catId": 1,
    "catName": "iOS",
    "subCatId": 1,
    "subcatName": "推荐",
    "name": "RxSwift实现MVVM架构",
    "image": "https://images.xiaozhuanlan.com/photo/2018/2f5dff865155d756dfe04f2909cd1a36.png",
    "description": "在本文中,我将介绍iOS编程中的MVVM设计模式,当然还有RxSwift的介绍。本文分为两部分。在第1部分中简要介绍了RxSwift的设计模式和基础知识,在第2部分中 ,我们有一个使用RxSwift的MVVM的示例项目。"
  },
  
  //省略...
]

命名为data.json将其放置在src文件夹下

创建路由信息
// 路由信息
let routes = [
  {
    path: "/",
    name: 'home',
    component: () => import("@/views/All.vue"),
  },
  {
    path: "/ios",
    name: 'ios',
    component: () => import("@/views/iOS.vue"),
  },
  {
    path: "/android",
    name: 'android',
    component: () => import("@/views/Android.vue"),
  },
  {
    path: "/flutter",
    name: 'flutter',
    component: () => import("@/views/Flutter.vue"),
  },
  {
    path: "/web",
    name: 'web',
    component: () => import("@/views/Web.vue"),
  },
];

设置5个路由:全部iOSAndroidFlutterWeb

顶部导航组件
<!-- TheNavigation.vue -->
<template>
  <div id="nav">
    <router-link to="/" class="nav-link">全部</router-link>
    <router-link to="/ios" class="nav-link">iOS</router-link>
    <router-link to="/android" class="nav-link">Android</router-link>
    <router-link to="/flutter" class="nav-link">Flutter</router-link>
    <router-link to="/web" class="nav-link">Web</router-link>
  </div>
</template>

TheNavigation导航组件中有5个router-link,分别切换到全部iOSAndroidFlutterWeb

5个页面组件
<template>
  <div class="container">
    <!-- 博客列表 -->
    <div v-for="blog in arrs" class="item" :key="blog.id">
      <!-- 图片 -->
      <img class="thumb" :src="blog.image" />
      <!-- 信息 -->
      <div class="info">
        <div class="title">{{ blog.name }}</div>
        <div class="message"> {{ blog.description }} </div>
      </div>
    </div>
  </div>
</template>

<script setup>

// 数据
import sourceData from "@/data.json";
let arrs = sourceData;

</script>
APP.vue
<script setup>
import TheNavigation from "@/components/TheNavigation.vue";
</script>

<template>
  <TheNavigation />
  <router-view></router-view>
</template>

至此,博客框架就完成了,实现了5个博客分类,效果如下图:

框架

设置linkActiveClass

路由器可以设置router-link激活的类:

const router = createRouter({
  history: createWebHistory(),
  routes,
  <!--// 添加激活的类-->
  linkActiveClass: "blog-active-link"
});

然后设置样式:

#nav .blog-active-link  {
  color: red;
  border-bottom: 2px solid red;
}

linkActiveClass

命名路由

我们在顶部导航组件使用的跳转都是路径跳转例如:to="/", 我们可以给路由设置一个名称name,这样可以通过路由的名称name进行跳转。

<template>
  <div id="nav">
    <router-link to="/" class="nav-link">全部</router-link>
    <!-- 修改 to 属性为 name -->
    <router-link :to="{name: 'ios'}" class="nav-link">iOS</router-link>
    <router-link :to="{name: 'android'}" class="nav-link">Android</router-link>
    <router-link :to="{name: 'flutter'}" class="nav-link">Flutter</router-link>
    <router-link :to="{name: 'web'}" class="nav-link">Web</router-link>
  </div>
</template>

路由的query

前面提到的5个博客分类是固定的,我们点击博客列表的每条数据进入博客详情,此时由于不同的博客内容是不同的,所以不能固定写死。实现方法一是通过路由传参实现。

添加博客详情的路由
let routes = [
  //...
  {
    path: '/blogdetail',
    name: "blogdetail",
    component: () => import("@/views/BlogDetail.vue")
  }
];
query传参
<template>
  <div class="container">
    <!-- 传参 -->
    <router-link v-for="blog in arrs" class="item" :key="blog.id" :to="{ name: 'blogdetail', query: { id: blog.id } }">
      // 省略
    </router-link>
  </div>
</template>

设置query: { id: blog.id } }给路由传参

接收query传参
<template>
  <div class="container">
    <h2>{{ blog.name }}</h2>
    <p>{{ blog.description }}</p>
  </div>
</template>

<script>
import sourceData from "@/data.json";
import { useRoute } from "vue-router";
export default {
  setup(props) {
    // 获取路由
    let route = useRoute();
    // 获取query参数
    let blogId = route.query.id;

    return {
      blog: sourceData.find((blog) => blog.id == blogId),
    };
  },
};
</script>

通过route.query.id就能获取到传递的博客id, 然后就能显示对应的博客信息了。

query

动态路由

博客详情的页面逻辑,也可以用动态路由去实现。

修改博客详情的路由
<!-- router.js -->
let routes = [
  //...
  {
    <!-- 动态路由路径 -->
    path: '/blogdetail/:id',
    name: "blogdetail",
    component: () => import("@/views/BlogDetail.vue")
  }
];

:id 表示 路由的路径是动态的,路径最后表示博客id.

传参
<template>
  <div class="container">
    <!-- 传参 -->
    <router-link v-for="blog in arrs" class="item" :key="blog.id" :to="{ name: 'blogdetail', params: { id: blog.id } }">
      // 省略
    </router-link>
  </div>
</template>

设置params: { id: blog.id } }给动态路由传参

接收参数
let blogId = route.params.id;

通过route.params.id就能获取到传递的博客id, 然后就能显示对应的博客信息了。

重命名路由

知道了动态路由的逻辑后,我们当然可以把iOS, Android, Flutter, Web四个页面合并为一个页面。

合并router
<!-- router.js -->
let routes = [
  {
    path: "/",
    name: 'home',
    component: () => import("@/views/All.vue"),
  },
  <!-- 将/ios,/android,/flutter,/web四个合并为/category/:catId -->
  {
    path: "/category/:catId",
    name: 'category',
    component: () => import("@/views/All.vue"),
  },
  {
    path: '/blogdetail/:id',
    name: "blogdetail",
    component: () => import("@/views/BlogDetail.vue")
  }
];
修改导航
<template>
  <div id="nav">
    <router-link to="/" class="nav-link">全部</router-link>
    <!-- 动态路由 -->
    <router-link :to="{name: 'category', params: { catId: 1 }}" class="nav-link">iOS</router-link>
    <router-link :to="{name: 'category', params: { catId: 2 }}" class="nav-link">Android</router-link>
    <router-link :to="{name: 'category', params: { catId: 3 }}" class="nav-link">Flutter</router-link>
    <router-link :to="{name: 'category', params: { catId: 4 }}" class="nav-link">Web</router-link>
  </div>
</template>

列表
<script setup>
import { useRoute } from 'vue-router';

// 获取路由
let route = useRoute();
// 获取params参数
let catId = route.params.catId;

// 数据
import sourceData from "@/data.json";
let arrs = sourceData.filter((blog) => blog.catId == catId);

</script>

这样我就可以把iOS.vue,Android.vue,Flutter.vue,Web.vue四个组件文件删除了。

你应该有个疑问,home路由的内容其实和category路由的内容也是一样的,是否可以合并呢?

重命名"/"

可以将"/"重命名为'/category/0',这样所有的5个路由都将访问"/category/:catId"这个路由了。

<!-- router.js -->
let routes = [
  {
    path: "/",
    <!-- 重命名 -->
    redirect: '/category/0'
  },
  {
    path: "/category/:catId",
    name: 'category',
    component: () => import("@/views/All.vue"),
  },
  {
    path: '/blogdetail/:id',
    name: "blogdetail",
    component: () => import("@/views/BlogDetail.vue")
  }
];
import sourceData from "@/data.json";
let arrs = catId != '0' ? sourceData.filter((blog) => blog.catId == catId) : sourceData;

判断下,如果catId != '0'为分类筛选,否则就是显示全部

监听路由变化

此时的代码出现了问题,点击顶部的导航切换不同的分类,底下的列表将不会变化。这是因为组件复用了。此时需要监听组件的路由的变化,切换数据。

路由

可以通过watch函数监听route.params, 当路由变化后,就可以重新获取数据。

<!-- All.vue -->
<script setup>
import { ref } from '@vue/reactivity';
import { useRoute } from 'vue-router';
import sourceData from "@/data.json";
import { watch } from '@vue/runtime-core';

let arrs = ref([]);

let route = useRoute();
let params = route.params;

let initData = (catId) => {
  arrs.value = catId != '0' ? sourceData.filter((blog) => blog.catId == catId) : sourceData;
}

// 初始化的时候获取数据
initData(params.catId);

// 监听paramas,更新数据
watch(() => route.params.catId, (value) => {
  initData(value);
})

</script>

禁止路由复用

解决上节问题,还有一个更简单的方法,就是禁止路由的复用。

<template>
  <TheNavigation />
  <!-- 禁止路由复用 -->
  <router-view :key="$route.path"></router-view>
</template>

通过这个方法,动态组件将不会复用,直接卸载旧组件,挂载新组件。所以性能上有丢丢的损耗。

给组件传递props

我们前面在组件中需要使用useRoute获取到路由,然后获取对应的route.params, 我们可以通过另外一种方式获取route.params

路由添加props属性
<!-- router.js -->
{
    path: "/category/:catId",
    name: 'category',
    component: () => import("@/views/All.vue"),
    <!-- 路由添加`props`属性 -->
    props: true,
}
组件中获取props属性
<script setup>
import { ref } from '@vue/reactivity';
import { useRoute } from 'vue-router';
import sourceData from "@/data.json";

// 定义props
const props = defineProps({
  catId: {
    type: String,
    required: true,
  }
})

let arrs = props.catId != '0' ? sourceData.filter((blog) => blog.catId == props.catId) : sourceData;

</script>

组件中可以直接获取到catId参数,个人认为这种写法更优美。

路由props属性支持函数
<!-- router.js -->
{
    path: "/category/:catId",
    name: 'category',
    component: () => import("@/views/All.vue"),
    props: route => ({ catId: parseInt(route.params.catId) }) ,
}

函数中,可以对参数进行处理,我们的例子中是将catId从字符串变成了数字

// 定义props
const props = defineProps({
  catId: {
    type: Number,
    required: true,
  }
})

let arrs = props.catId !== 0 ? sourceData.filter((blog) => blog.catId === props.catId) : sourceData;

props catId的定义和使用也要进行相应的修改

编程式导航

除了使用<router-link> 来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。

例如:可以在详情页加一个按钮,点击返回上一个页面

<button @click="$router.back()">返回</button>

转场动画

Vue Router4 的转场动画的实现 和 以前的版本有些不一致。需要将transition 包含在router-view, 如下所示:

  <router-view v-slot="{ Component }">
    <transition name="fade" mode="out-in">
      <component :is="Component" :key="$route.path" />
    </transition>
  </router-view>

加上对应的css样式

/* fade 模式 name="fade" mode="out-in" */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

这样切换就有淡入淡出的效果了。效果自定义,很方便。

路由未匹配上

有时候用户可能输入一个根本不存在的路劲(例如:http://localhost:3000/categorys),此时最好是给显示个默认的404页面,这样用户体验更好。

404页面
定义路由
<!-- router.js -->
{
    path: '/:pathMatch(.*)*',
    name: "NotFound",
    component: () => import("@/views/404.vue"),
}

注意,这个路由一定要放在最后,否则就有问题了。

404页面
<template>
  <div class="container">
    <h2>未找到页面</h2>
  <router-link to="/">回到首页</router-link>
  </div>
</template>

这个页面内容随意

路由守卫

路由独享的守卫

想象下用户浏览器地址栏输入http://localhost:3000/category/6, 其实也会出现一些问题,因为不存在这个分类。这时候需要进行处理, 当分类不存在的时候跳转到404页面。

<!-- router.js -->
  {
    path: "/category/:catId",
    name: 'category',
    component: () => import("@/views/All.vue"),
    props: route => ({ catId: parseInt(route.params.catId) }),
    <!-- 添加路由守卫 -->
    beforeEnter: (to, from) => {
      // 如果不是正确的分类,跳转到NotFound的页面
      console.log(to.params.catId);
      if (!["0", "1", "2", "3", "4"].includes(to.params.catId)) { 
        return {
          name: "NotFound",
          // 这个是在地址栏保留输入的信息,否则地址栏会非常的丑
          params: { pathMatch: to.path.split("/").slice(1) },
          query: to.query,
          hash: to.hash,
        };
      }
    }
  },

判断如果不是正确的分类,跳转到NotFound的页面

路由全局守卫

在某些路由中需要一些特定的操作,譬如访问前必须是登录用户。这时候可以通过使用meta属性和全局守卫来实现。

譬如有一个课程专栏我们设置为需要用户登录才能访问。我们可以如下设置

<!-- router.js -->
  {
    path: '/course',
    name: "course",
    component: () => import("@/views/Course.vue"),
    <!-- 需要登录 -->
    meta: {needLogin: true}
  },
  {
    path: '/login',
    name: "login",
    component: () => import("@/views/Login.vue"),
  },
  

添加一个全局守卫, 需要登录但是没有登录的情况下就跳转到登录页面

<!-- router.js -->
// 全局守卫
router.beforeEach((to, from) => {
  if (to.meta.needLogin && !userLogin) {
    // need to login
    return { name: "login" };
  }
});
组件内的路由守卫

前面的切换分类的章节的问题其实还有第三种解决方案,就是用组件内的路由守卫。

<script setup>
import { ref } from '@vue/reactivity';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import sourceData from "@/data.json";

// 定义props
const props = defineProps({
  catId: {
    type: Number,
    required: true,
  }
})

let arrs = ref([]);

let fetchData = (id) => {
  return id !== 0 ? sourceData.filter((blog) => blog.catId == id) : sourceData;
}

<!-- 组件内的路由守卫 -->
onBeforeRouteUpdate((to, from, next) => {
  arrs.value = fetchData(to.params.catId)
});

arrs.value = fetchData(props.catId);

</script>

对于一个带有动态参数的路径 /category/:catId,在 /category/1/category/2 之间跳转的时候, 会触发onBeforeRouteUpdate的路由钩子函数,在钩子函数中可以进行数据的更新。

扩展 RouterLink

router-link可以实现路由的跳转,此外为了更加丰富功能,可以对其进行扩展。譬如我们可以扩展实现能够跳转到外部链接。

<!--AppLink.vue-->
<template>
  <!-- 如果是外部链接,跳转(<slot />表示router-link组件中的slot内容)  -->
  <a v-if="isExternal" :href="to"><slot /></a>
  <!-- 如果是APP内的链接,路由跳转 (<slot />表示router-link组件中的slot内容) -->
  <router-link v-else v-bind="$props"><slot /></router-link>
</template>

<script>
import { computed, defineComponent } from "@vue/runtime-core";
import { RouterLink } from "vue-router";

export default {
  props: {
    // 继承RouterLink的props
    ...RouterLink.props,
  },
  setup(props) {
    
    // 如果`to`属性值是字符串类型,并且以`http`开头,我们认为它是外部链接
    let isExternal = computed(() => typeof props.to === 'string' && props.to.startsWith('http'));

    return {
      isExternal
    }
  }
};
</script>

使用:

<AppLink to="https://www.domain.cn" />

总结

Vue Router 4.x 的使用基本上介绍完了,最重要的特性是能和Composition API的搭配使用,此外使用上也还是有一些不小的变化。