Skip to content

中后台开发必修课:Vue 项目中 Pinia 与 Router 完全攻略 #2

@bryqiu

Description

@bryqiu

前言

本篇文章主要讲解如何来配置 Pinia 和 Vue Router

本文也是《通俗易懂的中后台系统建设指南》系列的第二篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统

写在前面

路由(Router)和状态管理(Vuex、Pinia)是 Vue 项目中的常客。基本上在 Vue 的项目中,我们构建一个 Web 应用都离不开它们,如果你是 Vue2 的用户,那么你对它们不会陌生

如果你是跟着本系列第一篇搭建文章的话,那么此时你应该拥有一个基础性的项目,搭好路由和状态管理这两个板块,是一块基石,下面我们讲解如何配置它们。

Pinia 状态管理

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态

在这之前,我们用的是 Vuex
如官网所说,与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了符合组合式 API 风格的 API,最重要的是,搭配 TypeScript 一起使用时有非常可靠的类型推断支持

Pinia 数据持久化

持久化,顾名思义,保持数据持久不消失

要实现 pinia 数据持久化,我们需要借助 pinia-plugin-persistedstate 插件

持久化的需求体现在哪里?

  • 用户偏好设置,比如主题、语言、个性等,将这些偏好持久化可以确保用户在下次打开时仍然保持个性设置
  • 用户认证态,即保存用户的登录数据,以便下次打开此应用时不需要重复登录
  • 性能优化,对于一些需要频繁访问但很少改变的数据,将其持久化到本地存储可以减少网络请求

Pinia 安装

安装 Pinia 及持久化依赖 pinia-plugin-persistedstate

pnpm add pinia pinia-plugin-persistedstate

初始化

安装完成后,我们创建 src/store 目录,表示这个目录下的内容都是与存储相关

然后在目录下新建 init.ts 文件和 modules 文件夹:

  • init.ts 作为基础文件,用于注册 Pinia 和 Pinia 的基本配置
  • modules 文件夹下存放的是 Store 文件,比如 user.tslocale.ts 等存储文件
  • index.ts 用于导出 modules 下的全部存储文件

现在来配置 Pinia 和持久化,在 init.ts 中写入以下内容

import { createPinia } from 'pinia';
import { App } from 'vue';
import { createPersistedState } from 'pinia-plugin-persistedstate';

// 实例
const store = createPinia();

// 配置持久化
store.use(
  createPersistedState({
    key: (id) => `__APP__${id}__`.toUpperCase(),
  }),
);

/**
 * 初始化 Pinia
 */
const initStore = (app: App<Element>) => {
  return app.use(store);
};

export { store, initStore };

main.ts 文件中进行注册

import './styles/tailwind.css';
import { createApp } from 'vue'
import { initStore } from './store/init';
import App from './App.vue'

async function bootstrapApp() {
    const app = createApp(App);
    initStore(app);
    app.mount('#app');
}

bootstrapApp()

存储文件的配置

初始化之后,我们来进行存储文件的配置,在 modules 文件夹下新建一个 user.ts 文件,写入以下内容

import { computed, ref } from 'vue';
import { store } from '../init';
import { acceptHMRUpdate, defineStore } from 'pinia';

const createUserStore = defineStore('user', () => {
  const token = ref<string>('')

  /** 获取Token */
  const getToken = computed(() => {
    return token.value;
  });
  
  /** 设置Token */
  const setToken = (value: string) => {
    return token.value = value
  }
  return { getToken, setToken };
});

import.meta.hot && import.meta.hot.accept(acceptHMRUpdate(createUserStore, import.meta.hot));

/**
 * 注入 Pinia 实例,使其能在组件外使用
 * @see:https://pinia.vuejs.org/zh/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
 */
export const useUserStore = () => {
  return createUserStore(store);
};

上面的代码定义了一个 user 的存储文件,用来存储用户信息,可以是用户基本信息、token、权限、角色等,其中主要的配置项为:

  • state:数据源,存储数据的地方,可以被读取和修改,本文中是 token
  • getter:计算属性,Getter 完全等同于 store 的 state 的计算值,一般用于读取数据,本文中是 getToken
  • actions:方法,Actions相当于组件中的 method,可以包含任意的异步操作或同步操作,用于修改 state 或者定义业务逻辑,本文中是 setToken

上面使用的语法是 Setup Store,即组合式写法

对于使用过 Vuex 的用户来说,你肯定会熟悉选项式写法 ,即 Option Store,两种写法没有绝对的好坏之分,Option Store 更容易使用,而 Setup Store 更灵活和强大

Pinia Error

在上面的 user.ts 文件底部,有这么一段代码:

export const useUserStore = () => {
  return createUserStore(store);
};

为什么导出一个 useUserStore 方法,而不是直接导出 createUserStore 呢?

其实,是用于解决以下报错问题:

在官网文档中,有这样一段话

Pinia store 依靠 pinia 实例在所有调用中共享同一个 store 实例。大多数时候,只需调用你定义的 useStore() 函数,完全开箱即用。例如,在 setup() 中,你不需要再做任何事情。但在组件之外,情况就有点不同了。 实际上,useStore() 给你的 app 自动注入了 pinia 实例。这意味着,如果 pinia 实例不能自动注入,你必须手动提供给 useStore() 函数

针对这种情况,我们就可以创建一个包装函数,就如 useUserStore()
这个函数在内部调用 createUserStore(store),这里的 store 是 Pinia 实例,这样做可以解决

  • Pinia 实例未被初始化的问题
  • 在组件外使用 store 的问题

这样,我们就可以在整个应用中一致地使用 useUserStore() 方法

统一引入

注意,此章节是可选的

上面篇章中,我们使用 init.ts 注册配置了 Pinia,又在 modules 创建了一个 useUserStore,其实到这一步,你已经可以使用 useUserStore 了,比如:

