Skip to content

Pinia 状态管理

本文介绍Pinia的核心概念、使用方法和最佳实践。Pinia是Vue官方推荐的新一代状态管理库,替代了Vuex,提供了更简洁的API、更好的TypeScript支持和更灵活的使用方式。

什么是Pinia?

Pinia是Vue.js的轻量级状态管理库,由Vue核心团队成员开发,是Vuex的继任者。它最初是为了探索Vuex的下一个版本而创建的,最终被官方推荐为Vue 3项目的状态管理解决方案。

Pinia的优势

  • 简洁的API:没有Mutation,没有模块嵌套,使用更直观
  • 完整的TypeScript支持:类型推断更加完善,开发体验更好
  • 轻量级:体积约1KB,性能优秀
  • 灵活的API:支持组合式API和选项式API
  • 开发工具支持:与Vue Devtools集成良好
  • 服务器端渲染支持:兼容SSR场景

与Vuex的对比

特性PiniaVuex 3Vuex 4
支持Vue 2
支持Vue 3
TypeScript支持⚠️ 有限支持
Mutation
Action
Getter
模块
模块嵌套
命名空间✅ (默认)❌ (需手动开启)❌ (需手动开启)

安装与配置

安装Pinia

bash
# 使用npm
npm install pinia --save

# 使用yarn
yarn add pinia

# 使用pnpm
pnpm add pinia

基本配置

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)
app.mount('#app')

核心概念

Pinia的核心概念比Vuex更简洁,主要包括:

  • Store:存储状态和业务逻辑的容器
  • State:存储应用状态的响应式数据
  • Getter:从state中派生出的计算属性
  • Action:修改state的方法,可以包含同步和异步操作

创建Store

Store是Pinia的核心,使用defineStore函数创建,需要一个唯一的ID和一个配置对象。

基本用法

javascript
// stores/counter.js
import { defineStore } from 'pinia'

// 定义并导出store
export const useCounterStore = defineStore('counter', {
  // 状态
  state: () => ({
    count: 0,
    name: 'Pinia'
  }),
  // 计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用this访问其他getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  // 方法
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    // 异步action
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    },
    // 带参数的action
    incrementBy(amount) {
      this.count += amount
    }
  }
})

使用组合式API风格

Pinia也支持使用组合式API风格定义store:

javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // 状态
  const name = ref('John')
  const age = ref(30)

  // getters
  const isAdult = computed(() => age.value >= 18)
  const userInfo = computed(() => `${name.value}, ${age.value} years old`)

  // actions
  function setName(newName) {
    name.value = newName
  }

  function incrementAge() {
    age.value++
  }

  async function fetchUser() {
    const response = await fetch('/api/user')
    const data = await response.json()
    name.value = data.name
    age.value = data.age
  }

  // 返回需要暴露的状态、getter和action
  return {
    name,
    age,
    isAdult,
    userInfo,
    setName,
    incrementAge,
    fetchUser
  }
})

使用Store

在组件中使用store非常简单,只需导入并调用定义的store函数。

基本用法

javascript
// Counter.vue
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    // 获取store实例
    const counterStore = useCounterStore()

    return {
      // 暴露状态、getter和action给模板
      counterStore
    }
  }
}
html
<template>
  <div>
    <h1>{{ counterStore.name }}</h1>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">Increment</button>
    <button @click="counterStore.decrement">Decrement</button>
    <button @click="counterStore.incrementAsync">Increment Async</button>
    <button @click="counterStore.incrementBy(5)">Increment by 5</button>
  </div>
</template>

在选项式API中使用

Pinia提供了mapStoresmapStatemapActions等辅助函数,方便在选项式API中使用:

javascript
import { mapStores, mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    // 映射整个store
    ...mapStores(useCounterStore),
    // 映射state
    ...mapState(useCounterStore, ['count', 'name', 'doubleCount']),
    // 重命名映射
    ...mapState(useCounterStore, {
      myCount: 'count',
      myDoubleCount: 'doubleCount'
    })
  },
  methods: {
    // 映射actions
    ...mapActions(useCounterStore, ['increment', 'decrement', 'incrementAsync']),
    // 重命名映射
    ...mapActions(useCounterStore, {
      add: 'increment'
    })
  }
}

State操作

访问State

直接通过store实例访问state:

javascript
const counterStore = useCounterStore()
console.log(counterStore.count) // 0
console.log(counterStore.name) // 'Pinia'

修改State

可以直接修改state,也可以使用$patch方法批量修改:

javascript
// 直接修改
counterStore.count++
counterStore.name = 'New Name'

// 使用$patch修改多个属性
counterStore.$patch({
  count: counterStore.count + 1,
  name: 'Updated Name'
})

// 使用函数式$patch(适用于复杂修改)
counterStore.$patch((state) => {
  state.count++
  state.name = 'Updated Name'
})

重置State

使用$reset方法将state重置为初始值:

javascript
counterStore.$reset()

替换State

使用$state属性替换整个state对象:

javascript
counterStore.$state = {
  count: 100,
  name: 'Replaced'
}

Getter

Getter是从state中派生出的计算属性,类似于组件中的computed。

定义Getter

javascript
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    // 基础用法
    doubleCount: (state) => state.count * 2,
    // 使用this访问其他getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
    // 带参数的getter
    getCountWithFactor: (state) => (factor) => {
      return state.count * factor
    },
    // 使用其他store
    userCount() {
      const userStore = useUserStore()
      return userStore.users.length
    }
  }
})

使用Getter

javascript
const counterStore = useCounterStore()
console.log(counterStore.doubleCount) // 0
console.log(counterStore.doubleCountPlusOne) // 1
console.log(counterStore.getCountWithFactor(3)) // 0

