Skip to main content

Vue状态管理方案

随着Vue应用规模的增长,组件间的状态共享和管理变得尤为重要。本文将介绍Vue生态系统中的各种状态管理方案,包括Pinia、Vuex以及使用Composition API的状态管理策略。

核心价值

Vue状态管理的价值

  • 📊 集中式状态:提供应用的单一状态源
  • 🔄 可预测的变更:通过明确的规则修改状态
  • 🧩 模块化设计:将复杂状态分解为可管理的模块
  • 🔍 开发工具:提供时间旅行、状态快照等调试功能
  • 性能优化:优化组件更新和渲染

1. Pinia - 新一代状态管理

Pinia是Vue官方推荐的新一代状态管理库,相比Vuex更轻量、更TypeScript友好,并且提供了更好的开发体验。

1.1 Pinia基础设置

首先安装Pinia:

bash
1npm install pinia

在Vue应用中注册Pinia:

src/main.js
js
1import { createApp } from 'vue'
2import { createPinia } from 'pinia'
3import App from './App.vue'
4
5const app = createApp(App)
6const pinia = createPinia()
7
8app.use(pinia)
9app.mount('#app')

1.2 创建Pinia Store

使用defineStore创建一个store:

src/stores/counter.js
js
1import { defineStore } from 'pinia'
2
3// 基于Option API风格的store
4export const useCounterStore = defineStore('counter', {
5 // state
6 state: () => ({
7 count: 0,
8 name: 'Counter',
9 users: []
10 }),
11
12 // getters
13 getters: {
14 doubleCount: (state) => state.count * 2,
15 // 访问其他getter
16 doubleCountPlusOne() {
17 return this.doubleCount + 1
18 }
19 },
20
21 // actions
22 actions: {
23 increment() {
24 this.count++
25 },
26 async fetchUsers() {
27 try {
28 const response = await fetch('https://jsonplaceholder.typicode.com/users')
29 const data = await response.json()
30 this.users = data
31 } catch (error) {
32 console.error('Failed to fetch users:', error)
33 }
34 }
35 }
36})

使用Composition API风格创建store:

src/stores/user.js
js
1import { defineStore } from 'pinia'
2import { ref, computed } from 'vue'
3
4// 基于组合式API风格的store
5export const useUserStore = defineStore('user', () => {
6 // state
7 const user = ref(null)
8 const isLoggedIn = ref(false)
9 const token = ref(localStorage.getItem('token') || null)
10 const error = ref(null)
11
12 // getters
13 const userFullName = computed(() => {
14 if (!user.value) return ''
15 return `${user.value.firstName} ${user.value.lastName}`
16 })
17
18 // actions
19 async function login(credentials) {
20 try {
21 error.value = null
22 // 模拟API调用
23 const response = await fetch('/api/login', {
24 method: 'POST',
25 headers: {
26 'Content-Type': 'application/json'
27 },
28 body: JSON.stringify(credentials)
29 })
30
31 if (!response.ok) {
32 throw new Error('Login failed')
33 }
34
35 const data = await response.json()
36 user.value = data.user
37 token.value = data.token
38 isLoggedIn.value = true
39
40 // 保存token到本地存储
41 localStorage.setItem('token', data.token)
42
43 return true
44 } catch (e) {
45 error.value = e.message
46 return false
47 }
48 }
49
50 function logout() {
51 user.value = null
52 token.value = null
53 isLoggedIn.value = false
54 localStorage.removeItem('token')
55 }
56
57 async function fetchUserProfile() {
58 if (!token.value) return null
59
60 try {
61 const response = await fetch('/api/profile', {
62 headers: {
63 'Authorization': `Bearer ${token.value}`
64 }
65 })
66
67 if (!response.ok) {
68 throw new Error('Failed to fetch profile')
69 }
70
71 user.value = await response.json()
72 isLoggedIn.value = true
73
74 return user.value
75 } catch (e) {
76 error.value = e.message
77 return null
78 }
79 }
80
81 return {
82 // state
83 user,
84 isLoggedIn,
85 token,
86 error,
87 // getters
88 userFullName,
89 // actions
90 login,
91 logout,
92 fetchUserProfile
93 }
94})

1.3 在组件中使用Pinia

