Pinia状态管理 #

一、Pinia简介 #

Pinia是Vue官方推荐的状态管理库,相比Vuex更加简洁、类型友好。

1.1 安装 #

bash
npm install pinia

1.2 基本配置 #

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')

1.3 Pinia vs Vuex #

特性 Pinia Vuex
模块化 每个Store独立 需要modules
TypeScript 完美支持 需要额外配置
Mutations 必须通过mutations修改
代码量 较少 较多
Composition API 原生支持 需要使用API

二、定义Store #

2.1 Setup Store(推荐) #

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

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  
  // getters
  const doubled = computed(() => count.value * 2)
  
  // actions
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function $reset() {
    count.value = 0
  }
  
  return {
    count,
    doubled,
    increment,
    decrement,
    $reset
  }
})

2.2 Options Store #

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

export const useCounterStore = defineStore('counter', {
  // state
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  
  // getters
  getters: {
    doubled: (state) => state.count * 2,
    
    // 使用this访问其他getter
    doubledPlusOne() {
      return this.doubled + 1
    }
  },
  
  // actions
  actions: {
    increment() {
      this.count++
    },
    
    decrement() {
      this.count--
    },
    
    async fetchCount() {
      const response = await fetch('/api/count')
      this.count = await response.json()
    }
  }
})

三、使用Store #

3.1 基本使用 #

vue
<template>
  <div>
    <p>计数: {{ counter.count }}</p>
    <p>双倍: {{ counter.doubled }}</p>
    <button @click="counter.increment">+1</button>
    <button @click="counter.decrement">-1</button>
  </div>
</template>

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

const counter = useCounterStore()

// 访问state
console.log(counter.count)

// 访问getter
console.log(counter.doubled)

// 调用action
counter.increment()
</script>

3.2 解构使用 #

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

const counter = useCounterStore()

// ❌ 直接解构会丢失响应性
// const { count, doubled } = counter

// ✅ 使用storeToRefs解构state和getter
const { count, doubled } = storeToRefs(counter)

// ✅ actions可以直接解构
const { increment, decrement } = counter
</script>

3.3 修改State #

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

const counter = useCounterStore()

// 直接修改
counter.count++

// $patch批量修改
counter.$patch({
  count: 10,
  name: '新名称'
})

// $patch函数形式
counter.$patch((state) => {
  state.count++
  state.name = '更新后的名称'
})

// $reset重置状态
counter.$reset()

// 替换整个state
counter.$state = {
  count: 100,
  name: '全新状态'
}
</script>

四、Getters #

4.1 定义Getters #

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

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [
      { id: 1, name: '张三', age: 25 },
      { id: 2, name: '李四', age: 30 },
      { id: 3, name: '王五', age: 28 }
    ]
  }),
  
  getters: {
    // 基本getter
    userCount: (state) => state.users.length,
    
    // 返回函数的getter
    getUserById: (state) => {
      return (id) => state.users.find(user => user.id === id)
    },
    
    // 访问其他getter
    adultUsers(state) {
      return state.users.filter(user => user.age >= 18)
    },
    
    adultCount() {
      return this.adultUsers.length
    }
  }
})

4.2 使用Getters #

vue
<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 访问getter
console.log(userStore.userCount)
console.log(userStore.adultUsers)

// 使用返回函数的getter
const user = userStore.getUserById(1)
</script>

五、Actions #

