Vue路由与状态管理
Vue-router
当我们还在手动撸路由的时候,别人页面都出几个了,这就是 vue-router 的威力, 相比我们自己的工具,他更能发挥规模化的力量。
Vue Router 是 Vue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
安装
第一个我们需要安装依赖, 当前项目下
npm install vue-router
Vue-router 是 Vue 的插件, 我们按照插件的方式引入到 main.js 中
import router from "./router";
app.use(router)
起步
可以看到 router 的定义都在一个模块(router/index.js)里面
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
const router = createRouter({
// 全局变量
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
// 一进来就直接加载
component: HomeView,
},
{
path: "/about",
name: "about",
// 惰性加载:点击后才会加载
component: () => import("../views/AboutView.vue"),
},
],
});
export default router;
比如我们在添加一个页面: Test.vue
<template>
<div>
<h1>This is an test page</h1>
</div>
</template>
然后我们补充到路由里面
const router = createRouter({
// 全局变量
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
// 一进来就直接加载
component: HomeView,
},
{
path: "/about",
name: "about",
// 惰性加载:点击后才会加载
component: () => import("../views/AboutView.vue"),
},
{
path: "/test",
name: "test",
component: () => import("../views/TestView.vue"),
}
],
});
然后我们在 AboutView 界面上添加一个跳转
<div>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/test">Test</router-link>
</div>
我们在About页面下设置了跳转按钮
编程式的导航
当你点击 <router-link> 时,内部会调用这个方法,所以点击 <router-link :to="..."> 相当于调用 router.push(...) :
| 声明式 | 编程式 |
|---|---|
<router-link :to="..."> | router.push(...) |
router-link 这种组件是需要用户点击才能生效的, 如果 需要动态加载,或者跳转前检查用户的权限,这个时候再使用 router-link 就不合适了 |
在之前的学习中,我们知道 window.history 和 location 可以模拟我们操作浏览器
location.assign('/')location.reload()history.back()history.forward()
vue-router 为我们提供了一个函数用于 JS 来控制路由那就是 push ,其功能和 location.assign 类似
router.push(location, onComplete?, onAbort?)
// location location参数 等价于 <router-link :to="...">, 比如<router-link :to="/home"> 等价于 router.push('/home')
// onComplete 完成后的回调
// onAbort 取消后的回调
我们调整下我们 AboutView.vue , 使用 a 标签
<template>
<div>
<a @click="jumpToHome">Home</a>
</div>
</template>
<script setup>
const jumpToHome = () => {
router.push("/");
};
</script>
动态路由匹配
现在我们的遇到的路由都是静态的, 我们看看前后端路由的区别
后端: path ---> handler
前端: path ---> view
我们看看之前 demo 里面的 http router 路由
r.GET("/hosts", api.QueryHost)
r.POST("/hosts", api.CreateHost)
r.GET("/hosts/:id", api.DescribeHost)
r.DELETE("/hosts/:id", api.DeleteHost)
r.PUT("/hosts/:id", api.PutHost)
r.PATCH("/hosts/:id", api.PatchHost)
vue-router的路由也支持像上面httprouter那样的路由匹配
我们在 index.js 中修改测试页面, 改为动态匹配
{
path: '/test/:id',
name: 'Test',
component: () => import("../views/TestView.vue"),
}
我们在 AboutView 里面进行修改
const jumpToTest = () => {
router.push({ name: "test", query: { name: "a" }, params: { id: 10 } });
};
最后我们可以在 TestView 里打印路由信息
<span>{{ $route }}</span>
我们还漏了一个404的处理, 如果我们找不到页面, 也需要返回一个视图, 告诉用户也没不存在
vue-router在处理404的方式和后端不同, 路由依次匹配, 如果都匹配不上 写一个特殊的路由作为 404路由
//和之前一样在index.js内添加
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("../views/NotFound.vue"),
},
那我们在views里补充一个404路由组件
<template>
<div>you URL is wrong!</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>
结果:
加载数据
你也许会问: 这有什么卵用? 就为了打印下 id 吗? 那我们做一个完整详情页面
我们可以使用这个来做详情页面, 根据不同的 id 往后端获取不同的对象, 用于显示
如何请求 id 对应的后端数据, 通过 axios
npm install --save axios
我们之前是这样使用 axios 的: 在 Views 里新建一个页面 BlogSet.vue
<script setup>
// 通过 axios 这个 http 客户端 获取来自server提供的数据
// axios.get 返回一个 promise 对象
import axios from "axios";
import { onMounted } from "vue";
const fetchData = () => {
axios
.get("http://localhost:7080/vblog/api/v1/blog/") //该路径是本地开启的HTTP接口,用于获取数据库信息
.then((resp) => {
console.log(resp);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log("fetch data complete");
});
};
onMounted(() => {
fetchData();
});
</script>
产生跨域问题
- 由于前端端口在 5173 后端端口在7080 跨域冲突,通过 代理 实现后端访问后端
- 在
vite.config.js的defineConfig内添加
export default defineConfig({
plugins: [vue()],
base: "/",
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
proxy: {
// http://localhost:5173/vblog/api/v1/blog/
// 重定向到 http://localhost:7080/vblog/api/v1/blog/
"/vblog/api/v1": "http://localhost:7080/",
},
},
});
并把 BlogSet 里面的 axios.get() 参数修改为 "vblog/api/v1/blog/"
我们声明变量并把它在前端展示出来
以下是 BlogSet.vue 需要添加的部分
<template>
<ul v-for="item in data" :key="item.id">
<li>
{{ item.title_name }}
</li>
</ul>
</template>
<script setup>
let data = ref([]);
axios
.get("vblog/api/v1/blog/")
.then((resp) => {
data.value = resp.data.items; // 将数据传给data
console.log(resp);
})</script>
这种方式短平快, 但是上了规模后就会有问题:
- 后期接口有变更怎么办? 一个一个找来更新吗?
- 我有一些通用的中间件需要加载, 每次请求时,添加token头
首先我们需要将 ajax 封装下, 因为需要添加一些通用逻辑, 在 src 文件夹下创建 api/client.js
import axios from "axios";
const client = axios.create({
timeout: 5000,
});
export default client;
我们在同目录下创建 blog.js 封装方法
import client from "./client";
export function LIST_BLOG(params) {
return client({ url: "vblog/api/v1/blog/", method: "get", params: params });
}
export function GET_BLOG(id, params) {
return client({
url: `vblog/api/v1/blog/${id}`,
method: "get",
params: params,
});
}
最后对 BlogList.vue 的fetchData 函数 进行修改
const fetchData = async () => {
try {
const resp = await LIST_BLOG();
console.log(resp);
data.value = resp.data.items;
} catch (error) {
console.log(error);
} finally {
console.log("fetch data complete");
}
};
请求中间件和响应中间件 在 client.js 内添加
// 添加请求拦截器,用于认证, 都需要携带 Token,或者 basic AUTH
client.interceptors.request.use(
(request) => {
console.log(request);
return request;
},
(error) => {
console.log(error);
}
);
// 添加响应拦截器, 用于处理返回异常, code != 0
client.interceptors.response.use(
(response) => {
console.log(response);
return response;
},
(error) => {
console.log(error);
}
);
Router对象
讲了那么久的router, router到底有写啥,我们可以看看Router的定义:
export declare interface Router {
constructor(options?: RouterOptions)
app: Vue
options: RouterOptions
mode: RouterMode
currentRoute: Route
beforeEach(guard: NavigationGuard): Function
beforeResolve(guard: NavigationGuard): Function
afterEach(hook: (to: Route, from: Route) => any): Function
push(location: RawLocation): Promise<Route>
replace(location: RawLocation): Promise<Route>
push(
location: RawLocation,
onComplete?: Function,
onAbort?: ErrorHandler
): void
replace(
location: RawLocation,
onComplete?: Function,
onAbort?: ErrorHandler
): void
go(n: number): void
back(): void
forward(): void
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route
getMatchedComponents(to?: RawLocation | Route): Component[]
onReady(cb: Function, errorCb?: ErrorHandler): void
onError(cb: ErrorHandler): void
addRoutes(routes: RouteConfig[]): void
addRoute(parent: string, route: RouteConfig): void
addRoute(route: RouteConfig): void
getRoutes(): RouteRecordPublic[]
resolve(
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location
route: Route
href: string
// backwards compat
normalizedTo: Location
resolved: Route
}
Router钩子
如果需要在路由前后做一些额外的处理, 这就需要路由为我们留钩子, 最常见的使用钩子的地方是认证, 在访问页面的时候, 判断用户是否有权限访问
router为我们提供了如下钩子
- beforeEach: 路由前处理
- beforeEnter
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
- afterEach: 路由后出来
我们为router设置钩子函数验证下:
router.beforeEach((to, from, next) => {
console.log(to, from, next)
next()
})
router.afterEach((to, from) => {
console.log(to, from)
})
广泛使用的就 beforeEach 和 afterEach , 我们以此为例, 做一个简单的页面加载 progress bar
这里我们选用nprogress这个库来实现: nprogress
npm install --save nprogress
这玩意使用也简单
NProgress.start();
NProgress.done();
NProgress.set(0.0); // Sorta same as .start()
NProgress.set(0.4);
NProgress.set(1.0); // Sorta same as .done()
我们先引入库和样式
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
// 路由开始时: NProgress.start();
// 路由结束时: NProgress.done();
按照这个逻辑修改我们的router
router.beforeEach((to, from, next) => {
// start progress bar
NProgress.start()
console.log(to, from, next)
next()
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
这个颜色好像不行? 我们怎么调整下喃?
找到样式,调整好 写入一个文件中: styles/index.css, 等下全局加载
#nprogress .bar {
background:#13C2C2;
}
在main.js加载全局样式
// 加载全局样式
import './styles/index.css'
或是直接在 assets/baase.css 里设置