08. 路由
很激动进入了 Vue 3 的学习,作为一个已经上线了三年多的框架,很多项目都开始使用 Vue 3 来编写了
这一组文章主要聚焦于 Vue 3 的新技术和新特性
如果想要学习基础的 Vue 语法可以看我专栏中的其他博客
Vue(一):Vue 入门与 Vue 指令
Vue(二):计算属性与 watch 监听器
Vue(三):Vue 生命周期与工程化开发
一篇文章快速通关 Vuex(适合小白学习)
Vue 框架前导:详解 Ajax
快速打通 Vue 3(一):基本介绍与组合式 API
快速打通 Vue 3(二):响应式对象基础
快速打通 Vue 3(三):Vue3 中的 watch 监听器与新特性
快速打通 Vue 3(四):标签的 ref 属性与 Vue3 生命周期
上一篇 Vue3 博客:快速打通 Vue 3(四):标签的 ref 属性与 Vue3 生命周期
后续还会继续更新,期待大家的关注!
这篇博客真的写了好久,期间我尝试了新的整理的方式,因为路由在开发中是十分重要的模块,所以我在博客中插入了实战的案例,希望能帮助大家更好的理解和学习路由。
8.1 路由的理解
提到路由器大家一定都非常熟悉,但是如果问起路由器的功能,或许很多人都有疑问
路由器(Router)是连接两个或多个网络的硬件设备,在网络间起 网关 的作用,是读取每一个 数据包 中的地址然后决定如何传送的专用智能性的 网络设备。
比如我们手机和电脑同时去接收网络数据的时候,这两个设备需要的信息肯定是不同的,传输过来的数据包会带有地址,路由器会根据传输过来的数据中的地址去决定选择哪个设备。
vue
中的路由和上面路由器的作用是相同的,路由会根据我们的地址不同去 局部的 更新我们页面的内容。
这么说可能很抽象,我们来看一个具体的例子:网易云音乐网页端
通过上方的导航栏实现跳转,点击导航栏之后,我们发现只是变化了导航栏下面的区域,而导航栏区域是没有变化的,做过网页的朋友应该都知道当我们点击一个超链接后,整个页面都会刷新,没法做到这种局部的更新,对于这种页面,我们称之为 单页应用[^4],这就需要来借助路由去实现了。
现在来回想一下我们的需求:我们想要做一个网页,网页上部有导航栏,通过单击导航栏来实现下面部分的改变但不想要刷新整个界面,这就有了一下的几个问题:
- 我们如何在不触发刷新的情况下改变地址呢?
- 程序是怎么知道我们想让内容变化的呢,并且找到响应的界面的呢?
- 我们更新的内容放置在哪里呢?
下面我们带着这两个问题继续学习。
8.2 基本使用
上面的一切内容都是路由实现的,在开始之前,我们先来导入 vue-router
在 vue3
的项目中,我们直接引入最新的路由,新建一个 vue3
的项目
$npm create vue@latest
$yarn create vue
$pnpm create vue
其实很容易的发现,上面的网易云跳转的时候,网址是在我们的点击中变化的,所以不难得出,路由是 监听网址的变化 来得知我们想要去更新界面,而不同的网址就对应不同的界面,上面提到的第二个问题就很好解决了:
路由根据我们的网址的变化去得知我们要更新界面;而根据网址的内容去匹配不同的组件[^5]。
那我们对路由的配置就需要这些内容,下面我们来看看路由的基本写法:
import { RouteRecordRaw } from "vue-router";
import HomeView from "@/views/HomeView.vue";
import AdminView from "@/views/admin/AdminView.vue";
import UserLayout from "@/layouts/UserLayout.vue";
import UserLoginView from "@/views/user/UserLoginView.vue";
import UserRegisterView from "@/views/user/UserRegisterView.vue";
import AboutView from "@/views/user/AboutView.vue";
import CreateView from "@/views/admin/CreateView.vue";
export const routes: Array<RouteRecordRaw> = [
{
path: "/user",
name: "用户",
component: UserLayout,
children: [
{
path: "/user/login",
component: UserLoginView,
},
{
path: "/user/register",
component: UserRegisterView,
},
],
},
{
path: "/",
name: "浏览题目",
component: HomeView,
},
{
path: "/create",
name: "答题界面",
component: CreateView,
},
{
path: "/admin",
name: "创建题目",
component: AdminView,
},
{
path: "/about",
name: "关于我的",
component: AboutView,
},
];
上面是我在我的 OJ
系统中写的路由配置,最基本的三个配置就是
- 路径的 地址
- 路径的 名字
- 路径对应的 组件
这就做好了匹配的关系:根据网址的变化去找路由,路由去根据配置去变化内容。
但是我们写的路由是没有作用的,我们既没有注册路由也没有去使用路由,下面我们来看路由的注册与使用:
注册:
import { createRouter, createWebHistory } from "vue-router";
import { routes } from "@/router/routes";
const router = createRouter({
// 这条语句的作用在后面去解答
history: createWebHistory(),
routes,
});
export default router;
使用:
import { createApp } from "vue";
import ArcoVue from "@arco-design/web-vue";
import App from "./App.vue";
import "@arco-design/web-vue/dist/arco.css";
import router from "./router";
import store from "./store";
import "@/access";
import "bytemd/dist/index.css";
import { Message } from "@arco-design/web-vue";
const app = createApp(App);
app.use(store).use(router).use(ArcoVue).mount("#app");
Message._context = app._context;
使用只需要引入注册的 router
然后在 app
后面加上一个 use
即可,这样我们 全局的跳转逻辑就全部由路由去接管了。
地址变化会改变下面的内容,这时候就需要有东西可以改变地址担又不刷新界面,这就是我们导航栏的作用,可以理解为一种特殊的超链接。
而这种效果需要用到一种特殊的组件 <RouterLink>
<RouterLink to="/home"> 首页 </RouterLink>
其中最重要的属性就是 to
表示要变化到什么内容。
那只剩下最后一个问题了,我们更新的内容,也就是新的组件要放在哪里,vue
也提供了我们一个组件 RouterView
直接将这个放在我们所需要的区域即可。
<template>
<div class="app">
<h2 class="title">Vue路由测试</h2>
<!-- 导航区 -->
<div class="navigate">
<RouterLink to="/home">首页</RouterLink>
<RouterLink to="/news">新闻</RouterLink>
<RouterLink to="/about">关于</RouterLink>
</div>
<!-- 展示区 -->
<div class="main-content">
<RouterView></RouterView>
</div>
</div>
</template>
这个区域就是放现在的组件和更新的组件放置的位置。
那我们理解之前网易云音乐网页端就很容易了:
上方的导航栏就类似 RouterLink
,下面展示的内容就是 RouterView
通过上面的例子,路由组件是一块相当大的区域,甚至在一整个界面中,而且路由组件不是只用导入然后在通过尖括号的方式直接添加到网页中,而是要借助 RouterView
。但是,这两种的组件的后缀居然都是 .vue
这里就需要区分开:
- 将路由的组件存放在
pages
或者views
文件夹中 - 一般的组件存放在
components
文件夹中 - 路由组件一般取名叫
xxxView.vue
比如UserLoginView.vue
,我们一般将路由组件视作一个视图。
8.3 路由的两种工作模式
这一段主要是解释上面创建路由的时候加了一条语句
const router = createRouter({
// 这条语句的作用在后面去解答
history: createWebHistory(),
routes,
});
就是这个 history
,这里就需要提到路由的两种工作模式
- 基于哈希(Hash-based)
- 基于浏览器历史记录(History-based)
来看看他们具体的解释:
- 基于哈希(Hash-based):
- 在这种模式下,路由信息被存储在URL的片段标识符中,即URL的哈希部分(通常是
#
后面的部分)。 - 哈希模式的优势在于它不会导致完整的页面刷新,因为哈希的变化不会触发浏览器向服务器请求新页面。
- 这种模式的一个示例是:
http://example.com/#/page1
。在这里,#/page1
是路由信息。
- 在这种模式下,路由信息被存储在URL的片段标识符中,即URL的哈希部分(通常是
- 基于浏览器历史记录(History-based):
- 在这种模式下,路由信息直接集成在URL的路径中,而不是依赖于哈希。
- 使用 HTML5 的 History API,开发者可以通过添加、修改和删除浏览器历史记录条目来实现前端路由。
- 这种模式的一个示例是:
http://example.com/page1
。在这里,/page1
是路由信息。
这两种模式带给我们最直观的效果就是带不带 #
,这两种模式各有优势:
history
模式优点:
URL
更加美观,不带有#
,更接近传统的网站URL
。缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有
404
错误。
hash
模式优点:兼容性更好,因为不需要服务器端处理路径。
缺点:
URL
带有#
不太美观,且在SEO
优化方面相对较差。
所以如果是内部的管理系统的话,用哈希模式其实方便我们的部署,而且内部应用也不需要 SEO,但像网易云这种开放给外部使用的还是建议使用第一种,虽然在部署的时候有一点麻烦(确实是只有一点),但 SEO 优化很好而且地址也比较美观。
8.4 to 的两种写法
让我们继续回到主线上来,RouterLink
中最重要的属性就是 to
,它可以指定我们选择的组件,并且可以 传参,之所以选择这种复杂的传参方式是因为路由组件并不像普通的组件那么方便,可以通过写自定义属性的方式来实现直接传参。
路由组件没有位置去写自定义属性,所以采取这种较为曲折的方式来传递参数,下面提供的 to
的第二种对象写法就是方便后面进行传参去使用的,具体的语法后面再提及。
<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>
<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>
8.5 嵌套路由
继续打开我们的网易云
在发现音乐的下面还有几个标题,这些
点击之后我们发现路径是这样的
https://music.163.com/#/discover/toplist
https://music.163.com/#/discover/playlist
如果我们在一个路由组件内还想嵌套另一个路由组件,就需要地址去嵌套另一个地址,这就是我们说的嵌套路由
下面我们来看具体的写法:
嵌套路由写法:
{
name: '推荐',
path: '/discover',
component: DiscoverView,
children:[
{
name: '排行榜',
path: '/toplist',
component: ToplistView
}
]
},
跳转写法:
<router-link to="/discover/toplist"> xxxx </router-link>
<!-- 或 -->
<router-link :to="{path:'/discover/toplist'}"> xxxx </router-link>
最后记得在组件中再去预留一个 RouterView
<template>
<div class="news">
<nav class="news-list">
<RouterLink v-for="title in titleList" :key="title.id"
:to="{path:/discover/toplist}">
{{title.name}}
</RouterLink>
</nav>
<div class="title-detail">
<RouterView/>
</div>
</div>
</template>
8.6 路由传参
因为和路由组件的特殊性(不能在父组件写自定义属性),所以我们需要新的方式来实现传参
8.6.1 query 参数
我们上面已经得知 to
参数的两种写法,但还是没有体会到第二种写法到底好在哪里,下面贴出一段代码:
不用对象写法
<!-- 跳转并携带query参数(to的字符串写法) -->
<router-link to="/news/detail?a=1&b=2&content=欢迎你">
跳转
</router-link>
使用对象写法
<RouterLink
:to="{
//name:'xiang', //用name也可以跳转
path:'/news/detail',
query:{
id:news.id,
title:news.title,
content:news.content
}
}"
>
{{news.title}}
</RouterLink>
这样一对比第二种写法的优势就很明显了,如果我们传递的参数变多后面就要嵌套一大堆 /
,而且我们需要传递 不写死的内容,还需要写好多 ${}
,所以以后在开发中最好使用第二种写法。
接收参数
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印query参数
console.log(route.query)
我们可以直接使用 route
中的 query
参数来取得传递的参数。
8.6.2 params 参数
用在传递的参数和路径紧密结合的情况,路径符合 RestFul
风格
定义路由
与前面不同的是,params
参数需要我们提前指定参数,当然也可以通过 ?
修饰符来作为选择传递的内容。
// 路由配置
const routes = [
{
// path: '/book/:id?', 可以选择是否传入
path: '/book/:id',
component: BookDetails,
name: 'book-details',
},
];
传递参数
<div>
<RouterLink :to="{ name: 'book-details', params: { id: 1 }}">Book 1</router-link>
<RouterLink :to="{ name: 'book-details', params: { id: 2 }}">Book 2</router-link>
<RouterView />
</div>
接收参数
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印params参数
console.log(route.params)
注意:如果使用 params
写法的时候,必须要使用 name
来指定路由,而不是使用前面经常使用的的 path
。
8.6.3 props 传参
props
传参可以将路由参数作为 props
传递给子组件,这是最简洁的一种方式
将 params
参数作为 props
传递
{
name:'xiang',
path:'detail/:id/:title/:content',
component:Detail,
// props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件
props:true
}
将 query
参数作为 props
传递
{
name:'xiang',
path:'detail',
component:Detail,
// props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
props(route){
return route.query
}
}
接收参数并且使用
const props = withDefaults(defineProps<Props>(), {
// 定义需要接收的属性和初始值
});
8.7 replace 与 push
我们可以把路由器的历史记录视作一个栈,我们前面使用的所有跳转逻辑都是 push
的方式,即在栈顶部插入一个数据
这样的排布方式可以让浏览器实现前进和后退
但如果用户进入一个界面后,我们不希望用户可以通过回退来返回上一个页面就要使用到 replace
配置
要实现也非常容易
<RouterLink to="/page1" replace>Page 1</RouterLink>
只需要在后面加上一个 replace
即可
8.7 编程式路由
这是路由非常重要的一部分,因为我们开发中不可能总是使用 RouteLink
去实现跳转,因为 RouterLink
最多算是一个特殊的超链接,超链接就需要用户去点击,当我们想做一些不点击就跳转的逻辑,就需要在 script
部分去编写跳转逻辑,而不能去使用 RouterLink
。
编程式路由学习起来非常简单,因为它和上面的 RouterLink
几乎是一模一样的,RouterLink
中的 to
属性中传递的内容就是我们使用编程式路由的时候传递的参数。
const route = useRoute();
const router = useRouter();
console.log("route", route);
console.log("router", router);
编程式最重要的两个内容就是 route
和 router
route
是路径,封装这我们传递的属性router
是路由器,负责跳转逻辑
我们的编程式就是通过 router
中的 push
和 replace
来实现的,分别对应着上面的两种方式,而传递的参数就是 RouterLink
中的 to
属性的参数。
下面给出一个案例,是我项目中代码的一部分,实现了登录的逻辑和登录之后的跳转:
页面结构部分
<template>
<div class="userLogin">
<a-layout style="height: 400px">
<a-layout-content class="content">
<div class="showImg">
<div class="loginForm">
<div class="title">登录</div>
<a-form
:model="form"
:style="{ width: '400px' }"
@submit="handleSubmit"
:size="'large'"
class="form"
>
<a-form-item
field="name"
style="margin-bottom: 25px"
arco-theme="dark"
>
<a-input
v-model="form.userAccount"
placeholder="请输入你的用户名"
/>
</a-form-item>
<a-form-item field="post" style="margin-bottom: 30px">
<a-input-password
v-model="form.userPassword"
placeholder="密码"
/>
</a-form-item>
<a-form-item>
<a-button html-type="submit">登录</a-button>
</a-form-item>
</a-form>
</div>
</div>
</a-layout-content>
<a-layout-footer></a-layout-footer>
</a-layout>
</div>
</template>
我这里使用的是 Arco Design
中的登录表单组件用户输入账号和密码
<script setup lang="ts">
import { reactive } from "vue";
import { useRouter } from "vue-router";
import store from "@/store";
import AccessEnum from "@/access/accessEnum";
import { UserControllerService, UserLoginDTO } from "../../../../generated";
import { Message } from "@arco-design/web-vue";
// 绑定表单属性
const form = reactive({
name: "",
password: "",
} as UserLoginDTO);
const router = useRouter();
// 向后端去请求检查
const handleSubmit = async () => {
const res = await UserControllerService.userLogin(form);
console.log(res.data);
if (res.code == 200) {
Message.success("登录成功");
localStorage.setItem(AccessEnum.TOKEN_NAME, res.data.token);
store.commit("updateUser", res.data);
console.log("登陆后的结果", store.state.loginUser);
// 跳转
router.push({
path: "/",
});
} else {
// 密码错误执行逻辑
Message.warning("用户名或密码错误");
console.error();
}
};
</script>
8.8 重定向
实现当我们进入一个路径的时候,重定向到已经有的路由:
{
path:'/',
redirect:'/about'
}
这就实现了当我们访问 /
的时候,自动重定向到 /about
对应的组件。
如果这篇博客帮助到了你,请留下你的关注和评论叭😊😊😊