使用Pinia的组件示例
vue
1<template>
2 <div class="user-profile">
3 <div v-if="userStore.isLoggedIn">
4 <h1>欢迎, {{ userStore.userFullName }}</h1>
5 <button @click="userStore.logout">退出登录</button>
6 </div>
7 <div v-else>
8 <h2>用户登录</h2>
9 <form @submit.prevent="handleLogin">
10 <input v-model="email" type="email" placeholder="邮箱" required />
11 <input v-model="password" type="password" placeholder="密码" required />
12 <p v-if="userStore.error" class="error">{{ userStore.error }}</p>
13 <button type="submit" :disabled="isLoading">
14 {{ isLoading ? '登录中...' : '登录' }}
15 </button>
16 </form>
17 </div>
18
19 <div class="counter-section">
20 <h3>计数器: {{ counterStore.count }}</h3>
21 <p>双倍值: {{ counterStore.doubleCount }}</p>
22 <button @click="counterStore.increment">增加</button>
23 </div>
24 </div>
25</template>
26
27<script setup>
28import { ref } from 'vue'
29import { useUserStore } from '../stores/user'
30import { useCounterStore } from '../stores/counter'
31
32const userStore = useUserStore()
33const counterStore = useCounterStore()
34
35const email = ref('')
36const password = ref('')
37const isLoading = ref(false)
38
39const handleLogin = async () => {
40 isLoading.value = true
41 try {
42 const success = await userStore.login({
43 email: email.value,
44 password: password.value
45 })
46
47 if (success) {
48 email.value = ''
49 password.value = ''
50 }
51 } finally {
52 isLoading.value = false
53 }
54}
55</script>

1.4 Pinia高级特性

Store间通信

Store之间可以相互引用和交互:

stores/cart.js
js
1import { defineStore } from 'pinia'
2import { useProductStore } from './product'
3import { useUserStore } from './user'
4
5export const useCartStore = defineStore('cart', {
6 state: () => ({
7 items: [],
8 }),
9
10 getters: {
11 totalItems: (state) => state.items.length,
12 totalPrice: (state) => {
13 const productStore = useProductStore()
14 return state.items.reduce((total, item) => {
15 const product = productStore.getProductById(item.productId)
16 return total + (product?.price || 0) * item.quantity
17 }, 0)
18 },
19 cartWithProducts: (state) => {
20 const productStore = useProductStore()
21 return state.items.map(item => {
22 const product = productStore.getProductById(item.productId)
23 return {
24 ...item,
25 product,
26 subtotal: (product?.price || 0) * item.quantity
27 }
28 })
29 }
30 },
31
32 actions: {
33 addToCart(productId, quantity = 1) {
34 const existingItem = this.items.find(item => item.productId === productId)
35
36 if (existingItem) {
37 existingItem.quantity += quantity
38 } else {
39 this.items.push({
40 productId,
41 quantity
42 })
43 }
44
45 this.saveCart()
46 },
47
48 removeItem(productId) {
49 const index = this.items.findIndex(item => item.productId === productId)
50 if (index > -1) {
51 this.items.splice(index, 1)
52 this.saveCart()
53 }
54 },
55
56 updateQuantity(productId, quantity) {
57 const item = this.items.find(item => item.productId === productId)
58 if (item) {
59 item.quantity = quantity
60 this.saveCart()
61 }
62 },
63
64 async checkout() {
65 const userStore = useUserStore()
66
67 if (!userStore.isLoggedIn) {
68 throw new Error('请先登录')
69 }
70
71 try {
72 // 创建订单
73 const orderData = {
74 userId: userStore.user.id,
75 items: this.items,
76 totalAmount: this.totalPrice
77 }
78
79 // 模拟API调用
80 const response = await fetch('/api/orders', {
81 method: 'POST',
82 headers: {
83 'Content-Type': 'application/json',
84 'Authorization': `Bearer ${userStore.token}`
85 },
86 body: JSON.stringify(orderData)
87 })
88
89 if (!response.ok) {
90 throw new Error('结算失败')
91 }
92
93 // 清空购物车
94 this.clearCart()
95
96 return await response.json()
97 } catch (error) {
98 console.error('结算错误:', error)
99 throw error
100 }
101 },
102
103 clearCart() {
104 this.items = []
105 this.saveCart()
106 },
107
108 loadCart() {
109 const savedCart = localStorage.getItem('cart')
110 if (savedCart) {
111 this.items = JSON.parse(savedCart)
112 }
113 },
114
115 saveCart() {
116 localStorage.setItem('cart', JSON.stringify(this.items))
117 }
118 }
119})

