Vue3 Pinia 入门教程

在 Vue3 中,状态管理是一个绕不开的话题。

Pinia 是 Vue.js 的轻量级状态管理库,它让你能够在组件之间共享和管理状态,我们可以把 Pinia 想象成一个全局的数据仓库,所有组件都可以从这里获取数据或者更新数据。

本章节我们要介绍的是 Vue3 官方推荐的状态管理库 —— Pinia,相比 Vuex,Pinia 提供了更简洁、更符合 Vue3 组合式 API 思维的状态管理方案。

下图对比了两者的结构差异:

  • Vuex 采用单仓库加多级模块的树状结构,层级固定,依赖 mutations,整体更重。
  • Pinia 则由多个独立 store 组成,扁平、轻量、不分模块,无命名空间负担。

Pinia 核心特点:

  • 支持 Vue2 和 Vue3
  • 极简的 API 设计
  • 完整的 TypeScript 支持
  • 支持组合式 API
  • 模块化设计,无需嵌套模块

Pinia 的扁平结构让组件可以直接连到任意 store,各取所需,不经过树状模块,也不走命名空间路径,状态流动简单直接,耦合度低。


安装与配置

用 Vite 创建项目:

npm create vite@latest vue-pinia-demo --template vue
cd vue-pinia-demo
npm install

安装 Pinia

首先,在你的 Vue3 项目中安装 Pinia:

npm install pinia
# 或者
yarn add pinia

Pinia 有三部分:

  • state:存数据
  • getters:算派生数据
  • actions:执行逻辑、修改 state

配置 Pinia

在 src/main.js 或 src/main.ts 中配置 Pinia:

实例

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

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

// 使用 Pinia
app.use(pinia)
app.mount('#app')

创建第一个 Store

Store 就是 Pinia 中的数据仓库,我们来创建一个简单的计数器 Store。

定义 Store

新建 src/stores/useCounter.js:

实例

// stores/useCounter.js
import { defineStore } from 'pinia'

// 使用 defineStore 定义 store
// 第一个参数是 store 的唯一 ID
// 第二个参数是 store 的配置选项
export const useCounterStore = defineStore('counter', {
  // state: 定义 store 的状态数据
  state: () => ({
    count: 0,
    name: '我的计数器'
  }),

  // getters: 定义基于 state 的计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用 this 访问其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },

  // actions: 定义修改 state 的方法
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    // 可以接收参数
    incrementBy(amount) {
      this.count += amount
    },
    // 异步 action
    async incrementAsync() {
      // 模拟异步操作
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

Store 的结构说明

让我们通过一个表格来理解 Store 的各个部分:

部分 作用 示例
state 定义存储的数据 count: 0
getters 基于 state 的计算属性 doubleCount: state => state.count * 2
actions 修改 state 的方法 increment() { this.count++ }

在组件中使用 Store

基本使用

src/components/CounterComponent.vue 组件代码如下:

实例

<!-- CounterComponent.vue -->
<template>
  <div class="counter">
    <h3>{{ store.name }}</h3>
    <p>当前计数: {{ store.count }}</p>
    <p>双倍计数: {{ store.doubleCount }}</p>
    <p>双倍加一: {{ store.doubleCountPlusOne }}</p>
   
    <button @click="store.increment()">+1</button>
    <button @click="store.decrement()">-1</button>
    <button @click="store.incrementBy(5)">+5</button>
    <button @click="store.incrementAsync()">异步 +1</button>
   
    <button @click="reset">重置</button>
  </div>
</template>

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

// 在 setup 中使用 store
const store = useCounterStore()

// 重置状态的方法
function reset() {
  store.$reset() // $reset 方法可以重置 state 到初始值
}
</script>

修改 src/App.vue 代码如下:

实例

<script setup>
import CounterComponent from './components/CounterComponent.vue'
</script>

<template>
  <CounterComponent />
</template>

整个项目结构:

执行 npm run dev 命令,在浏览器访问 http://localhost:5173/,查看效果:

响应式解构

如果你想在模板中直接使用 state 的属性,可以使用 storeToRefs

实例

<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>名称: {{ name }}</p>
  </div>
</template>

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

const store = useCounterStore()

// 使用 storeToRefs 保持响应式
const { count, name } = storeToRefs(store)

// 注意:直接解构会失去响应式!
// 错误写法:const { count, name } = store
</script>

组合式 API 风格的 Store

Pinia 也支持使用组合式 API 的风格来定义 Store:

实例

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

export const useUserStore = defineStore('user', () => {
  // state
  const user = ref(null)
  const isLoggedIn = ref(false)

  // getters
  const userName = computed(() => user.value?.name || '游客')
  const userAge = computed(() => user.value?.age || 0)

  // actions
  function login(userData) {
    user.value = userData
    isLoggedIn.value = true
  }

  function logout() {
    user.value = null
    isLoggedIn.value = false
  }

  return {
    user,
    isLoggedIn,
    userName,
    userAge,
    login,
    logout
  }
})

Store 之间的交互

多个 Store 之间可以相互调用:

实例

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

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),

  actions: {
    addItem(product) {
      this.items.push(product)
     
      // 调用其他 store 的 action
      const userStore = useUserStore()
      if (userStore.isLoggedIn) {
        // 同步到用户购物车
        this.syncToUserCart()
      }
    }
  }
})