Action

Action是修改state的方法,可以包含同步和异步操作。与Vuex不同,Pinia没有Mutation,所有状态修改都在Action中完成。

定义Action

javascript
export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    loading: false,
    error: null
  }),
  actions: {
    // 同步action
    addUser(user) {
      this.users.push(user)
    },
    // 异步action
    async fetchUsers() {
      this.loading = true
      this.error = null
      try {
        const response = await fetch('/api/users')
        if (!response.ok) throw new Error('Failed to fetch users')
        this.users = await response.json()
      } catch (err) {
        this.error = err.message
      } finally {
        this.loading = false
      }
    },
    // 使用其他store
    async fetchUserAndPosts(userId) {
      const userStore = useUserStore()
      const postStore = usePostStore()

      await Promise.all([
        userStore.fetchUser(userId),
        postStore.fetchPostsByUser(userId)
      ])
    }
  }
})

调用Action

javascript
const userStore = useUserStore()
userStore.addUser({ id: 1, name: 'John' })
userStore.fetchUsers().then(() => {
  console.log('Users fetched')
})

模块(Modules)

Pinia没有Vuex中的模块嵌套概念,而是通过创建多个store来实现模块化。每个store都是独立的,可以相互引用。

创建多个Store

stores/
├── counter.js
├── user.js
├── post.js
└── cart.js

在Store中引用其他Store

javascript
// stores/post.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const usePostStore = defineStore('post', {
  state: () => ({
    posts: []
  }),
  actions: {
    async fetchPostsByUser(userId) {
      const userStore = useUserStore()
      if (!userStore.currentUser) {
        await userStore.fetchUser(userId)
      }
      // ... fetch posts
    }
  }
})

开发工具集成

Pinia与Vue Devtools集成良好,支持:

  • 时间旅行调试:可以回溯和重放状态变化
  • 状态快照:查看任意时刻的状态
  • Action调用日志:记录所有Action调用
  • Store结构可视化:清晰展示所有Store

持久化存储

Pinia本身不提供持久化存储功能,但可以与第三方库结合使用,如pinia-plugin-persistedstate

安装插件

bash
npm install pinia-plugin-persistedstate --save

使用插件

javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

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

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

配置持久化

javascript
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Pinia'
  }),
  // 配置持久化
  persist: true,
  // 或自定义配置
  persist: {
    key: 'counter-store',
    storage: localStorage, // 默认是localStorage
    paths: ['count'] // 只持久化count字段
  }
  // ...
})

最佳实践

Store设计

  • 按功能划分Store:每个Store负责一个功能模块
  • 保持Store精简:只存储全局共享状态,局部状态应放在组件内
  • 单一职责原则:一个Store只处理一个领域的逻辑
  • 避免深层嵌套:状态结构应扁平化,便于访问和修改

项目结构

stores/
├── index.js          # 导出所有store
├── counter.js        # 计数器相关状态
├── user.js           # 用户相关状态
├── post.js           # 文章相关状态
└── cart.js           # 购物车相关状态

命名规范

  • Store文件名:使用小写字母,多个单词用连字符连接,如user.jsshopping-cart.js
  • Store函数名:使用useXxxStore格式,如useUserStoreuseCartStore
  • Store ID:与Store函数名对应,使用小写字母,多个单词用连字符连接,如usershopping-cart

性能优化

  • 避免不必要的全局状态:局部状态应放在组件内
  • 使用解构时注意响应性:直接解构store会失去响应性,应使用storeToRefs
javascript
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()
// 保持响应性
const { count, name } = storeToRefs(counterStore)
// actions不需要解构,直接使用store调用
const { increment, decrement } = counterStore
  • 批量更新状态:使用$patch方法减少响应式更新次数

测试

Pinia的测试非常简单,可以像测试普通函数一样测试Store:

javascript
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    // 创建一个新的pinia实例
    setActivePinia(createPinia())
  })

  it('should increment count', () => {
    const counterStore = useCounterStore()
    expect(counterStore.count).toBe(0)
    counterStore.increment()
    expect(counterStore.count).toBe(1)
  })

  it('should double count', () => {
    const counterStore = useCounterStore()
    counterStore.count = 2
    expect(counterStore.doubleCount).toBe(4)
  })
})

从Vuex迁移到Pinia

从Vuex迁移到Pinia非常简单,主要步骤包括:

  1. 安装Pinia并在应用中配置
  2. 将Vuex的Store逐个转换为Pinia的Store
    • 将state转换为函数返回对象
    • 将getters转换为对象或函数
    • 将mutations和actions合并为actions
    • 将modules拆分为多个独立的Store
  3. 更新组件中使用Store的方式
  4. 移除Vuex相关依赖

迁移示例

Vuex Store:

javascript
// store/modules/counter.js
const state = {
  count: 0
}

const getters = {
  doubleCount: state => state.count * 2
}

const mutations = {
  increment(state) {
    state.count++
  }
}

const actions = {
  incrementAsync({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

Pinia Store:

javascript
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: state => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    incrementAsync() {
      setTimeout(() => {
        this.increment()
      }, 1000)
    }
  }
})

总结

Pinia是Vue官方推荐的状态管理库,提供了简洁的API、优秀的TypeScript支持和灵活的使用方式。通过Store、State、Getter和Action等核心概念,Pinia帮助我们更好地管理应用状态。相比Vuex,Pinia去除了Mutation和模块嵌套,使状态管理更加直观和简单。对于新项目,推荐使用Pinia;对于现有Vuex项目,可以逐步迁移到Pinia。合理使用Pinia可以使应用状态变化更加可预测、可调试,同时提高代码的可维护性和复用性。