Vue Pinia 状态管理进阶:模块化与持久化

春秋大王2025-10-1058 次阅读
Vue Pinia 状态管理进阶:模块化与持久化


Pinia 作为 Vue3 官方状态管理库,支持模块化拆分、异步操作、状态持久化等高级功能,解决了 Vuex 的复杂语法问题,以下是企业级项目中的进阶用法:​

(1)模块化状态管理(按业务拆分 Store)​

按业务领域拆分 Store(如用户、购物车、商品),使状态管理更清晰,便于维护。

// 1. 用户Store(src/store/modules/userStore.js)
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi, logoutApi } from '@/api/user'  // 模拟API请求

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '',  // 从localStorage初始化
    userInfo: JSON.parse(localStorage.getItem('userInfo')) || {},
    isLogin: !!localStorage.getItem('token')  // 布尔值:是否登录
  }),
  getters: {
    // 计算用户是否为管理员
    isAdmin: (state) => state.userInfo.role === 'admin',
    // 获取用户昵称(优先显示昵称,无则显示用户名)
    userNickname: (state) => state.userInfo.nickname || state.userInfo.username
  },
  actions: {
    // 异步登录:调用API获取token和用户信息
    async login(userData) {
      try {
        const { data } = await loginApi(userData)  // 调用登录API
        const { token } = data
        // 存储token
        this.token = token
        localStorage.setItem('token', token)
        // 获取用户信息
        await this.fetchUserInfo()
        // 更新登录状态
        this.isLogin = true
        return Promise.resolve()  // 登录成功
      } catch (error) {
        console.error('登录失败:', error)
        return Promise.reject(error)  // 登录失败,抛出错误
      }
    },
    // 异步获取用户信息
    async fetchUserInfo() {
      try {
        const { data } = await getUserInfoApi()  // 调用获取用户信息API
        this.userInfo = data
        localStorage.setItem('userInfo', JSON.stringify(data))
      } catch (error) {
        console.error('获取用户信息失败:', error)
        // 信息获取失败,清除token并退出登录
        this.logout()
      }
    },
    // 异步退出登录
    async logout() {
      try {
        await logoutApi()  // 调用退出登录API(通知后端销毁token)
      } finally {
        // 无论API是否成功,都清除本地状态
        this.token = ''
        this.userInfo = {}
        this.isLogin = false
        localStorage.removeItem('token')
        localStorage.removeItem('userInfo')
      }
    }
  }
})

// 2. 购物车Store(src/store/modules/cartStore.js)
import { defineStore } from 'pinia'
import { getCartListApi, addCartApi, removeCartApi, updateCartApi } from '@/api/cart'

export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: [],  // 购物车列表
    totalCount: 0,  // 购物车商品总数
    totalPrice: 0   // 购物车商品总价
  }),
  getters: {
    // 计算是否全选(所有商品都选中则返回true)
    isAllSelected: (state) => {
      if (state.cartList.length === 0) return false
      return state.cartList.every(item => item.selected)
    },
    // 计算选中商品的数量
    selectedCount: (state) => {
      return state.cartList.filter(item => item.selected).reduce((sum, item) => sum + item.quantity, 0)
    },
    // 计算选中商品的总价
    selectedPrice: (state) => {
      return state.cartList.filter(item => item.selected).reduce((sum, item) => sum + (item.price * item.quantity), 0).toFixed(2)
    }
  },
  actions: {
    // 初始化购物车:获取购物车列表
    async initCart() {
      try {
        const { data } = await getCartListApi()
        this.cartList = data.list || []
        // 更新购物车总数和总价
        this.calcTotal()
      } catch (error) {
        console.error('获取购物车列表失败:', error)
      }
    },
    // 计算购物车总数和总价
    calcTotal() {
      this.totalCount = this.cartList.reduce((sum, item) => sum + item.quantity, 0)
      this.totalPrice = this.cartList.reduce((sum, item) => sum + (item.price * item.quantity), 0).toFixed(2)
    },
    // 新增商品到购物车
    async addToCart(productData) {
      try {
        await addCartApi(productData)  // 调用添加购物车API
        await this.initCart()  // 重新获取购物车列表,更新状态
        return Promise.resolve('添加成功')
      } catch (error) {
        console.error('添加购物车失败:', error)
        return Promise.reject(error)
      }
    },
    // 移除购物车商品
    async removeFromCart(cartId) {
      try {
        await removeCartApi(cartId)  // 调用删除购物车API
        await this.initCart()  // 重新获取购物车列表
      } catch (error) {
        console.error('删除购物车商品失败:', error)
      }
    },
    // 更新购物车商品(数量/选中状态)
    async updateCart(cartId, updateData) {
      try {
        await updateCartApi(cartId, updateData)  // 调用更新购物车API
        // 本地临时更新状态(优化体验,避免等待API响应)
        const index = this.cartList.findIndex(item => item.id === cartId)
        if (index !== -1) {
          this.cartList[index] = { ...this.cartList[index], ...updateData }
          this.calcTotal()  // 重新计算总数和总价
        }
      } catch (error) {
        console.error('更新购物车商品失败:', error)
        await this.initCart()  // 失败后重新获取,确保状态一致
      }
    },
    // 全选/取消全选
    async toggleAllSelect(isSelected) {
      try {
        // 批量更新所有商品的选中状态
        const updatePromises = this.cartList.map(item => 
          updateCartApi(item.id, { selected: isSelected })
        )
        await Promise.all(updatePromises)  // 等待所有更新完成
        // 本地更新状态
        this.cartList.forEach(item => item.selected = isSelected)
        this.calcTotal()
      } catch (error) {
        console.error('全选操作失败:', error)
        await this.initCart()
      }
    }
  }
})

// 3. 统一导出Store(src/store/index.js)
export * from './modules/userStore'
export * from './modules/cartStore'

(2)Pinia 状态持久化(避免刷新丢失)​

虽然可通过localStorage手动存储状态,但使用pinia-plugin-persistedstate插件能更优雅地实现持久化,支持指定字段、存储介质(localStorage/sessionStorage)。

# 安装持久化插件
npm install pinia-plugin-persistedstate
// 1. 配置Pinia与持久化插件(src/main.js)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'  // 引入插件
import App from './App.vue'
import router from './router'

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

// 安装持久化插件
pinia.use(piniaPluginPersistedstate)

app.use(pinia)
app.use(router)
app.mount('#app')
// 2. 在Store中配置持久化(以用户Store为例)
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: {},
    isLogin: false
  }),
  // 新增persist配置:实现状态持久化
  persist: {
    key: 'user-store',  // 存储的键名(默认是store的id)
    storage: localStorage,  // 存储介质:localStorage(默认)或sessionStorage
    paths: ['token', 'userInfo']  // 指定需要持久化的字段(默认所有字段)
    // 排除字段:可通过不列入paths实现,如不持久化isLogin
  },
  // ... 其余getters、actions不变
})

// 购物车Store同理配置持久化
export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: [],
    totalCount: 0,
    totalPrice: 0
  }),
  persist: {
    key: 'cart-store',
    storage: localStorage,
    paths: ['cartList']  // 仅持久化购物车列表,总数和总价可通过计算获取
  },
  // ... 其余getters、actions不变
})

(3)组件中使用多模块 Store