持久化状态

使用Pinia插件实现状态持久化:

src/plugins/piniaPersist.js
js
1import { toRaw } from 'vue'
2
3export function piniaPersistPlugin({ options, store }) {
4 // 从本地存储恢复状态
5 if (options.persist) {
6 const storageKey = `pinia-${store.$id}`
7 const savedState = localStorage.getItem(storageKey)
8
9 if (savedState) {
10 store.$patch(JSON.parse(savedState))
11 }
12
13 // 监听状态变化并保存
14 store.$subscribe((mutation, state) => {
15 localStorage.setItem(storageKey, JSON.stringify(toRaw(state)))
16 })
17 }
18}

在应用中使用插件:

src/main.js
js
1import { createApp } from 'vue'
2import { createPinia } from 'pinia'
3import App from './App.vue'
4import { piniaPersistPlugin } from './plugins/piniaPersist'
5
6const app = createApp(App)
7const pinia = createPinia()
8
9// 注册Pinia插件
10pinia.use(piniaPersistPlugin)
11
12app.use(pinia)
13app.mount('#app')

配置Store启用持久化:

src/stores/theme.js
js
1import { defineStore } from 'pinia'
2
3export const useThemeStore = defineStore('theme', {
4 state: () => ({
5 darkMode: false,
6 accentColor: '#3b82f6',
7 fontSize: 'medium'
8 }),
9
10 actions: {
11 toggleDarkMode() {
12 this.darkMode = !this.darkMode
13 document.documentElement.classList.toggle('dark', this.darkMode)
14 },
15 setAccentColor(color) {
16 this.accentColor = color
17 document.documentElement.style.setProperty('--accent-color', color)
18 }
19 },
20
21 // 启用持久化
22 persist: true
23})

热重载与开发工具

Pinia支持热模块替换(HMR)和Vue开发工具:

src/stores/counter.js
js
1import { defineStore } from 'pinia'
2
3export const useCounterStore = defineStore('counter', {
4 state: () => ({
5 count: 0
6 }),
7 actions: {
8 increment() {
9 this.count++
10 }
11 }
12})
13
14// 启用热重载
15if (import.meta.hot) {
16 import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
17}

2. Vuex - 经典状态管理

虽然Pinia是新一代状态管理库,但许多项目仍在使用Vuex。了解Vuex对维护现有项目非常重要。

2.1 Vuex基础设置

安装Vuex:

bash
1# 对于Vue 3项目
2npm install vuex@next

创建Vuex store:

src/store/index.js
js
1import { createStore } from 'vuex'
2
3export default createStore({
4 state() {
5 return {
6 count: 0,
7 todos: [],
8 user: null
9 }
10 },
11
12 mutations: {
13 INCREMENT(state) {
14 state.count++
15 },
16 SET_TODOS(state, todos) {
17 state.todos = todos
18 },
19 ADD_TODO(state, todo) {
20 state.todos.push(todo)
21 },
22 SET_USER(state, user) {
23 state.user = user
24 }
25 },
26
27 actions: {
28 increment({ commit }) {
29 commit('INCREMENT')
30 },
31 async fetchTodos({ commit }) {
32 try {
33 const response = await fetch('https://jsonplaceholder.typicode.com/todos')
34 const todos = await response.json()
35 commit('SET_TODOS', todos)
36 return todos
37 } catch (error) {
38 console.error('Error fetching todos:', error)
39 return []
40 }
41 },
42 async login({ commit }, credentials) {
43 try {
44 const response = await fetch('/api/login', {
45 method: 'POST',
46 headers: { 'Content-Type': 'application/json' },
47 body: JSON.stringify(credentials)
48 })
49
50 if (!response.ok) {
51 throw new Error('Login failed')
52 }
53
54 const data = await response.json()
55 commit('SET_USER', data.user)
56 localStorage.setItem('token', data.token)
57 return true
58 } catch (error) {
59 console.error('Login error:', error)
60 return false
61 }
62 }
63 },
64
65 getters: {
66 completedTodos(state) {
67 return state.todos.filter(todo => todo.completed)
68 },
69 incompleteTodos(state) {
70 return state.todos.filter(todo => !todo.completed)
71 },
72 todoCount(state) {
73 return state.todos.length
74 },
75 isLoggedIn(state) {
76 return !!state.user
77 }
78 }
79})

在Vue应用中注册Vuex:

src/main.js
js
1import { createApp } from 'vue'
2import App from './App.vue'
3import store from './store'
4
5const app = createApp(App)
6app.use(store)
7app.mount('#app')

2.2 在组件中使用Vuex

选项式API中使用Vuex:

使用选项式API的Vuex示例
vue
1<template>
2 <div class="todo-app">
3 <h1>待办事项 ({{ todoCount }})</h1>
4
5 <div v-if="isLoading">加载中...</div>
6 <div v-else>
7 <form @submit.prevent="addNewTodo">
8 <input v-model="newTodo" placeholder="添加新待办..." />
9 <button type="submit">添加</button>
10 </form>
11
12 <ul class="todo-list">
13 <li
14 v-for="todo in todos"
15 :key="todo.id"
16 :class="{ completed: todo.completed }"
17 >
18 {{ todo.title }}
19 </li>
20 </ul>
21
22 <div class="todo-stats">
23 <p>完成: {{ completedTodos.length }}</p>
24 <p>未完成: {{ incompleteTodos.length }}</p>
25 </div>
26 </div>
27 </div>
28</template>
29
30<script>
31import { mapState, mapGetters, mapActions } from 'vuex'
32
33export default {
34 data() {
35 return {
36 isLoading: false,
37 newTodo: ''
38 }
39 },
40
41 computed: {
42 ...mapState(['todos']),
43 ...mapGetters(['completedTodos', 'incompleteTodos', 'todoCount'])
44 },
45
46 methods: {
47 ...mapActions(['fetchTodos']),
48
49 async addNewTodo() {
50 if (!this.newTodo.trim()) return
51
52 this.$store.commit('ADD_TODO', {
53 id: Date.now(),
54 title: this.newTodo,
55 completed: false
56 })
57
58 this.newTodo = ''
59 }
60 },
61
62 async mounted() {
63 this.isLoading = true
64 await this.fetchTodos()
65 this.isLoading = false
66 }
67}
68</script>

组合式API中使用Vuex:

使用组合式API的Vuex示例
vue
1<template>
2 <div class="user-profile">
3 <div v-if="isLoggedIn">
4 <h1>欢迎, {{ user?.name }}</h1>
5 <button @click="handleLogout">退出登录</button>
6 </div>
7 <div v-else>
8 <h2>用户登录</h2>
9 <form @submit.prevent="handleLogin">
10 <input v-model="email" type="email" placeholder="邮箱" required />
11 <input v-model="password" type="password" placeholder="密码" required />
12 <button type="submit" :disabled="isLoading">
13 {{ isLoading ? '登录中...' : '登录' }}
14 </button>
15 </form>
16 </div>
17
18 <div class="counter-section">
19 <h3>计数器: {{ count }}</h3>
20 <button @click="increment">增加</button>
21 </div>
22 </div>
23</template>
24
25<script setup>
26import { ref, computed } from 'vue'
27import { useStore } from 'vuex'
28
29const store = useStore()
30const email = ref('')
31const password = ref('')
32const isLoading = ref(false)
33
34// 计算属性
35const count = computed(() => store.state.count)
36const user = computed(() => store.state.user)
37const isLoggedIn = computed(() => store.getters.isLoggedIn)
38
39// 方法
40const increment = () => {
41 store.dispatch('increment')
42}
43
44const handleLogin = async () => {
45 if (!email.value || !password.value) return
46
47 isLoading.value = true
48 try {
49 const success = await store.dispatch('login', {
50 email: email.value,
51 password: password.value
52 })
53
54 if (success) {
55 email.value = ''
56 password.value = ''
57 }
58 } finally {
59 isLoading.value = false
60 }
61}
62
63const handleLogout = () => {
64 store.commit('SET_USER', null)
65 localStorage.removeItem('token')
66}
67</script>

2.3 Vuex模块化

对于大型应用,Vuex支持将store分割成模块:

src/store/modules/auth.js
js
1export default {
2 namespaced: true,
3
4 state: () => ({
5 user: null,
6 token: localStorage.getItem('token') || null,
7 error: null
8 }),
9
10 mutations: {
11 SET_USER(state, user) {
12 state.user = user
13 },
14 SET_TOKEN(state, token) {
15 state.token = token
16 if (token) {
17 localStorage.setItem('token', token)
18 } else {
19 localStorage.removeItem('token')
20 }
21 },
22 SET_ERROR(state, error) {
23 state.error = error
24 }
25 },
26
27 actions: {
28 async login({ commit }, credentials) {
29 commit('SET_ERROR', null)
30 try {
31 const response = await fetch('/api/login', {
32 method: 'POST',
33 headers: { 'Content-Type': 'application/json' },
34 body: JSON.stringify(credentials)
35 })
36
37 if (!response.ok) {
38 throw new Error('Login failed')
39 }
40
41 const data = await response.json()
42 commit('SET_USER', data.user)
43 commit('SET_TOKEN', data.token)
44
45 return true
46 } catch (error) {
47 commit('SET_ERROR', error.message)
48 return false
49 }
50 },
51
52 logout({ commit }) {
53 commit('SET_USER', null)
54 commit('SET_TOKEN', null)
55 },
56
57 async fetchUserProfile({ commit, state }) {
58 if (!state.token) return null
59
60 try {
61 const response = await fetch('/api/profile', {
62 headers: {
63 'Authorization': `Bearer ${state.token}`
64 }
65 })
66
67 if (!response.ok) {
68 throw new Error('Failed to fetch profile')
69 }
70
71 const user = await response.json()
72 commit('SET_USER', user)
73
74 return user
75 } catch (error) {
76 commit('SET_ERROR', error.message)
77 return null
78 }
79 }
80 },
81
82 getters: {
83 isLoggedIn(state) {
84 return !!state.user
85 },
86 userRole(state) {
87 return state.user?.role || 'guest'
88 },
89 hasPermission: (state) => (permission) => {
90 return state.user?.permissions?.includes(permission) || false
91 }
92 }
93}
src/store/modules/products.js
js
1export default {
2 namespaced: true,
3
4 state: () => ({
5 products: [],
6 loading: false,
7 error: null
8 }),
9
10 mutations: {
11 SET_PRODUCTS(state, products) {
12 state.products = products
13 },
14 SET_LOADING(state, loading) {
15 state.loading = loading
16 },
17 SET_ERROR(state, error) {
18 state.error = error
19 }
20 },
21
22 actions: {
23 async fetchProducts({ commit }) {
24 commit('SET_LOADING', true)
25 commit('SET_ERROR', null)
26
27 try {
28 const response = await fetch('/api/products')
29 if (!response.ok) {
30 throw new Error('Failed to fetch products')
31 }
32
33 const products = await response.json()
34 commit('SET_PRODUCTS', products)
35 return products
36 } catch (error) {
37 commit('SET_ERROR', error.message)
38 return []
39 } finally {
40 commit('SET_LOADING', false)
41 }
42 },
43
44 async fetchProductById({ commit, state }, id) {
45 // 如果已经在本地存储,直接返回
46 const existingProduct = state.products.find(p => p.id === id)
47 if (existingProduct) return existingProduct
48
49 // 否则从API获取
50 commit('SET_LOADING', true)
51 commit('SET_ERROR', null)
52
53 try {
54 const response = await fetch(`/api/products/${id}`)
55 if (!response.ok) {
56 throw new Error(`Failed to fetch product ${id}`)
57 }
58
59 const product = await response.json()
60 // 添加到本地产品列表
61 commit('SET_PRODUCTS', [...state.products, product])
62 return product
63 } catch (error) {
64 commit('SET_ERROR', error.message)
65 return null
66 } finally {
67 commit('SET_LOADING', false)
68 }
69 }
70 },
71
72 getters: {
73 featuredProducts(state) {
74 return state.products.filter(p => p.featured)
75 },
76 productById: (state) => (id) => {
77 return state.products.find(p => p.id === id)
78 }
79 }
80}

组合模块到主store:

src/store/index.js
js
1import { createStore } from 'vuex'
2import auth from './modules/auth'
3import products from './modules/products'
4import cart from './modules/cart'
5
6export default createStore({
7 modules: {
8 auth,
9 products,
10 cart
11 }
12})

在组件中使用模块化store:

