头图

前言

本篇文章主要讲解如何来配置 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'

2) routes 路由集,所有路由在这里注册(在动态路由中也可以通过实例上的方法 addRoute 来注册)
3) strict,控制路由的路径最后是否包含斜线 /,即是否进行严格匹配
4) 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

十五
4 声望1 粉丝