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: "//www.greatytc.com/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="//www.greatytc.com/about">About</router-link><br />
  </div>
  <router-view></router-view>
</template>

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

basic

几个重要的概念

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

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

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

Hash模式和HTML5模式的区别?

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

HTML5模式和正常的链接地址一样的,eg:http://localhost:3000//www.greatytc.com/about, 这个地址很优雅,但是有一个问题,需要服务器支持。 原因是浏览器中输入http://localhost:3000//www.greatytc.com/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: "//www.greatytc.com/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的搭配使用,此外使用上也还是有一些不小的变化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容