5.1 定义Actions #

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

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    loading: false,
    error: null
  }),
  
  actions: {
    async fetchProducts() {
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch('/api/products')
        this.products = await response.json()
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },
    
    async addProduct(product) {
      const response = await fetch('/api/products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(product)
      })
      const newProduct = await response.json()
      this.products.push(newProduct)
    },
    
    async updateProduct(id, updates) {
      const response = await fetch(`/api/products/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updates)
      })
      const updatedProduct = await response.json()
      const index = this.products.findIndex(p => p.id === id)
      if (index > -1) {
        this.products[index] = updatedProduct
      }
    },
    
    async deleteProduct(id) {
      await fetch(`/api/products/${id}`, { method: 'DELETE' })
      this.products = this.products.filter(p => p.id !== id)
    }
  }
})

5.2 使用Actions #

vue
<script setup>
import { onMounted } from 'vue'
import { useProductStore } from '@/stores/product'

const productStore = useProductStore()

onMounted(() => {
  productStore.fetchProducts()
})

async function handleAdd() {
  await productStore.addProduct({
    name: '新产品',
    price: 99.9
  })
}
</script>

六、组合式Store #

6.1 在Store中使用其他Store #

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

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  
  getters: {
    // 使用其他store的数据
    cartWithUser(state) {
      const userStore = useUserStore()
      return {
        items: state.items,
        user: userStore.currentUser
      }
    }
  },
  
  actions: {
    async checkout() {
      const userStore = useUserStore()
      
      if (!userStore.isLoggedIn) {
        throw new Error('请先登录')
      }
      
      // 结账逻辑
    }
  }
})

6.2 组合式函数风格 #

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

export const useAuthStore = defineStore('auth', () => {
  const token = ref(localStorage.getItem('token'))
  const user = ref(null)
  
  const isLoggedIn = computed(() => !!token.value)
  
  async function login(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials)
    })
    const data = await response.json()
    token.value = data.token
    user.value = data.user
    localStorage.setItem('token', data.token)
  }
  
  function logout() {
    token.value = null
    user.value = null
    localStorage.removeItem('token')
  }
  
  return {
    token,
    user,
    isLoggedIn,
    login,
    logout
  }
})

七、插件 #

7.1 创建插件 #

javascript
// plugins/piniaPersist.js
import { watch } from 'vue'

export function piniaPersist({ store }) {
  // 从localStorage恢复状态
  const savedState = localStorage.getItem(`pinia-${store.$id}`)
  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }
  
  // 监听变化并保存
  watch(
    () => store.$state,
    (state) => {
      localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
    },
    { deep: true }
  )
}

7.2 使用插件 #

javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { piniaPersist } from './plugins/piniaPersist'
import App from './App.vue'

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

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

八、完整示例 #

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

export const useTodoStore = defineStore('todo', () => {
  const todos = ref([])
  const filter = ref('all')
  const loading = ref(false)
  
  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'active':
        return todos.value.filter(t => !t.completed)
      case 'completed':
        return todos.value.filter(t => t.completed)
      default:
        return todos.value
    }
  })
  
  const remaining = computed(() => 
    todos.value.filter(t => !t.completed).length
  )
  
  async function fetchTodos() {
    loading.value = true
    try {
      const response = await fetch('/api/todos')
      todos.value = await response.json()
    } finally {
      loading.value = false
    }
  }
  
  async function addTodo(text) {
    const response = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text, completed: false })
    })
    const todo = await response.json()
    todos.value.push(todo)
  }
  
  async function toggleTodo(id) {
    const todo = todos.value.find(t => t.id === id)
    if (todo) {
      await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed: !todo.completed })
      })
      todo.completed = !todo.completed
    }
  }
  
  async function removeTodo(id) {
    await fetch(`/api/todos/${id}`, { method: 'DELETE' })
    todos.value = todos.value.filter(t => t.id !== id)
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  return {
    todos,
    filter,
    loading,
    filteredTodos,
    remaining,
    fetchTodos,
    addTodo,
    toggleTodo,
    removeTodo,
    setFilter
  }
})

九、总结 #

Pinia核心概念 #

概念 说明
Store 状态容器
State 状态数据
Getters 计算属性
Actions 方法

Pinia要点:

  • 推荐使用Setup Store风格
  • 使用storeToRefs解构保持响应性
  • 直接修改state或使用$patch
  • Actions支持异步操作
  • 可以组合使用多个Store
  • 支持插件扩展功能
最后更新:2026-03-26