实际应用示例:购物车

让我们创建一个完整的购物车示例:

实例

// stores/products.js
export const useProductStore = defineStore('products', {
  state: () => ({
    products: [
      { id: 1, name: '笔记本电脑', price: 5999, stock: 10 },
      { id: 2, name: '智能手机', price: 3999, stock: 20 },
      { id: 3, name: '无线耳机', price: 299, stock: 50 }
    ]
  }),

  getters: {
    // 根据 ID 获取商品
    getProductById: (state) => (id) => {
      return state.products.find(product => product.id === id)
    },
    // 获取有库存的商品
    availableProducts: (state) => {
      return state.products.filter(product => product.stock > 0)
    }
  }
})

实例

// stores/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [], // { productId, quantity }
    discount: 0
  }),

  getters: {
    // 购物车商品总数
    totalItems: (state) => {
      return state.items.reduce((total, item) => total + item.quantity, 0)
    },
    // 购物车总金额
    totalPrice: (state) => {
      const productStore = useProductStore()
      return state.items.reduce((total, item) => {
        const product = productStore.getProductById(item.productId)
        return total + (product?.price || 0) * item.quantity
      }, 0)
    },
    // 折后总价
    finalPrice: (state) => {
      return state.totalPrice * (1 - state.discount / 100)
    }
  },

  actions: {
    // 添加商品到购物车
    addToCart(productId, quantity = 1) {
      const existingItem = this.items.find(item => item.productId === productId)
     
      if (existingItem) {
        existingItem.quantity += quantity
      } else {
        this.items.push({ productId, quantity })
      }
    },

    // 从购物车移除商品
    removeFromCart(productId) {
      this.items = this.items.filter(item => item.productId !== productId)
    },

    // 清空购物车
    clearCart() {
      this.items = []
      this.discount = 0
    },

    // 设置折扣
    setDiscount(percent) {
      this.discount = Math.max(0, Math.min(100, percent))
    }
  }
})

在组件中使用购物车

实例

<!-- ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <h3>购物车 ({{ cart.totalItems }} 件商品)</h3>
   
    <div v-if="cart.items.length === 0">
      <p>购物车为空</p>
    </div>
   
    <div v-else>
      <div v-for="item in cartItems" :key="item.product.id" class="cart-item">
        <span>{{ item.product.name }}</span>
        <span>¥{{ item.product.price }}</span>
        <span>数量: {{ item.quantity }}</span>
        <span>小计: ¥{{ item.product.price * item.quantity }}</span>
        <button @click="cart.removeFromCart(item.product.id)">删除</button>
      </div>
     
      <div class="cart-summary">
        <p>总价: ¥{{ cart.totalPrice }}</p>
        <p v-if="cart.discount > 0">折扣: {{ cart.discount }}%</p>
        <p>实付: ¥{{ cart.finalPrice }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/products'

const cart = useCartStore()
const products = useProductStore()

// 计算购物车商品详情
const cartItems = computed(() => {
  return cart.items.map(item => ({
    product: products.getProductById(item.productId),
    quantity: item.quantity
  })).filter(item => item.product) // 过滤掉不存在的商品
})
</script>

最佳实践和注意事项

1. Store 命名规范

实例

// 好的命名
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('products', { /* ... */ })

// 避免的命名
export const userStore = defineStore('user', { /* ... */ }) // 缺少 use 前缀

2. 状态初始化

实例

// 推荐:使用函数返回初始状态
state: () => ({
  items: [],
  loading: false,
  error: null
})

// 不推荐:直接使用对象
state: {
  items: []  // 这会导致所有实例共享同一个数组!
}

3. 异步操作处理

实例

actions: {
  async fetchUserData(userId) {
    this.loading = true
    this.error = null
   
    try {
      const response = await api.getUser(userId)
      this.user = response.data
    } catch (error) {
      this.error = error.message
    } finally {
      this.loading = false
    }
  }
}

4. 数据持久化

对于需要持久化的数据,可以使用插件:

npm install pinia-plugin-persistedstate

实例

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: {}
  }),
 
  // 启用持久化
  persist: true
})

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,保留响应式

更多内容参考 Pinia 官网:https://pinia.vuejs.org/