<template>
  <div class="cart-page">
    <h3>我的购物车</h3>
    <!-- 未登录提示 -->
    <div v-if="!userStore.isLogin" class="login-tip">
      <p>请先<a @click="goToLogin">登录</a>查看购物车</p>
    </div>

    <!-- 购物车列表(已登录) -->
    <div v-else class="cart-container">
      <!-- 全选与统计 -->
      <div class="cart-header">
        <label>
          <input 
            type="checkbox" 
            v-model="cartStore.isAllSelected" 
            @change="cartStore.toggleAllSelect($event.target.checked)"
          >
          全选
        </label>
        <div class="cart-stats">
          <p>已选商品:{{ cartStore.selectedCount }}件</p>
          <p>总价:¥{{ cartStore.selectedPrice }}</p>
          <button class="checkout-btn" :disabled="cartStore.selectedCount === 0">
            去结算
          </button>
        </div>
      </div>

      <!-- 购物车商品列表 -->
      <div class="cart-list">
        <div class="cart-item" v-for="item in cartStore.cartList" :key="item.id">
          <label>
            <input 
              type="checkbox" 
              v-model="item.selected" 
              @change="cartStore.updateCart(item.id, { selected: item.selected })"
            >
          </label>
          <img :src="item.imgUrl" alt="商品图片" class="item-img">
          <div class="item-info">
            <p class="item-name">{{ item.name }}</p>
            <p class="item-price">¥{{ item.price }}</p>
          </div>
          <div class="item-quantity">
            <button @click="cartStore.updateCart(item.id, { quantity: item.quantity - 1 })" :disabled="item.quantity <= 1">
              -
            </button>
            <span>{{ item.quantity }}</span>
            <button @click="cartStore.updateCart(item.id, { quantity: item.quantity + 1 })">
              +
            </button>
          </div>
          <button class="item-remove" @click="cartStore.removeFromCart(item.id)">
            删除
          </button>
        </div>
      </div>

      <!-- 空购物车提示 -->
      <div v-if="cartStore.cartList.length === 0" class="empty-cart">
        <p>购物车是空的,去<a @click="goToHome">首页</a>添加商品吧~</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from '@/store/modules/userStore'
import { useCartStore } from '@/store/modules/cartStore'
import { useRouter } from 'vue-router'
import { onMounted } from 'vue'

// 获取Store实例
const userStore = useUserStore()
const cartStore = useCartStore()
const router = useRouter()

// 组件挂载时初始化购物车(仅已登录)
onMounted(() => {
  if (userStore.isLogin) {
    cartStore.initCart()
  }
})

// 跳转登录页
const goToLogin = () => {
  router.push('/login')
}

// 跳转首页
const goToHome = () => {
  router.push('/')
}
</script>

<style scoped>
.cart-page {
  max-width: 1200px;
  margin: 20px auto;
  padding: 0 20px;
}
.login-tip {
  padding: 50px;
  text-align: center;
  color: #666;
}
.login-tip a {
  color: #42b983;
  cursor: pointer;
  text-decoration: underline;
}
.cart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #eee;
}
.cart-stats {
  display: flex;
  gap: 20px;
  align-items: center;
}
.checkout-btn {
  padding: 8px 20px;
  background: #ff4444;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
.checkout-btn:disabled {
  background: #ccc;
  cursor: not-allowed;
}
.cart-list {
  margin-top: 20px;
}
.cart-item {
  display: flex;
  align-items: center;
  padding: 15px 0;
  border-bottom: 1px solid #f5f5f5;
  gap: 20px;
}
.item-img {
  width: 80px;
  height: 80px;
  object-fit: cover;
  border-radius: 4px;
}
.item-info {
  flex: 1;
}
.item-name {
  margin: 0 0 10px;
  color: #333;
}
.item-price {
  margin: 0;
  color: #ff4444;
}
.item-quantity {
  display: flex;
  align-items: center;
  gap: 10px;
}
.item-quantity button {
  width: 30px;
  height: 30px;
  border: 1px solid #eee;
  background: white;
  cursor: pointer;
  border-radius: 4px;
}
.item-quantity button:disabled {
  color: #ccc;
  cursor: not-allowed;
}
.item-remove {
  color: #666;
  border: none;
  background: transparent;
  cursor: pointer;
}
.item-remove:hover {
  color: #ff4444;
}
.empty-cart {
  padding: 50px;
  text-align: center;
  color: #666;
}
.empty-cart a {
  color: #42b983;
  cursor: pointer;
  text-decoration: underline;
}
</style>