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