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应用在自己的项目中。项目初
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的搭配使用,此外使用上也还是有一些不小的变化。
更多推荐
所有评论(0)