前端状态管理之Pinia库

Pinia 知识笔记

1. Pinia 简介

Pinia 是 Vue 的专属状态管理库,是 Vuex 的替代品,由 Vue 核心团队推荐在 Vue 3 项目中使用。Pinia 最初于 2019 年 11 月左右开始设计,目的是创建一个支持组合式 API 的 Vue 状态管理库。

Pinia 的主要特点:

  • 同时支持 Vue 2 和 Vue 3
  • 支持 TypeScript
  • 支持 Vue DevTools
  • 支持服务端渲染 (SSR)
  • 支持热更新 (HMR)
  • 简化的 API,去掉了 Vuex 中 mutations 和 actions 的区分
  • 更好的组合式 API 支持

为什么选择 Pinia 而非 Vuex?

  • 更简单的 API,更少的样板代码
  • 更好的 TypeScript 支持
  • 不需要嵌套模块,扁平化的 Store 结构
  • 支持多个 Store,不需要命名空间

2. 安装和配置

安装 Pinia

npm install pinia
# 或
yarn add pinia

在 Vue 3 项目中配置 Pinia

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

3. Pinia 核心概念

3.1 Store

Store 是一个保存状态和业务逻辑的实体,它不与组件树绑定,可以在整个应用中访问。Store 有三个核心概念:

  • state: 存储数据的地方(相当于组件中的refreactive
  • getters: 计算属性(相当于组件中的 computed)
  • actions: 可执行的方法(相当于组件中的函数)

3.2 使用组合式 API 定义一个用户状态的Store

import { getCurrentUser } from "@/api/user"; 
import { defineStore } from "pinia"; 
import { ref } from "vue"; 

export const useLoginUserStore = defineStore("loginUser", () => { 
  const loginUser = ref({ 
    id: "", 
    userRole: 0, 
    userName: "未登录", 
    userAccount: "", 
    avatarUrl: "", 
  }); 

  const initialized = ref(false); 

  //远程获取登录用户信息 
  async function fetchLoginUser() { 
    try { 
      const res = await getCurrentUser(); 
      if (res.data.code === 0) { 
        loginUser.value = res.data.data; 
      } 
      initialized.value = true; 
    } catch (error) { 
      initialized.value = true; 
      throw error; 
    } 
  } 

  //单独设置登录用户信息 
  function setLoginUser(newLoginUser: any) { 
    loginUser.value = newLoginUser; 
  } 

  return { loginUser, fetchLoginUser, setLoginUser, initialized }; 
});

3.2.1 代码要点解释

  • Store定义

    export const useLoginUserStore = defineStore("loginUser", () => {
      // Store 内容
    });
    
    • defineStore 是 Pinia 的核心函数,用于创建一个 Store

    • 第一个参数 loginUser 是 Store 的唯一 ID

    • 第二个参数是一个函数,使用组合式 API 风格定义 Store

  • State定义

    const loginUser = ref({ 
      id: "", 
      userRole: 0, 
      userName: "未登录", 
      userAccount: "", 
      avatarUrl: "", 
    }); 
    
    const initialized = ref(false);
    
    • 使用 Vue 的 ref() 函数定义响应式状态
    • 每个 ref 变量都会成为 Store 的一个状态属性
    • 初始值设置为默认值
    • initialized 用于跟踪用户信息是否已初始化
  • Actions定义

    //远程获取登录用户信息 
    async function fetchLoginUser() { 
      try { 
        const res = await getCurrentUser(); 
        if (res.data.code === 0) { 
          loginUser.value = res.data.data; 
        } 
        initialized.value = true; 
      } catch (error) { 
        initialized.value = true; 
        throw error; 
      } 
    } 
    
    //单独设置登录用户信息 
    function setLoginUser(newLoginUser: any) { 
      loginUser.value = newLoginUser; 
    }
    
    • 定义了两个actions: fetchLoginUsersetLoginUser
    • fetchLoginUser 是一个异步函数,用于从 API 获取当前登录用户信息
    • setLoginUser 是一个同步函数,用于手动设置用户信息
    • 两个函数都直接修改 state(loginUser.value),这是Pinia 的特点,不需要像 Vuex 那样通过 mutations
  • 返回

    return { loginUser, fetchLoginUser, setLoginUser, initialized };
    
    • 返回所有需要暴露的状态和方法
    • 这些属性和方法将可以在组件中访问

3.3 Store ID

每个 Store 都需要一个唯一的 ID,这个 ID 用于区分不同的 Store,也用于持久化等功能。

4. 在组件中使用 Store

<script setup>
import { useLoginUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';

// 获取 store 实例
const loginUserStore = useLoginUserStore();

// 解构 store 的状态 (按照业务需求是否需要解构)
const { loginUser, initialized } = storeToRefs(loginUserStore);

// 直接解构 actions (不需要 storeToRefs)
const { fetchLoginUser, setLoginUser } = loginUserStore;

// 组件挂载时获取用户信息
onMounted(async () => {
  if (!initialized.value) {
    try {
      await fetchLoginUser();
    } catch (error) {
      console.error('获取用户信息失败:', error);
    }
  }
});
</script>

<template>
  <div>
    <p v-if="!initialized">加载中...</p>
    <div v-else>
      <p>用户名: {{ loginUser.userName }}</p>
      <p>账号: {{ loginUser.userAccount }}</p>
      <img v-if="loginUser.avatarUrl" :src="loginUser.avatarUrl" alt="头像" />
    </div>
  </div>
</template>

4.1 修改用户信息

<script setup>
import { useLoginUserStore } from '@/stores/user';

const loginUserStore = useLoginUserStore();

function updateUsername(newName) {
  // 方法 1: 使用 setLoginUser 更新整个用户对象
  const updatedUser = { ...loginUserStore.loginUser, userName: newName };
  loginUserStore.setLoginUser(updatedUser);
  
  // 方法 2: 直接修改 state
  // loginUserStore.loginUser.userName = newName;
  
  // 方法 3: 使用 $patch 方法
  // loginUserStore.$patch({
  //   loginUser: { ...loginUserStore.loginUser, userName: newName }
  // });
}
</script>

<template>
  <div>
    <input 
      v-model="loginUserStore.loginUser.userName" 
      placeholder="修改用户名"
    />
    <button @click="updateUsername('新用户名')">更新用户名</button>
  </div>
</template>

5. Getters

5.1 定义 Getters

import { getCurrentUser } from "@/api/user"; 
import { defineStore } from "pinia"; 
import { ref, computed } from "vue"; 

export const useLoginUserStore = defineStore("loginUser", () => { 
  const loginUser = ref({ 
    id: "", 
    userRole: 0, 
    userName: "未登录", 
    userAccount: "", 
    avatarUrl: "", 
  }); 

  const initialized = ref(false); 

  // Getters
  const isLoggedIn = computed(() => !!loginUser.value.id);
  const isAdmin = computed(() => loginUser.value.userRole === 1);
  const displayName = computed(() => 
    loginUser.value.userName || loginUser.value.userAccount || "未登录用户"
  );

  //远程获取登录用户信息 
  async function fetchLoginUser() { 
    try { 
      const res = await getCurrentUser(); 
      if (res.data.code === 0) { 
        loginUser.value = res.data.data; 
      } 
      initialized.value = true; 
    } catch (error) { 
      initialized.value = true; 
      throw error; 
    } 
  } 

  //单独设置登录用户信息 
  function setLoginUser(newLoginUser: any) { 
    loginUser.value = newLoginUser; 
  } 

  // 登出
  function logout() {
    loginUser.value = { 
      id: "", 
      userRole: 0, 
      userName: "未登录", 
      userAccount: "", 
      avatarUrl: "", 
    };
  }

  return { 
    loginUser, 
    initialized, 
    isLoggedIn, 
    isAdmin, 
    displayName, 
    fetchLoginUser, 
    setLoginUser, 
    logout 
  }; 
});

5.2 持久化

import { getCurrentUser } from "@/api/user"; 
import { defineStore } from "pinia"; 
import { ref, computed, watch } from "vue"; 

export const useLoginUserStore = defineStore("loginUser", () => { 
  // 从本地存储加载初始状态
  const savedUser = localStorage.getItem('loginUser');
  
  const loginUser = ref(savedUser ? JSON.parse(savedUser) : { 
    id: "", 
    userRole: 0, 
    userName: "未登录", 
    userAccount: "", 
    avatarUrl: "", 
  }); 

  const initialized = ref(!!savedUser);

  // 监听用户信息变化并保存到本地存储
  watch(
    loginUser,
    (newValue) => {
      localStorage.setItem('loginUser', JSON.stringify(newValue));
    },
    { deep: true }
  );

  // Getters
  const isLoggedIn = computed(() => !!loginUser.value.id);
  
  // 其他代码保持不变...
  
  return { 
    // 返回值保持不变...
  }; 
});

6. 插件系统

Pinia 支持插件系统,可以扩展 Store 的功能。

6.1 创建插件

// plugins/persistPlugin.js
export function persistPlugin({ store }) {
  // 从 localStorage 恢复状态
  const storedState = localStorage.getItem(`pinia-${store.$id}`)
  
  if (storedState) {
    store.$patch(JSON.parse(storedState))
  }
  
  // 监听状态变化并保存到 localStorage
  store.$subscribe((mutation, state) => {
    localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
  })
}

6.2 使用插件

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { persistPlugin } from './plugins/persistPlugin'

const pinia = createPinia()
pinia.use(persistPlugin)

const app = createApp(App)
app.use(pinia)
app.mount('#app')

7. 订阅 Store 变化(进阶技巧)

7.1 订阅 State 变化

import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

// 订阅状态变化
const unsubscribe = counterStore.$subscribe((mutation, state) => {
  // mutation 包含:
  // - type: 'direct' | 'patch object' | 'patch function'
  // - storeId: store 的 id
  // - payload: 传递给 patch 的参数
  
  console.log('State changed:', mutation, state)
  
  // 可以在这里执行任何操作,如持久化到本地存储
  localStorage.setItem('counter', JSON.stringify(state))
})

// 停止订阅
// unsubscribe()

7.2 订阅 Actions

const unsubscribe = counterStore.$onAction({
  before: (actionName, store, args) => {
    // action 执行前调用
    console.log(`${actionName} 即将被调用,参数:`, args)
  },
  after: (actionName, store, args, result) => {
    // action 成功执行后调用
    console.log(`${actionName} 被调用,结果:`, result)
  },
  error: (actionName, store, args, error) => {
    // action 抛出错误时调用
    console.error(`${actionName} 出错:`, error)
  },
})

// 停止订阅
// unsubscribe()

8. 实际应用示例

8.1 Store 目录结构

src/
  stores/
    index.ts          # 导出所有 store
    user.ts           # 用户相关 store
    settings.ts       # 应用设置 store
    notification.ts   # 通知 store

8.2 在 index.ts 中统一导出

// src/stores/index.ts
import { useLoginUserStore } from './user';
import { useSettingsStore } from './settings';
import { useNotificationStore } from './notification';

export {
  useLoginUserStore,
  useSettingsStore,
  useNotificationStore
};

8.3 Store 之间的交互

// src/stores/notification.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { useLoginUserStore } from './user';

export const useNotificationStore = defineStore('notification', () => {
  const notifications = ref([]);
  
  async function fetchUserNotifications() {
    const userStore = useLoginUserStore();
    
    if (!userStore.isLoggedIn) {
      return [];
    }
    
    // 获取用户通知的逻辑
    // ...
  }
  
  return {
    notifications,
    fetchUserNotifications
  };
});

9. 标准用法

  1. 按功能模块拆分 Store:每个功能模块创建独立的 Store 文件
  2. 使用组合式 API 风格:更符合 Vue 3 的设计理念
  3. 使用 TypeScript:获得更好的类型提示和错误检查
  4. 避免过度使用 Store:不是所有状态都需要放在全局 Store 中
  5. 合理使用 getters:对于需要计算的派生状态,使用 getters 而非直接在组件中计算
  6. 在 actions 中集中处理业务逻辑:保持组件的简洁性
  7. 使用 storeToRefs 解构:保持响应性
  8. 使用 $reset() 重置状态:需要恢复初始状态时使用

10. 调试技巧

  • 查看和修改 Store 状态
    • 使用 store.$state 查看完整状态
    • 使用 store.$id 获取 Store 的唯一标识符
    • 使用 store.$reset() 重置状态

欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub