学习:Pinia(1)

Pinia简介与特点

Pinia是Vue的官方状态管理库,也是Vuex的继任者,专门为Vue 3设计,但也可以与Vue 2一起使用。

Pinia的主要特点:

  • 完整的TypeScript支持
  • 极其简单的API设计
  • 模块化设计,每个store都是独立的
  • 没有mutations,只有state、getters和actions
  • 支持Vue Devtools调试
  • 支持插件扩展
  • 与Vue Composition API完美集成
  • 支持服务端渲染(SSR)

Pinia安装与配置

安装Pinia:

复制代码
# 使用npm
npm install pinia

# 使用yarn
yarn add pinia

# 使用pnpm
pnpm add pinia

在main.js中配置:

javascript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

// 创建Vue应用实例
const app = createApp(App)

// 创建Pinia实例
const pinia = createPinia()

// 添加持久化插件
pinia.use(piniaPluginPersistedstate)

// 注册Pinia
app.use(pinia)

app.mount('#app')

Store的定义方式

Pinia提供了两种定义store的方式:选项式API和组合式API。

选项式API (Options API)
javascript 复制代码
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // 相当于组件中的data
  state: () => ({
    count: 0,
    name: 'Pinia Counter'
  }),
  
  // 相当于组件中的computed
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  
  // 相当于组件中的methods
  actions: {
    increment() {
      this.count++
    },
    reset() {
      this.count = 0
    },
    async fetchData() {
      // 异步操作示例
      const response = await fetch('https://api.example.com/data')
      const data = await response.json()
      this.name = data.name
    }
  },
  
  // 持久化配置
  persist: {
    key: 'my-counter',
    storage: localStorage,
    paths: ['count'] // 只持久化count
  }
})
组合式API (Composition API)
javascript 复制代码
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const name = ref('Pinia Counter')
  
  // getters
  const doubleCount = computed(() => count.value * 2)
  const doubleCountPlusOne = computed(() => doubleCount.value + 1)
  
  // actions
  function increment() {
    count.value++
  }
  
  function reset() {
    count.value = 0
  }
  
  async function fetchData() {
    const response = await fetch('https://api.example.com/data')
    const data = await response.json()
    name.value = data.name
  }
  
  return {
    count,
    name,
    doubleCount,
    doubleCountPlusOne,
    increment,
    reset,
    fetchData
  }
},
{
  persist: {
    key: 'my-counter',
    storage: localStorage,
    paths: ['count']
  }
})

Store的使用方法

在组件中使用Store:

html 复制代码
<template>
  <div>
    <h1>{{ name }}</h1>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <p>Double Count + 1: {{ doubleCountPlusOne }}</p>
    <button @click="increment">+</button>
    <button @click="reset">Reset</button>
    <button @click="fetchData">Fetch Data</button>
  </div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

// 获取store实例
const counterStore = useCounterStore()

// 解构保持响应性
const { count, name, doubleCount, doubleCountPlusOne } = storeToRefs(counterStore)

// 调用actions
const { increment, reset, fetchData } = counterStore
</script>

不使用storeToRefs的情况(会失去响应性):

javascript 复制代码
// ❌ 错误方式 - 会失去响应性
const { count, name } = useCounterStore()

// ✅ 正确方式 - 直接使用store
const counterStore = useCounterStore()
// 模板中通过counterStore.count访问

在选项式API中使用:

javascript 复制代码
<script>
import { useCounterStore } from '@/stores/counter'
import { mapState, mapActions } from 'pinia'

export default {
  computed: {
    // 使用映射函数访问state
    ...mapState(useCounterStore, ['count', 'name']),
    
    // 或者使用更复杂的映射
    ...mapState(useCounterStore, {
      myCount: 'count',
      myName: 'name',
      doubleCount: store => store.doubleCount
    })
  },
  
  methods: {
    // 映射actions
    ...mapActions(useCounterStore, ['increment', 'reset', 'fetchData']),
    
    // 或者直接调用
    customIncrement() {
      const counterStore = useCounterStore()
      counterStore.increment()
    }
  }
}
</script>

State、Getters、Actions详解

State详解

定义State:

javascript 复制代码
export const useUserStore = defineStore('user', () => {
  // 使用ref定义基础类型
  const isLoggedIn = ref(false)
  const userId = ref(null)
  
  // 使用reactive定义对象
  const userInfo = reactive({
    name: '',
    email: '',
    avatar: '',
    roles: []
  })
  
  // 使用ref定义数组
  const permissions = ref([])
  
  return {
    isLoggedIn,
    userId,
    userInfo,
    permissions
  }
})

修改State:

javascript 复制代码
// 直接修改
const userStore = useUserStore()
userStore.isLoggedIn = true
userStore.userInfo.name = 'John Doe'

// 通过$patch批量修改
userStore.$patch({
  isLoggedIn: true,
  userId: 123,
  userInfo: {
    name: 'John Doe',
    email: 'john@example.com'
  }
})

// 通过函数形式批量修改(适合复杂逻辑)
userStore.$patch((state) => {
  state.isLoggedIn = true
  state.userInfo.name = 'John Doe'
  state.permissions.push('admin')
})

// 通过action修改
userStore.login({
  userId: 123,
  userInfo: {
    name: 'John Doe',
    email: 'john@example.com'
  }
})

重置State:

javascript 复制代码
userStore.$reset() // 重置为初始值

Getters详解

基础Getters:

javascript 复制代码
export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  
  // 基础getter - 接收state作为参数
  const itemCount = computed(() => items.value.length)
  
  // 可缓存的getter - 只在依赖项变化时重新计算
  const totalPrice = computed(() => {
    return items.value.reduce((total, item) => {
      return total + (item.price * item.quantity)
    }, 0)
  })
  
  return {
    items,
    itemCount,
    totalPrice
  }
})

带参数的Getters:

javascript 复制代码
export const useProductStore = defineStore('product', () => {
  const products = ref([])
  
  // 返回函数,可以接收参数
  const getProductById = computed(() => {
    return (productId) => products.value.find(product => product.id === productId)
  })
  
  // 或者使用箭头函数
  const getProductsByCategory = computed(() => {
    return (category) => products.value.filter(product => product.category === category)
  })
  
  return {
    products,
    getProductById,
    getProductsByCategory
  }
})

// 使用
const productStore = useProductStore()
const product = productStore.getProductById(123)
const electronics = productStore.getProductsByCategory('electronics')

在Getters中使用其他Store:

javascript 复制代码
import { useUserStore } from './user'

export const useOrderStore = defineStore('order', () => {
  const userStore = useUserStore()
  
  const orders = ref([])
  
  const userOrders = computed(() => {
    // 过滤当前用户的订单
    return orders.value.filter(order => order.userId === userStore.userId)
  })
  
  return {
    orders,
    userOrders
  }
})

Actions详解

同步Actions:

javascript 复制代码
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const history = ref([])
  
  function increment(amount = 1) {
    count.value += amount
    history.value.push(`Added ${amount}`)
  }
  
  function decrement(amount = 1) {
    count.value -= amount
    history.value.push(`Subtracted ${amount}`)
  }
  
  return {
    count,
    history,
    increment,
    decrement
  }
})

异步Actions:

javascript 复制代码
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const isLoading = ref(false)
  const error = ref(null)
  
  async function fetchUser(userId) {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await fetch(`https://api.example.com/users/${userId}`)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const data = await response.json()
      user.value = data
    } catch (err) {
      error.value = err.message
      console.error('Failed to fetch user:', err)
    } finally {
      isLoading.value = false
    }
  }
  
  async function updateUser(userData) {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await fetch(`https://api.example.com/users/${user.value.id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(userData),
      })
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      const updatedUser = await response.json()
      user.value = updatedUser
    } catch (err) {
      error.value = err.message
      console.error('Failed to update user:', err)
    } finally {
      isLoading.value = false
    }
  }
  
  return {
    user,
    isLoading,
    error,
    fetchUser,
    updateUser
  }
})

在Actions中调用其他Store的Actions:

javascript 复制代码
import { useNotificationStore } from './notification'

export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const notificationStore = useNotificationStore()
  
  async function login(credentials) {
    try {
      // 登录逻辑...
      
      // 通知其他store
      notificationStore.showNotification('Login successful', 'success')
    } catch (error) {
      notificationStore.showNotification('Login failed', 'error')
    }
  }
  
  return {
    user,
    login
  }
})

Store的组合与模块化

Store之间的通信:

javascript 复制代码
// user.js
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const permissions = ref([])
  
  function setUserInfo(userData) {
    user.value = userData
  }
  
  return { user, permissions, setUserInfo }
})

// product.js
export const useProductStore = defineStore('product', () => {
  const products = ref([])
  const userStore = useUserStore()
  
  function getAccessibleProducts() {
    // 根据用户权限过滤产品
    return products.value.filter(product => 
      userStore.permissions.value.includes(product.accessLevel)
    )
  }
  
  return { products, getAccessibleProducts }
})

模块化组织结构:

javascript 复制代码
src/stores/
  ├── index.js           # 导出所有store
  ├── modules/
  │   ├── user.js        # 用户相关store
  │   ├── product.js     # 产品相关store
  │   └── cart.js        # 购物车相关store
  └── plugins/
      └── persist.js     # 持久化插件配置

使用动态Store名称:

javascript 复制代码
// 为每个用户创建独立的store
export const createUserStore = (userId) => {
  return defineStore(`user-${userId}`, () => {
    const user = ref(null)
    const posts = ref([])
    
    async function fetchUserData() {
      const response = await fetch(`/api/users/${userId}`)
      const userData = await response.json()
      user.value = userData
    }
    
    return {
      user,
      posts,
      fetchUserData
    }
  })
}

// 使用
const UserStoreFactory = createUserStore(123)
const userStore = UserStoreFactory()
await userStore.fetchUserData()

Pinia持久化插件

基础持久化配置:

javascript 复制代码
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const token = ref('')
  
  function login(credentials) {
    // 登录逻辑...
    token.value = 'your-token'
  }
  
  function logout() {
    user.value = null
    token.value = ''
  }
  
  return {
    user,
    token,
    login,
    logout
  }
}, {
  // 简单配置 - 持久化整个store到localStorage
  persist: true
})

高级持久化配置:

javascript 复制代码
export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  const totalItems = ref(0)
  const checkoutDate = ref(null)
  
  function addItem(item) {
    items.value.push(item)
    totalItems.value++
  }
  
  function clearCart() {
    items.value = []
    totalItems.value = 0
    checkoutDate.value = null
  }
  
  return {
    items,
    totalItems,
    checkoutDate,
    addItem,
    clearCart
  }
}, {
  persist: {
    key: 'my-cart',             // 自定义key,默认为store id
    storage: localStorage,     // 存储方式,默认localStorage
    paths: ['items'],           // 只持久化items和totalItems
    // 序列化函数
    serializer: {
      serialize: JSON.stringify,
      deserialize: JSON.parse,
    },
    // 自定义恢复逻辑
    beforeRestore: (context) => {
      console.log('即将恢复store', context)
    },
    afterRestore: (context) => {
      console.log('已恢复store', context)
      // 可以在这里添加额外的逻辑
      const cartStore = context.store
      // 计算持久化后的totalItems
      cartStore.totalItems = cartStore.items.length
    }
  }
})

使用sessionStorage:

javascript 复制代码
export const useFormStore = defineStore('form', () => {
  const formData = ref({})
  
  function updateField(field, value) {
    formData.value[field] = value
  }
  
  return {
    formData,
    updateField
  }
}, {
  persist: {
    key: 'temp-form',
    storage: sessionStorage,    // 使用sessionStorage,关闭浏览器后清除
  }
})

自定义存储适配器:

javascript 复制代码
// 使用cookies存储
import Cookies from 'js-cookie'

export const useAuthStore = defineStore('auth', () => {
  const token = ref('')
  const user = ref(null)
  
  function setToken(newToken) {
    token.value = newToken
  }
  
  return {
    token,
    user,
    setToken
  }
}, {
  persist: {
    key: 'auth-token',
    storage: {
      getItem: key => Cookies.get(key),
      setItem: (key, value) => Cookies.set(key, value, { expires: 7 }),
      removeItem: key => Cookies.remove(key)
    },
    paths: ['token'] // 只持久化token
  }
})

Pinia与Vue3的响应式集成

在组合式API中使用:

javascript 复制代码
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { computed, watch } from 'vue'

const userStore = useUserStore()

// 使用storeToRefs保持响应性
const { user, isLoggedIn, permissions } = storeToRefs(userStore)

// 派生状态
const isAdmin = computed(() => {
  return permissions.value.includes('admin')
})

// 监听store变化
watch(() => userStore.isLoggedIn, (newValue) => {
  console.log('登录状态变化:', newValue)
  // 可以在这里执行导航等操作
})

// 生命周期钩子中
onMounted(() => {
  if (!userStore.isLoggedIn) {
    userStore.checkAuthStatus()
  }
})
</script>

在选项式API中使用:

javascript 复制代码
<script>
import { defineComponent } from 'vue'
import { useUserStore } from '@/stores/user'
import { mapState, mapGetters, mapActions } from 'pinia'

export default defineComponent({
  data() {
    return {
      localData: '本地数据'
    }
  },
  
  computed: {
    // 映射state
    ...mapState(useUserStore, ['user', 'isLoggedIn']),
    
    // 映射getters
    ...mapGetters(useUserStore, ['isAdmin', 'permissions']),
    
    // 本地计算属性
    fullName() {
      return this.user ? `${this.user.firstName} ${this.user.lastName}` : ''
    }
  },
  
  methods: {
    // 映射actions
    ...mapActions(useUserStore, ['login', 'logout', 'fetchUserData']),
    
    // 本地方法
    handleLogin() {
      this.login(this.credentials)
        .then(() => {
          // 登录成功
        })
        .catch(error => {
          // 处理错误
        })
    }
  },
  
  created() {
    this.fetchUserData()
  }
})
</script>

与provide/inject集成:

javascript 复制代码
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

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

app.use(pinia)

// 可以通过provide将store实例提供给整个应用
app.provide('pinia', pinia)

app.mount('#app')

// 在组件中使用
<script setup>
import { inject } from 'vue'

const pinia = inject('pinia')
// 或者直接使用 useStore
const userStore = useUserStore()
</script>

Pinia最佳实践

1. Store设计原则:

  • 一个store应该专注于一个功能领域
  • 保持store的简单和专注,不要创建过大的store
  • 将相关的数据和行为放在同一个store中

2. 命名规范:

javascript 复制代码
// ✅ 好的命名 - 描述性强,使用camelCase
export const useUserStore = defineStore('user', () => {...})
export const useShoppingCartStore = defineStore('shoppingCart', () => {...})

// ❌ 避免的命名 - 过于简单或不清晰
export const useStore1 = defineStore('store1', () => {...})
export const useData = defineStore('data', () => {...})

3. Store组织结构:

javascript 复制代码
src/stores/
  ├── index.js                # 导出所有store
  ├── user.js                 # 用户相关
  ├── products/
  │   ├── index.js            # 导出产品相关store
  │   ├── list.js             # 产品列表
  │   ├── details.js          # 产品详情
  │   └── search.js           # 产品搜索
  └── shopping/
      ├── index.js            # 导出购物相关store
      ├── cart.js             # 购物车
      └── checkout.js         # 结账流程

4. TypeScript最佳实践:

javascript 复制代码
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, UserRole } from '@/types/user'

export const useUserStore = defineStore('user', () => {
  // 定义状态类型
  const user = ref<User | null>(null)
  const isLoading = ref<boolean>(false)
  const error = ref<string | null>(null)
  const permissions = ref<UserRole[]>([])
  
  // 带类型的getter
  const isAdmin = computed<boolean>(() => {
    return permissions.value.includes(UserRole.ADMIN)
  })
  
  // 带类型的action
  async function fetchUser(userId: string): Promise<void> {
    isLoading.value = true
    error.value = null
    
    try {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const userData: User = await response.json()
      user.value = userData
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Unknown error'
    } finally {
      isLoading.value = false
    }
  }
  
  return {
    user,
    isLoading,
    error,
    permissions,
    isAdmin,
    fetchUser
  }
})

Pinia API

分类 API 说明
创建 defineStore 定义一个独立的 store,包含 state / getters / actions
实例化 createPinia 创建 Pinia 根实例,用于 app.use()
组件使用 useStore()(用户自定义,如 useUserStore 调用某个 store,返回响应式 state、getter、action
状态 state 返回一个对象函数,用于定义可变全局状态
状态操作 store.$state 直接读写整个 state(对象级)
状态操作 store.$patch() 批量修改 state,支持对象和函数两种模式
状态替换 store.$reset() 重置为初始 state,仅在 setup 方式定义时可用
状态订阅 store.$subscribe() 监听 state 变化,适合本地持久化
Action 调用 store.$onAction() 监听 action 调用前后,可做日志、埋点
Getter getters 派生数据,基于 state 自动缓存
插件 pinia.use() 注册插件,扩展 store 能力
定义方式 defineStore(id, options) Options API 写法
定义方式 defineStore(id, () => {...}) Setup 写法,返回 state / getter / action
Store 属性 store.$id 当前 store 的唯一标识
Store 属性 store.$ready(部分版本) Store 初始化完成后的状态标记
持久化(插件) persist 启用存储插件(如 localStorage)
工具函数 storeToRefs() 将 state / getter 转为 refs,保留响应式
相关推荐
小白学大数据7 小时前
实时监控 1688 商品价格变化的爬虫系统实现
javascript·爬虫·python
Century_Dragon7 小时前
MR实训探秘新能源动力系统——虚实融合助力职校拆装检测教学
学习
Darkershadow7 小时前
Python学习之使用笔记本摄像头截屏
python·opencv·学习
哆啦A梦15887 小时前
商城后台管理系统 04 登录-功能实现-数据持久化-vuex
javascript·vue.js·elementui
深蓝海拓9 小时前
PySide6从0开始学习的笔记(四)QMainWindow
笔记·python·学习·pyqt
深蓝海拓9 小时前
PySide6 的 QSettings简单应用学习笔记
python·学习·pyqt
毕设源码-朱学姐15 小时前
【开题答辩全过程】以 工厂能耗分析平台的设计与实现为例,包含答辩的问题和答案
java·vue.js
码界奇点16 小时前
Python从0到100一站式学习路线图与实战指南
开发语言·python·学习·青少年编程·贴图
老前端的功夫17 小时前
Vue 3 性能深度解析:从架构革新到运行时的全面优化
javascript·vue.js·架构