vue
1<template>
2 <div>
3 <!-- 商品列表 -->
4 <div v-if="loading">加载中...</div>
5 <div v-else-if="error">{{ error }}</div>
6 <div v-else class="product-grid">
7 <div
8 v-for="product in products"
9 :key="product.id"
10 class="product-card"
11 >
12 <h3>{{ product.name }}</h3>
13 <p>{{ product.price | currency }}</p>
14 <button @click="addToCart(product.id)">加入购物车</button>
15 </div>
16 </div>
17 </div>
18</template>
19
20<script setup>
21import { computed, onMounted } from 'vue'
22import { useStore } from 'vuex'
23
24const store = useStore()
25
26// 映射模块状态
27const products = computed(() => store.state.products.products)
28const loading = computed(() => store.state.products.loading)
29const error = computed(() => store.state.products.error)
30
31// 方法
32const fetchProducts = () => store.dispatch('products/fetchProducts')
33const addToCart = (productId) => store.dispatch('cart/addToCart', productId)
34
35onMounted(fetchProducts)
36</script>

3. Composition API状态管理

对于中小型应用,可以使用Vue 3的Composition API创建自定义状态管理方案,无需引入额外库。

3.1 简单状态管理

使用provide/injectreactive创建简单状态管理:

src/composables/useThemeState.js
js
1import { reactive, provide, inject } from 'vue'
2
3// 创建一个唯一的key
4const ThemeStateSymbol = Symbol('ThemeState')
5
6export function provideThemeState() {
7 // 创建响应式状态
8 const state = reactive({
9 darkMode: false,
10 fontSize: 'medium',
11 colorScheme: 'blue'
12 })
13
14 // 主题切换函数
15 function toggleDarkMode() {
16 state.darkMode = !state.darkMode
17 document.documentElement.classList.toggle('dark', state.darkMode)
18 }
19
20 // 设置字体大小
21 function setFontSize(size) {
22 state.fontSize = size
23 document.documentElement.setAttribute('data-font-size', size)
24 }
25
26 // 设置颜色主题
27 function setColorScheme(scheme) {
28 state.colorScheme = scheme
29 document.documentElement.setAttribute('data-color-scheme', scheme)
30 }
31
32 // 提供状态和方法
33 provide(ThemeStateSymbol, {
34 state,
35 toggleDarkMode,
36 setFontSize,
37 setColorScheme
38 })
39}
40
41export function useThemeState() {
42 // 注入状态和方法
43 const theme = inject(ThemeStateSymbol)
44
45 if (!theme) {
46 throw new Error('useThemeState() must be used within a component that calls provideThemeState()')
47 }
48
49 return theme
50}

在应用中使用:

src/App.vue
vue
1<template>
2 <div :class="{ 'dark-theme': state.darkMode }">
3 <header>
4 <button @click="toggleDarkMode">
5 {{ state.darkMode ? '切换到亮色模式' : '切换到暗色模式' }}
6 </button>
7
8 <div class="font-size-controls">
9 <button @click="setFontSize('small')">小</button>
10 <button @click="setFontSize('medium')">中</button>
11 <button @click="setFontSize('large')">大</button>
12 </div>
13 </header>
14
15 <router-view />
16 </div>
17</template>
18
19<script setup>
20import { provideThemeState, useThemeState } from './composables/useThemeState'
21
22// 提供主题状态
23provideThemeState()
24
25// 使用主题状态
26const { state, toggleDarkMode, setFontSize } = useThemeState()
27</script>

3.2 完整状态管理

创建一个具有类似Vuex/Pinia特性的状态管理解决方案:

src/composables/createStore.js
js
1import { reactive, readonly, provide, inject, computed, watch } from 'vue'
2
3export function createStore(options) {
4 // 创建唯一symbol
5 const storeSymbol = Symbol('store')
6
7 function useCreateStore() {
8 // 状态初始化
9 const state = reactive(typeof options.state === 'function' ? options.state() : options.state || {})
10
11 // 处理getters
12 const getters = {}
13 if (options.getters) {
14 Object.keys(options.getters).forEach(getterName => {
15 getters[getterName] = computed(() => options.getters[getterName](state, getters))
16 })
17 }
18
19 // 处理actions
20 const actions = {}
21 if (options.actions) {
22 Object.keys(options.actions).forEach(actionName => {
23 actions[actionName] = (...args) => options.actions[actionName]({ state, getters, actions }, ...args)
24 })
25 }
26
27 // 持久化支持
28 if (options.persist?.enabled) {
29 const storageKey = options.persist.key || storeSymbol.toString()
30 const storage = options.persist.storage || localStorage
31
32 // 从存储中恢复状态
33 const savedState = storage.getItem(storageKey)
34 if (savedState) {
35 const parsed = JSON.parse(savedState)
36 Object.assign(state, parsed)
37 }
38
39 // 保存状态到存储
40 watch(
41 () => JSON.parse(JSON.stringify(state)),
42 (newState) => {
43 storage.setItem(storageKey, JSON.stringify(newState))
44 },
45 { deep: true }
46 )
47 }
48
49 return {
50 state: readonly(state),
51 getters: readonly(getters),
52 actions,
53 // 内部使用,用于重置状态
54 _reset: () => Object.assign(state, typeof options.state === 'function' ? options.state() : options.state || {})
55 }
56 }
57
58 const store = useCreateStore()
59
60 // 提供store
61 function provideStore() {
62 provide(storeSymbol, store)
63 return store
64 }
65
66 // 注入store
67 function useStore() {
68 const injectedStore = inject(storeSymbol, null)
69 if (!injectedStore) {
70 throw new Error('Store not provided. Make sure to call provideStore() in a parent component.')
71 }
72 return injectedStore
73 }
74
75 return {
76 provideStore,
77 useStore
78 }
79}

