Vue Router

94 阅读4分钟

Vue路由与状态管理

Vue-router

当我们还在手动撸路由的时候,别人页面都出几个了,这就是 vue-router 的威力, 相比我们自己的工具,他更能发挥规模化的力量。

Vue RouterVue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

安装

第一个我们需要安装依赖, 当前项目下

npm install vue-router

Vue-routerVue 的插件, 我们按照插件的方式引入到 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页面下设置了跳转按钮

image.png

编程式的导航

当你点击 <router-link> 时,内部会调用这个方法,所以点击 <router-link :to="..."> 相当于调用 router.push(...) :

声明式编程式
<router-link :to="...">router.push(...)
router-link 这种组件是需要用户点击才能生效的, 如果 需要动态加载,或者跳转前检查用户的权限,这个时候再使用 router-link 就不合适了

在之前的学习中,我们知道 window.historylocation 可以模拟我们操作浏览器

  • 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>

image.png

我们还漏了一个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>

结果: image.png

加载数据

你也许会问: 这有什么卵用? 就为了打印下 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.jsdefineConfig 内添加
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>

image.png

这种方式短平快, 但是上了规模后就会有问题:

  • 后期接口有变更怎么办? 一个一个找来更新吗?
  • 我有一些通用的中间件需要加载, 每次请求时,添加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.vuefetchData 函数 进行修改

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);
  }
);

image.png

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)
})

广泛使用的就 beforeEachafterEach , 我们以此为例, 做一个简单的页面加载 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()
})

image.png

这个颜色好像不行? 我们怎么调整下喃?

找到样式,调整好 写入一个文件中: styles/index.css, 等下全局加载

#nprogress .bar {
    background:#13C2C2;
  }

在main.js加载全局样式

// 加载全局样式
import './styles/index.css'

或是直接在 assets/baase.css 里设置