这里的 @ 代表 src 目录,需要在 Vite 中配置

<script setup lang="ts">
import { useUserStore } from '@/store/modules/user';

defineOptions({
  name: 'Test',
});

const userStore = useUserStore();

console.log(userStore.getToken);
</script>

<template>
  <div>这里是测试</div>
</template>

但我们可以做的更好点,比如创建一个 index.ts 文件,并导出 modules 下所有存储,方便后续的引入使用,操作如下:

export * from './modules/user';
// ...引入更多

那么需要使用的时候,路径可以是这样写:

// import { useUserStore } from '@/store/modules/user'; (之前写法)
import { useUserStore } from '@/store';

Router 路由

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用 (SPA) 变得轻而易举

如果你是使用过 Vue2 的用户,那么你应该对 Vue Router 并不陌生,我们先来安装它

pnpm add vue-router@4

然后来对 Vue Router 进行配置,与上述的 Pinia 配置类似,创建一个 src/router 目录,表示这个目录下的内容都是与路由相关

里面新建 init.ts 文件、index.ts 文件 和 modules 文件夹

  • init.ts 用于 Router 的注册和基本配置
  • index.ts 用于对 modules 下的路由做处理,比如自动导出
  • modules 文件夹下存放的是全部路由配置文件

具体配置

我们先来基本配置一下 Vue Router,在 init.ts 中写入以下内容:

import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router';
import type { App } from 'vue';
import { routes } from './index';

export const router = createRouter({
  history: createWebHashHistory(),
  routes: routes as RouteRecordRaw[],
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
});

/** 初始化路由 */
export function initRouter(app: App<Element>) {
  app.use(router);
}

上面内容通过 createRouter() 函数创建了一个路由器实例,函数内有一些配置项:

  1. history 配置路由模式,分为三种模式:
    • createMemoryHistory()(不推荐),参阅 Menory-模式
    • createWebHashHistory(),Hash 模式,在URL上会有一个哈希字符 #,参阅Hash-模式
    • createWebHistory()(推荐),HTML5 模式,URL上无多余字符,参阅HTML5-模式
import { createMemoryHistory, createWebHashHistory, createWebHistory } from 'vue-router'
  1. routes 路由集,所有路由在这里注册(在动态路由中也可以通过实例上的方法 addRoute 来注册)
  2. strict,控制路由的路径最后是否包含斜线 /,即是否进行严格匹配
  3. scrollBehavior,定义滚动行为,参阅滚动行为

createRouter 注册完后,它会全局注册 RouterViewRouterLink 组件,启用 useRouter() 和 useRoute() 组合式函数等操作,请参阅 注册路由器插件

最后导出一个 initRouter() 函数,用于注册路由器

这时候,你会报 找不到模块“./index”或其相应的类型声明 问题,我们接着来配置 index.ts

index.ts

import type { RouteRecordRaw } from 'vue-router';

/** 基础路由 */
const basicRoutes: Record<string, any> = import.meta.glob(['./modules/basic/**/*.ts'], {
  eager: true,
});

const routes: RouteRecordRaw[] = [];

Object.keys(basicRoutes).forEach((key) => {
  routes.push(basicRoutes[key].default);
});

export { routes };

这里的 basicRoutes 会自动导入 modules/basic 下所有 TS 文件,包括嵌套的深度文件,核心实现在于 Vite 提供的 import.meta.glob 函数,参阅 Glob导入

什么,你的 modules 目录下没有 basic?是的,确实没有,请接着往下操作

接下来配置初始的页面,在 src 目录下新建一个 views 文件夹,里面存放的是页面文件

在刚刚新建的 views 中新建一个 home 文件夹,里面加入 index.vue ,代表路由初始化的页面

index.vue

<script setup lang="ts">
defineOptions({
  name: 'Home',
});
</script>

<template>
  <div><span class="title">这里是Home页面</span></div>
</template>

<style scoped lang="scss">
.title{
  font-size: 3rem;
}
</style>

记得把 App.vue 文件的默认内容清理并写入 RouterView,表示在这里渲染路由组件

到目前还没有在 main.ts 中注册路由,所以 RouterView 不会起效

App.vue

<script setup lang="ts"></script>

<template>
  <RouterView />
</template>

<style scoped></style>

写完了这个页面文件,我们就可以回头接着路由下配置它,在 modules 目录下新增一个 basic 文件夹,表示基础路由的存放地,然后新建一个 home.ts,写入以下内容

import type { RouteRecordRaw } from 'vue-router';

const Home: RouteRecordRaw = {
  path: '/',
  redirect: '/home',
  children: [
    {
      path: '/home',
      name: 'Home',
      component: () => import('@/views/home/index.vue'),
    },
  ],
};

export default Home;

最后一步,在 main.ts 中注册路由

import { createApp } from 'vue';
import { initStore } from '@/store/init';
import { initRouter } from '@/router/init';

import App from './App.vue';

async function bootstrapApp() {
  const app = createApp(App);
  initStore(app);
  initRouter(app);
  app.mount('#app');
}
bootstrapApp();

完成!到这一步,你应该可以看到页面上显示的文字了

实现页面:
Pasted image 20241024171052

最终的文件结构是
Pasted image 20241024173649

最后,如果你想要添加更多的路由页面,只需要在 modules 下新增配置文件即可生效,无须手动引入注册

了解更多

系列专栏地址:GitHub(推荐) | 掘金专栏 | 思否专栏

专栏往期回顾:

  1. 收下这份 Vue + TS + Vite 中后台系统搭建指南,从此不再害怕建项目

交流讨论

文章如有错误或需要改进之处,欢迎指正

更多高质量文章请关注 GitHub 博客,欢迎 star

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions