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'45const app = createApp(App)6const pinia = createPinia()78app.use(pinia)9app.mount('#app')1.2 创建Pinia Store
使用defineStore创建一个store:
src/stores/counter.js
js
1import { defineStore } from 'pinia'23// 基于Option API风格的store4export const useCounterStore = defineStore('counter', {5 // state6 state: () => ({7 count: 0,8 name: 'Counter',9 users: []10 }),11 12 // getters13 getters: {14 doubleCount: (state) => state.count * 2,15 // 访问其他getter16 doubleCountPlusOne() {17 return this.doubleCount + 118 }19 },20 21 // actions22 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 = data31 } 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'34// 基于组合式API风格的store5export const useUserStore = defineStore('user', () => {6 // state7 const user = ref(null)8 const isLoggedIn = ref(false)9 const token = ref(localStorage.getItem('token') || null)10 const error = ref(null)11 12 // getters13 const userFullName = computed(() => {14 if (!user.value) return ''15 return `${user.value.firstName} ${user.value.lastName}`16 })17 18 // actions19 async function login(credentials) {20 try {21 error.value = null22 // 模拟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.user37 token.value = data.token38 isLoggedIn.value = true39 40 // 保存token到本地存储41 localStorage.setItem('token', data.token)42 43 return true44 } catch (e) {45 error.value = e.message46 return false47 }48 }49 50 function logout() {51 user.value = null52 token.value = null53 isLoggedIn.value = false54 localStorage.removeItem('token')55 }56 57 async function fetchUserProfile() {58 if (!token.value) return null59 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 = true73 74 return user.value75 } catch (e) {76 error.value = e.message77 return null78 }79 }80 81 return {82 // state83 user,84 isLoggedIn,85 token,86 error,87 // getters88 userFullName,89 // actions90 login,91 logout,92 fetchUserProfile93 }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>2627<script setup>28import { ref } from 'vue'29import { useUserStore } from '../stores/user'30import { useCounterStore } from '../stores/counter'3132const userStore = useUserStore()33const counterStore = useCounterStore()3435const email = ref('')36const password = ref('')37const isLoading = ref(false)3839const handleLogin = async () => {40 isLoading.value = true41 try {42 const success = await userStore.login({43 email: email.value,44 password: password.value45 })46 47 if (success) {48 email.value = ''49 password.value = ''50 }51 } finally {52 isLoading.value = false53 }54}55</script>1.4 Pinia高级特性
Store间通信
Store之间可以相互引用和交互:
stores/cart.js
js
1import { defineStore } from 'pinia'2import { useProductStore } from './product'3import { useUserStore } from './user'45export 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.quantity17 }, 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 += quantity38 } else {39 this.items.push({40 productId,41 quantity42 })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 = quantity60 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.totalPrice77 }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 error100 }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'23export 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'56const app = createApp(App)7const pinia = createPinia()89// 注册Pinia插件10pinia.use(piniaPersistPlugin)1112app.use(pinia)13app.mount('#app')配置Store启用持久化:
src/stores/theme.js
js
1import { defineStore } from 'pinia'23export 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.darkMode13 document.documentElement.classList.toggle('dark', this.darkMode)14 },15 setAccentColor(color) {16 this.accentColor = color17 document.documentElement.style.setProperty('--accent-color', color)18 }19 },20 21 // 启用持久化22 persist: true23})热重载与开发工具
Pinia支持热模块替换(HMR)和Vue开发工具:
src/stores/counter.js
js
1import { defineStore } from 'pinia'23export const useCounterStore = defineStore('counter', {4 state: () => ({5 count: 06 }),7 actions: {8 increment() {9 this.count++10 }11 }12})1314// 启用热重载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'23export default createStore({4 state() {5 return {6 count: 0,7 todos: [],8 user: null9 }10 },11 12 mutations: {13 INCREMENT(state) {14 state.count++15 },16 SET_TODOS(state, todos) {17 state.todos = todos18 },19 ADD_TODO(state, todo) {20 state.todos.push(todo)21 },22 SET_USER(state, user) {23 state.user = user24 }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 todos37 } 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 true58 } catch (error) {59 console.error('Login error:', error)60 return false61 }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.length74 },75 isLoggedIn(state) {76 return !!state.user77 }78 }79})在Vue应用中注册Vuex:
src/main.js
js
1import { createApp } from 'vue'2import App from './App.vue'3import store from './store'45const 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>2930<script>31import { mapState, mapGetters, mapActions } from 'vuex'3233export 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()) return51 52 this.$store.commit('ADD_TODO', {53 id: Date.now(),54 title: this.newTodo,55 completed: false56 })57 58 this.newTodo = ''59 }60 },61 62 async mounted() {63 this.isLoading = true64 await this.fetchTodos()65 this.isLoading = false66 }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>2425<script setup>26import { ref, computed } from 'vue'27import { useStore } from 'vuex'2829const store = useStore()30const email = ref('')31const password = ref('')32const isLoading = ref(false)3334// 计算属性35const count = computed(() => store.state.count)36const user = computed(() => store.state.user)37const isLoggedIn = computed(() => store.getters.isLoggedIn)3839// 方法40const increment = () => {41 store.dispatch('increment')42}4344const handleLogin = async () => {45 if (!email.value || !password.value) return46 47 isLoading.value = true48 try {49 const success = await store.dispatch('login', {50 email: email.value,51 password: password.value52 })53 54 if (success) {55 email.value = ''56 password.value = ''57 }58 } finally {59 isLoading.value = false60 }61}6263const 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: null8 }),9 10 mutations: {11 SET_USER(state, user) {12 state.user = user13 },14 SET_TOKEN(state, token) {15 state.token = token16 if (token) {17 localStorage.setItem('token', token)18 } else {19 localStorage.removeItem('token')20 }21 },22 SET_ERROR(state, error) {23 state.error = error24 }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 true46 } catch (error) {47 commit('SET_ERROR', error.message)48 return false49 }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 null59 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 user75 } catch (error) {76 commit('SET_ERROR', error.message)77 return null78 }79 }80 },81 82 getters: {83 isLoggedIn(state) {84 return !!state.user85 },86 userRole(state) {87 return state.user?.role || 'guest'88 },89 hasPermission: (state) => (permission) => {90 return state.user?.permissions?.includes(permission) || false91 }92 }93}src/store/modules/products.js
js
1export default {2 namespaced: true,3 4 state: () => ({5 products: [],6 loading: false,7 error: null8 }),9 10 mutations: {11 SET_PRODUCTS(state, products) {12 state.products = products13 },14 SET_LOADING(state, loading) {15 state.loading = loading16 },17 SET_ERROR(state, error) {18 state.error = error19 }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 products36 } 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 existingProduct48 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 product63 } catch (error) {64 commit('SET_ERROR', error.message)65 return null66 } 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'56export default createStore({7 modules: {8 auth,9 products,10 cart11 }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>1920<script setup>21import { computed, onMounted } from 'vue'22import { useStore } from 'vuex'2324const store = useStore()2526// 映射模块状态27const products = computed(() => store.state.products.products)28const loading = computed(() => store.state.products.loading)29const error = computed(() => store.state.products.error)3031// 方法32const fetchProducts = () => store.dispatch('products/fetchProducts')33const addToCart = (productId) => store.dispatch('cart/addToCart', productId)3435onMounted(fetchProducts)36</script>3. Composition API状态管理
对于中小型应用,可以使用Vue 3的Composition API创建自定义状态管理方案,无需引入额外库。
3.1 简单状态管理
使用provide/inject和reactive创建简单状态管理:
src/composables/useThemeState.js
js
1import { reactive, provide, inject } from 'vue'23// 创建一个唯一的key4const ThemeStateSymbol = Symbol('ThemeState')56export 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.darkMode17 document.documentElement.classList.toggle('dark', state.darkMode)18 }19 20 // 设置字体大小21 function setFontSize(size) {22 state.fontSize = size23 document.documentElement.setAttribute('data-font-size', size)24 }25 26 // 设置颜色主题27 function setColorScheme(scheme) {28 state.colorScheme = scheme29 document.documentElement.setAttribute('data-color-scheme', scheme)30 }31 32 // 提供状态和方法33 provide(ThemeStateSymbol, {34 state,35 toggleDarkMode,36 setFontSize,37 setColorScheme38 })39}4041export 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 theme50}在应用中使用:
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>1819<script setup>20import { provideThemeState, useThemeState } from './composables/useThemeState'2122// 提供主题状态23provideThemeState()2425// 使用主题状态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'23export function createStore(options) {4 // 创建唯一symbol5 const storeSymbol = Symbol('store')6 7 function useCreateStore() {8 // 状态初始化9 const state = reactive(typeof options.state === 'function' ? options.state() : options.state || {})10 11 // 处理getters12 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 // 处理actions20 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 || localStorage31 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 // 提供store61 function provideStore() {62 provide(storeSymbol, store)63 return store64 }65 66 // 注入store67 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 injectedStore73 }74 75 return {76 provideStore,77 useStore78 }79}使用自定义store:
src/stores/userStore.js
js
1import { createStore } from '../composables/createStore'23export const { provideStore: provideUserStore, useStore: useUserStore } = createStore({4 state: () => ({5 user: null,6 token: localStorage.getItem('token') || null,7 isLoading: false,8 error: null9 }),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 = true20 state.error = null21 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.user35 state.token = data.token36 localStorage.setItem('token', data.token)37 38 return true39 } catch (error) {40 state.error = error.message41 return false42 } finally {43 state.isLoading = false44 }45 },46 47 logout({ state }) {48 state.user = null49 state.token = null50 localStorage.removeItem('token')51 }52 },53 54 persist: {55 enabled: true,56 key: 'user-store',57 storage: localStorage58 }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>1415<script setup>16import { provideUserStore, useUserStore } from './stores/userStore'1718// 提供用户store19provideUserStore()2021// 使用用户store22const { state, getters, actions } = useUserStore()23const { isLoggedIn, username } = getters24</script>4. 状态管理最佳实践
4.1 何时使用状态管理
状态管理不是必须的,应根据应用复杂度决定:
- 小型应用: 使用props、emits和简单的provide/inject
- 中型应用: 使用Composition API状态管理或轻量级Pinia
- 大型应用: 使用Pinia或Vuex进行模块化状态管理
4.2 状态组织原则
- 单一职责: 每个store只管理一个领域的状态
- 最小化状态: 只存储必要的状态,避免冗余数据
- 规范命名: 使用一致的命名约定
- 避免重复: 避免在多个store中存储相同数据
- 层次结构: 复杂状态采用合理的层次结构
4.3 性能优化
- 避免过度订阅: 仅订阅需要的状态
- 使用计算属性: 缓存派生状态
- 懒加载模块: 按需加载状态模块
- 精细化更新: 避免整体状态对象替换
- 防抖/节流: 频繁变化的状态使用防抖/节流
4.4 调试技巧
- 使用Vue开发工具: Vue Devtools可检查Pinia/Vuex状态
- 时间旅行调试: 使用开发工具的时间旅行功能
- 日志插件: 添加日志插件记录状态变化
- 状态快照: 在关键点保存状态快照
- 中间件: 使用中间件拦截状态操作
评论