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.vue
和About.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-link
和 router-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文件。
webpack和vite启动的服务器是支持
HTML5
模式的,所以开发环境使用HTML5
模式没有问题。
router-link
组件和router-view
组件为什么能直接使用?
安装router
插件的时候注册了这两个全局组件,所以能直接使用。
install(app: App) {
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
}
路由懒加载
上面写法有一个严重的问题,router
中所有的组件都会被一次加载。我们的例子中就是 Home
和About
组件,即使有时候不会用到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个路由:全部,iOS,Android,Flutter,Web。
顶部导航组件
<!-- 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
,分别切换到全部,iOS,Android,Flutter,Web。
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;
}
命名路由
我们在顶部导航组件使用的跳转都是路径跳转例如: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, 然后就能显示对应的博客信息了。
动态路由
博客详情的页面逻辑,也可以用动态路由去实现。
修改博客详情的路由
<!-- 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的搭配使用,此外使用上也还是有一些不小的变化。