使用自定义store:

src/stores/userStore.js
js
1import { createStore } from '../composables/createStore'
2
3export const { provideStore: provideUserStore, useStore: useUserStore } = createStore({
4 state: () => ({
5 user: null,
6 token: localStorage.getItem('token') || null,
7 isLoading: false,
8 error: null
9 }),
10
11 getters: {
12 isLoggedIn: (state) => !!state.token && !!state.user,
13 username: (state) => state.user?.name || 'Guest',
14 userRole: (state) => state.user?.role || 'guest'
15 },
16
17 actions: {
18 async login({ state }, credentials) {
19 state.isLoading = true
20 state.error = null
21
22 try {
23 const response = await fetch('/api/login', {
24 method: 'POST',
25 headers: { 'Content-Type': 'application/json' },
26 body: JSON.stringify(credentials)
27 })
28
29 if (!response.ok) {
30 throw new Error('Login failed')
31 }
32
33 const data = await response.json()
34 state.user = data.user
35 state.token = data.token
36 localStorage.setItem('token', data.token)
37
38 return true
39 } catch (error) {
40 state.error = error.message
41 return false
42 } finally {
43 state.isLoading = false
44 }
45 },
46
47 logout({ state }) {
48 state.user = null
49 state.token = null
50 localStorage.removeItem('token')
51 }
52 },
53
54 persist: {
55 enabled: true,
56 key: 'user-store',
57 storage: localStorage
58 }
59})

在应用中使用:

src/App.vue
vue
1<template>
2 <div class="app">
3 <header>
4 <nav>
5 <span v-if="isLoggedIn">欢迎, {{ username }}</span>
6 <button v-if="isLoggedIn" @click="actions.logout">退出登录</button>
7 <router-link v-else to="/login">登录</router-link>
8 </nav>
9 </header>
10
11 <router-view />
12 </div>
13</template>
14
15<script setup>
16import { provideUserStore, useUserStore } from './stores/userStore'
17
18// 提供用户store
19provideUserStore()
20
21// 使用用户store
22const { state, getters, actions } = useUserStore()
23const { isLoggedIn, username } = getters
24</script>

4. 状态管理最佳实践

4.1 何时使用状态管理

状态管理不是必须的,应根据应用复杂度决定:

  1. 小型应用: 使用props、emits和简单的provide/inject
  2. 中型应用: 使用Composition API状态管理或轻量级Pinia
  3. 大型应用: 使用Pinia或Vuex进行模块化状态管理

4.2 状态组织原则

  1. 单一职责: 每个store只管理一个领域的状态
  2. 最小化状态: 只存储必要的状态,避免冗余数据
  3. 规范命名: 使用一致的命名约定
  4. 避免重复: 避免在多个store中存储相同数据
  5. 层次结构: 复杂状态采用合理的层次结构

4.3 性能优化

  1. 避免过度订阅: 仅订阅需要的状态
  2. 使用计算属性: 缓存派生状态
  3. 懒加载模块: 按需加载状态模块
  4. 精细化更新: 避免整体状态对象替换
  5. 防抖/节流: 频繁变化的状态使用防抖/节流

4.4 调试技巧

  1. 使用Vue开发工具: Vue Devtools可检查Pinia/Vuex状态
  2. 时间旅行调试: 使用开发工具的时间旅行功能
  3. 日志插件: 添加日志插件记录状态变化
  4. 状态快照: 在关键点保存状态快照
  5. 中间件: 使用中间件拦截状态操作

参与讨论