Vuex的使用

1. Vuex

VuexVue.js 应用的状态管理库,用于集中管理所有组件的状态【数据】。是专门在Vue中实现集中管理共享数据的vue插件,适用于任意组件间的通信,尤其是跨组件通信,如果只依赖于全局事件总线,代码就会很冗余,且在大型项目中难以维护。那么什么时候使用Vuex呢,官网说法如下:

  • 多个组件依赖于统一状态。
  • 来自不同的组件行为需要变更同一状态。

VueX的核心流程总结如下:

  • State:存储应用状态 ,即this.$store.state.xxx
  • Mutation:通过 commit 修改状态 ,即this.$store.commit('mutationName')
  • Action:通过 dispatch 触发异步操作 ,即this.$store.dispatch('actionName')
  • Getter:通过计算属性获取派生状态 ,即this.$store.getters.xxx

1.1 Vuex的环境搭建

需要注意的是,vue2只能使用vuex 3.x的版本,vue3只能使用vuex 4.x的版本,对于Vue2项目,安装指令如下:

npm install vuex3@3

Vue CLI 项目中创建 src/store 文件夹并在其中编写 index.js ,该结构Vuex 的推荐项目结构,主要目的是为了代码的可维护性、模块化和遵循最佳实践。src/storeVuex状态管理的专用目录,核心作用包括:

  • 集中管理状态:所有全局状态state、操作mutations/actions和派生状态getters集中在此目录,避免代码分散。

  • 模块化开发:方便将大型应用的状态拆分为多个模块modules,每个模块对应一个文件,如 user.jscart.js

    // 典型的项目结构
    src/
    ├── store/
    │   ├── index.js          # Store 入口,整合模块和全局配置
    │   ├── actions.js        # 全局 actions(可选)
    │   ├── mutations.js      # 全局 mutations(可选)
    │   └── modules/
    │       ├── user.js       # 用户模块
    │       └── cart.js       # 购物车模块
    ├── components/           # 组件
    ├── router/               # 路由
    └── main.js               # 应用入口
    

store/index.jsVuex Store 的入口文件,主要职责是:

  • 创建并导出Vuex Store实例:在此文件中初始化 Vuexstatemutationsactionsgetters,并导出 store 实例供 Vue 应用使用。

    // store/index.js
    import Vue from 'vue';
    import Vuex from 'vuex';
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
        // 用于存放数据
      state: { count: 0 },
        // 用于数据处理
      mutations: { increment(state) { state.count++ } },
       // 用于响应组件中的动作
      actions: { increment({ commit }) { commit('increment') } },
      getters: { doubleCount: state => state.count * 2 }
    });
    
  • 整合模块:如果项目使用模块化,index.js 负责导入所有模块并合并到 Store 中:

    // store/index.js
    import user from './modules/user';
    import cart from './modules/cart';
    
    export default new Vuex.Store({
      modules: { user, cart }
    });
    

1.2 Vuex的基本使用

首先搭建一个最简单的vue CLI项目,项目结构如下:

src/
├── store/
│   └── index.js       # Vuex 主文件
├── components/
│   └── Counter.vue    # 使用 Vuex 的组件
└── main.js            # Vue 入口文件
  • 创建Store

    import Vue from 'vue';
    import Vuex from 'vuex';
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
      state: {
        count: 0,
        message: 'Hello Vuex'
      },
      mutations: {
        // 同步修改 state
        INCREMENT(state) {
          state.count++;
        },
        SET_MESSAGE(state, newMessage) {
          state.message = newMessage;
        }
      },
      actions: {
        // 异步操作
        incrementAsync({ commit }) {
          setTimeout(() => {
            commit('INCREMENT');
          }, 1000);
        },
        updateMessage({ commit }, newMessage) {
          commit('SET_MESSAGE', newMessage);
        }
      },
      getters: {
        reversedMessage: state => {
          return state.message.split('').reverse().join('');
        }
      }
    });
    
  • main.js挂载Store

    import Vue from 'vue';
    import App from './App.vue';
    import store from './store';
    
    new Vue({
      store,    // 挂载到根实例
      render: h => h(App)
    }).$mount('#app');
    
  • Counter.vue组件中使用

    <template>
      <div>
        <p>Count: {{ count }}</p>
        <p>Message: {{ message }}</p>
        <p>Reversed Message: {{ reversedMessage }}</p>
    
        <button @click="increment">+1 (Mutation)</button>
        <button @click="incrementAsync">+1 Async (Action)</button>
        <input v-model="inputMessage" @change="updateMessage">
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          inputMessage: ''
        };
      },
      computed: {
        // 获取 state
        count() {
          return this.$store.state.count;
        },
        message() {
          return this.$store.state.message;
        },
        // 使用 getter
        reversedMessage() {
          return this.$store.getters.reversedMessage;
        }
      },
      methods: {
        // 提交 mutation
        increment() {
          this.$store.commit('INCREMENT');
        },
        // 分发 action
        incrementAsync() {
          this.$store.dispatch('incrementAsync');
        },
        updateMessage() {
          this.$store.dispatch('updateMessage', this.inputMessage);
        }
      }
    };
    </script>
    

1.3 mapStatemapGetters

Vuex 中,mapStatemapGetters 是用于简化组件中访问 StateGetters 的辅助函数。它们通过映射关系将 Store 中的状态或计算属性直接绑定到组件的计算属性中,减少冗余代码。

  • mapState的用法:用于将 Vuex Store 中的 state 映射为组件的计算属性。

    import { mapState } from 'vuex';
    
    export default {
      computed: {
        // 使用对象展开符将 mapState 返回的多个计算属性合并到 computed 中
        ...mapState({
          count: state => state.count,        // 箭头函数形式
          message: 'message',                 // 字符串简写,等价于 state => state.message)
          userInfo: state => state.user.info  // 嵌套状态访问
        })
      }
    };
    
    // 数组简写,适合同名映射
    computed: {
      ...mapState(['count', 'message', 'user'])
    }
    
    // 使用了模块化且模块启用了命名空间namespaced: true需指定模块路径
    computed: {
      ...mapState('user', {
        username: state => state.username
      })
    }
    
  • mapGetters:用于将 Vuex Store 中的 getters 映射为组件的计算属性。

    import { mapGetters } from 'vuex';
    
    export default {
      computed: {
        ...mapGetters({
          reversedMessage: 'reversedMessage',     // 映射 this.reversedMessage 为 this.$store.getters.reversedMessage
          formattedDate: 'user/formattedBirthday' // 模块化命名空间下的 getter
        })
      }
    };
    
    // 数组简写
    computed: {
      ...mapGetters(['reversedMessage', 'user/formattedBirthday'])
    }
    
    // 模块化命名空间处理
    computed: {
      ...mapGetters('user', ['formattedBirthday']) // 等价于 this.$store.getters['user/formattedBirthday']
    }
    

1.4 mapActionmapMutaions

Vuex 中,mapActionsmapMutations 是用于简化组件中调用 ActionsMutations 的辅助函数。它们将 Vuex 中的方法映射到组件的 methods 中,减少重复代码。

  • mapActions:用于将 Vuexactions 映射为组件的方法,直接分发 action

    import { mapActions } from 'vuex';
    
    export default {
      methods: {
        // 对象语法(可重命名)
        ...mapActions({
          fetchData: 'loadData',   // 将 this.fetchData() 映射为 this.$store.dispatch('loadData')
          submit: 'user/submitForm' // 模块化路径
        }),
    
        // 数组语法(同名映射)
        ...mapActions([
          'loadData',             // 将 this.loadData() 映射为 this.$store.dispatch('loadData')
          'user/fetchProfile'
        ])
      }
    };
    
    methods: {
      ...mapActions('cart', [
        'addToCart'              // 映射 this.addToCart() 为 this.$store.dispatch('cart/addToCart')
      ])
    }
    
  • mapMutatios:用于将 Vuexmutations 映射为组件的方法,直接提交 mutation

    import { mapMutations } from 'vuex';
    
    export default {
      methods: {
        // 对象语法(可重命名)
        ...mapMutations({
          add: 'INCREMENT',      // 将 this.add() 映射为 this.$store.commit('INCREMENT')
          setMsg: 'SET_MESSAGE'
        }),
    
        // 数组语法(同名映射)
        ...mapMutations([
          'INCREMENT',          // 将 this.INCREMENT() 映射为 this.$store.commit('INCREMENT')
          'SET_MESSAGE'
        ])
      }
    };
    
    methods: {
      ...mapMutations('user', {
        updateName: 'SET_NAME'  // 映射 this.updateName() 为 this.$store.commit('user/SET_NAME')
      })
    }
    

2. Vuex模块化

Vuex 中,模块化Modularization 是一种将大型应用的状态管理拆分为独立模块的机制,适用于复杂项目。那么为什么需要模块化:

  • 代码解耦:将不同业务域的状态,如用户、商品、订单分离到独立模块。
  • 命名空间隔离:避免 statemutationaction 的命名冲突。
  • 可维护性:每个模块独立管理,便于团队协作和代码维护。
  • 按需加载:支持动态注册模块,优化应用性能。

下面是模块化基础结构:

src/
└── store/
    ├── index.js          # 主入口文件
    ├── modules/
    │   ├── user.js       # 用户模块
    │   ├── product.js    # 商品模块
    │   └── cart.js       # 购物车模块
    └── getters.js        # 全局 getters(可选)

2.1 基本使用

  • store/modules/user.js

    export default {
      namespaced: true, // 开启命名空间
      state: () => ({
        id: null,
        name: 'Guest',
        isLoggedIn: false
      }),
      mutations: {
        SET_USER(state, payload) {
          state.id = payload.id;
          state.name = payload.name;
          state.isLoggedIn = true;
        },
        LOGOUT(state) {
          state.id = null;
          state.name = 'Guest';
          state.isLoggedIn = false;
        }
      },
      actions: {
        async login({ commit }, credentials) {
          const userData = await api.login(credentials);
          commit('SET_USER', userData);
        }
      },
      getters: {
        username: state => state.name,
        isAdmin: state => state.id === 1
      }
    };
    
  • 主入口文件 store/index.js

import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';
import cart from './modules/cart';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    user, // 注册用户模块
    cart  // 注册购物车模块
  }
});

2.2 模块核心配置

  • namespace: true:开启模块的命名空间,使模块的 statemutationsactionsgetters 与其他模块隔离。

  • 模块的局部状态

    • state:始终是模块的局部状态,即使不开启命名空间。

    • 访问根节点状态:在模块的 gettersactions 中,可通过第三个参数访问根状态。

      getters: {
        fullName(state, getters, rootState) {
          return `${state.name} (${rootState.appVersion})`;
        }
      }
      

2.3 组件中访问模块

  • 直接访问【不推荐】

    // 访问模块的 state
    this.$store.state.user.name;
    
    // 调用模块的 action
    this.$store.dispatch('user/login', credentials);
    
    // 调用模块的 mutation
    this.$store.commit('user/LOGOUT');
    
  • 使用辅助函数 + 命名空间

    import { mapState, mapActions } from 'vuex';
    
    export default {
      computed: {
        // 映射模块的 state
        ...mapState('user', ['name', 'isLoggedIn']),
        // 映射模块的 getter
        ...mapGetters('user', ['isAdmin'])
      },
      methods: {
        // 映射模块的 action
        ...mapActions('user', ['login']),
        // 映射模块的 mutation
        ...mapMutations('user', ['LOGOUT'])
      }
    };
    
  • 使用 createNamespacedHelpers 【推荐】

    import { createNamespacedHelpers } from 'vuex';
    const { mapState, mapActions } = createNamespacedHelpers('user');
    
    export default {
      computed: {
        ...mapState(['name', 'isLoggedIn']),
        ...mapGetters(['isAdmin'])
      },
      methods: {
        ...mapActions(['login']),
        ...mapMutations(['LOGOUT'])
      }
    };
    

2.4 模块间通信

  • 访问根节点状态、方法:在模块的 actionsgetters 中。

    actions: {
      fetchData({ commit, rootState, rootGetters }) {
        // rootState 访问根状态
        const appVersion = rootState.appVersion;
        
        // rootGetters 访问根 getters
        const isMobile = rootGetters.isMobile;
      }
    }
    
  • 调用其他模块的action:使用 { root: true } 参数。

    actions: {
      async checkout({ dispatch }) {
        // 调用另一个模块的 action
        await dispatch('cart/loadCartItems', null, { root: true });
      }
    }
    

2.5 动态注册模块

  • 注册模块

    // 在组件或逻辑中动态添加模块
    import userProfile from './modules/userProfile';
    
    this.$store.registerModule('userProfile', userProfile);
    
  • 卸载模块

    this.$store.unregisterModule('userProfile');
    

2.6 完整案例

通过下面案例,可以清晰理解Vuex模块化在复杂项目中的应用方式,包括模块定义、通信机制和工程化实践。项目结构如下:

src/
├── store/
│   ├── index.js          # Store 主入口
│   ├── modules/
│   │   ├── user.js       # 用户模块
│   │   ├── cart.js       # 购物车模块
│   │   └── product.js    # 商品模块(动态注册)
│   └── types.js          # Mutation/Action 类型常量
└── components/
    └── App.vue           # 主组件

完整代码如下:

  • 类型常量:store/types.js

    // Mutation 类型
    export const SET_USER = 'SET_USER';
    export const ADD_TO_CART = 'ADD_TO_CART';
    
    // Action 类型
    export const FETCH_USER = 'FETCH_USER';
    
  • 用户模块:store/modules/user.js

    import { FETCH_USER, SET_USER } from '../types';
    
    export default {
      namespaced: true,
      state: () => ({
        id: null,
        name: 'Guest',
        isLoggedIn: false
      }),
      mutations: {
        [SET_USER](state, payload) {
          state.id = payload.id;
          state.name = payload.name;
          state.isLoggedIn = true;
        }
      },
      actions: {
        async [FETCH_USER]({ commit }, userId) {
          const response = await fetch(`/api/users/${userId}`);
          const userData = await response.json();
          commit(SET_USER, userData);
        }
      },
      getters: {
        username: state => state.name
      }
    };
    
  • 购物车模块 store/modules/cart.js

    import { ADD_TO_CART } from '../types';
    
    export default {
      namespaced: true,
      state: () => ({
        items: [],
        total: 0
      }),
      mutations: {
        [ADD_TO_CART](state, product) {
          state.items.push(product);
          state.total += product.price;
        }
      },
      actions: {
        // 调用商品模块的 action(假设商品模块已注册)
        async addProduct({ commit, dispatch }, productId) {
          const product = await dispatch(
            'product/fetchProduct', 
            productId, 
            { root: true }
          );
          commit(ADD_TO_CART, product);
        }
      }
    };
    
  • 商品模块:store/modules/product.js

    export default {
      namespaced: true,
      state: () => ({
        list: []
      }),
      actions: {
        async fetchProduct({ commit }, productId) {
          const response = await fetch(`/api/products/${productId}`);
          return response.json();
        }
      }
    };
    
  • Store 主入口 store/index.js

    import Vue from 'vue';
    import Vuex from 'vuex';
    import user from './modules/user';
    import cart from './modules/cart';
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
      modules: {
        user,
        cart
      },
      // 全局状态(可选)
      state: {
        appVersion: '1.0.0'
      },
      getters: {
        appInfo: state => `App v${state.appVersion}`
      }
    });
    
  • 组件中使用:components/App.vue

    <template>
      <div>
        <!-- 用户模块 -->
        <div v-if="isLoggedIn">
          <p>Welcome, {{ username }}!</p>
          <button @click="logout">Logout</button>
        </div>
        <div v-else>
          <button @click="login(1)">Login as User 1</button>
        </div>
    
        <!-- 购物车模块 -->
        <div>
          <h3>Cart Total: ${{ cartTotal }}</h3>
          <button @click="addToCart(101)">Add Product 101</button>
          <ul>
            <li v-for="item in cartItems" :key="item.id">{{ item.name }} - ${{ item.price }}</li>
          </ul>
        </div>
    
        <!-- 全局状态 -->
        <p>{{ appInfo }}</p>
      </div>
    </template>
    
    <script>
    import { mapState, mapActions, mapGetters } from 'vuex';
    import { FETCH_USER, SET_USER } from '../store/types';
    
    export default {
      computed: {
        // 用户模块
        ...mapState('user', ['isLoggedIn']),
        ...mapGetters('user', ['username']),
    
        // 购物车模块
        ...mapState('cart', ['items', 'total']),
        cartItems: state => state.cart.items,
        cartTotal: state => state.cart.total,
    
        // 全局状态
        ...mapGetters(['appInfo'])
      },
      methods: {
        // 用户模块的 action 和 mutation
        ...mapActions('user', {
          login: FETCH_USER 
        }),
        ...mapMutations('user', {
          logout: SET_USER 
        }),
    
        // 购物车模块的 action
        ...mapActions('cart', ['addProduct']),
    
        // 封装商品添加逻辑
        addToCart(productId) {
          this.addProduct(productId);
        }
      },
      created() {
        // 动态注册商品模块
        import('../store/modules/product').then(module => {
          this.$store.registerModule('product', module.default);
        });
      },
      beforeDestroy() {
        // 卸载商品模块(按需)
        this.$store.unregisterModule('product');
      }
    };
    </script>
    

你可能感兴趣的:(vue,前端)