🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# Vuex > 原文:[https://docs.gitlab.com/ee/development/fe_guide/vuex.html](https://docs.gitlab.com/ee/development/fe_guide/vuex.html) * [Separation of concerns](#separation-of-concerns) * [File structure](#file-structure) * [`index.js`](#indexjs) * [`state.js`](#statejs) * [Access `state` properties](#access-state-properties) * [`actions.js`](#actionsjs) * [Dispatching actions](#dispatching-actions) * [`mutations.js`](#mutationsjs) * [Naming Pattern: `REQUEST` and `RECEIVE` namespaces](#naming-pattern-request-and-receive-namespaces) * [Updating complex state](#updating-complex-state) * [`getters.js`](#gettersjs) * [`mutation_types.js`](#mutation_typesjs) * [Initializing a store’s state](#initializing-a-stores-state) * [Why not just …spread the initial state?](#why-not-just-spread-the-initial-state) * [Communicating with the Store](#communicating-with-the-store) * [Vuex Gotchas](#vuex-gotchas) * [Testing Vuex](#testing-vuex) * [Testing Vuex concerns](#testing-vuex-concerns) * [Testing components that need a store](#testing-components-that-need-a-store) * [Two way data binding](#two-way-data-binding) # Vuex[](#vuex "Permalink") 如果将状态管理与组件分离有明显的好处(例如,由于状态复杂性),我们建议使用[Vuex 而](https://vuex.vuejs.org)不是其他任何 Flux 模式. 否则,请随时管理组件中的状态. 在以下情况下,应强烈考虑 Vuex: * 您期望应用程序的多个部分对状态变化做出反应 * 需要在多个组件之间共享数据 * 与后端的交互非常复杂,例如多个 API 调用 * 该应用程序涉及通过传统 REST API 和 GraphQL 与后端进行交互(尤其是将 REST API 移至 GraphQL 时,这是一项待处理的后端任务) *注意:以下*所有内容在[Vuex](https://vuex.vuejs.org)官方[文档](https://vuex.vuejs.org)中有更详细的[说明](https://vuex.vuejs.org) . ## Separation of concerns[](#separation-of-concerns "Permalink") Vuex 由状态,获取器,变异,动作和模块组成. 当用户点击一个动作时,我们需要`dispatch`它. 此操作将`commit`将更改状态的突变. *注意:*动作本身不会更新状态,只有突变可以更新状态. ## File structure[](#file-structure "Permalink") 在 GitLab 上使用 Vuex 时,请将这些问题分为不同的文件以提高可读性: ``` └── store ├── index.js # where we assemble modules and export the store ├── actions.js # actions ├── mutations.js # mutations ├── getters.js # getters ├── state.js # state └── mutation_types.js # mutation types ``` 下例显示了一个列出用户并将其添加到状态的应用程序. (有关更复杂的示例实现,请查看[此处](https://gitlab.com/gitlab-org/gitlab/tree/master/ee/app/assets/javascripts/vue_shared/security_reports/store)的安全应用程序商店) ### `index.js`[](#indexjs "Permalink") 这是我们商店的入口点. 您可以使用以下内容作为指导: ``` import Vuex from 'vuex'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import state from './state'; export const createStore = () => new Vuex.Store({ actions, getters, mutations, state, }); ``` *注意:*在实施此[RFC](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20)之前,以上内容将需要禁用`import/prefer-default-export` ESLint 规则. ### `state.js`[](#statejs "Permalink") 在编写任何代码之前,您应该做的第一件事就是设计状态. 通常,我们需要将数据从 haml 提供给 Vue 应用程序. 让我们将其存储在状态中以便更好地访问. ``` export default () => ({ endpoint: null, isLoading: false, error: null, isAddingUser: false, errorAddingUser: false, users: [], }); ``` #### Access `state` properties[](#access-state-properties "Permalink") 您可以使用`mapState`访问组件中的状态属性. ### `actions.js`[](#actionsjs "Permalink") An action is a payload of information to send data from our application to our store. 动作通常由`type`和`payload` ,它们描述发生了什么. 与[变种](#mutationsjs)不同,动作可以包含异步操作-这就是为什么我们始终需要在动作中处理异步逻辑. 在此文件中,我们将编写将调用突变的操作以处理用户列表: ``` import * as types from './mutation_types'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; export const fetchUsers = ({ state, dispatch }) => { commit(types.REQUEST_USERS); axios.get(state.endpoint) .then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data)) .catch((error) => { commit(types.RECEIVE_USERS_ERROR, error) createFlash('There was an error') }); } export const addUser = ({ state, dispatch }, user) => { commit(types.REQUEST_ADD_USER); axios.post(state.endpoint, user) .then(({ data }) => commit(types.RECEIVE_ADD_USER_SUCCESS, data)) .catch((error) => commit(types.REQUEST_ADD_USER_ERROR, error)); } ``` #### Dispatching actions[](#dispatching-actions "Permalink") 要从组件调度动作,请使用`mapActions`帮助器: ``` import { mapActions } from 'vuex'; { methods: { ...mapActions([ 'addUser', ]), onClickUser(user) { this.addUser(user); }, }, }; ``` ### `mutations.js`[](#mutationsjs "Permalink") 变异指定应用程序状态如何响应发送到商店的操作而改变. 更改 Vuex 存储中状态的唯一方法应该是通过提交突变. **在编写任何代码之前先考虑状态是一个好主意.** 请记住,动作仅描述发生了某些事情,而没有描述应用程序状态如何变化. **切勿直接从组件提交突变** 相反,您应该创建一个将导致突变的动作. ``` import * as types from './mutation_types'; export default { [types.REQUEST_USERS](state) { state.isLoading = true; }, [types.RECEIVE_USERS_SUCCESS](state, data) { // Do any needed data transformation to the received payload here state.users = data; state.isLoading = false; }, [types.RECEIVE_USERS_ERROR](state, error) { state.isLoading = false; }, [types.REQUEST_ADD_USER](state, user) { state.isAddingUser = true; }, [types.RECEIVE_ADD_USER_SUCCESS](state, user) { state.isAddingUser = false; state.users.push(user); }, [types.REQUEST_ADD_USER_ERROR](state, error) { state.isAddingUser = false; state.errorAddingUser = error; }, }; ``` #### Naming Pattern: `REQUEST` and `RECEIVE` namespaces[](#naming-pattern-request-and-receive-namespaces "Permalink") 发出请求时,我们通常希望向用户显示加载状态. 与其创建一个突变来切换加载状态,不如: 1. 类型为`REQUEST_SOMETHING`的突变,以切换加载状态 2. 类型为`RECEIVE_SOMETHING_SUCCESS`的突变,用于处理成功回调 3. 类型为`RECEIVE_SOMETHING_ERROR`的突变,用于处理错误回调 4. 动作`fetchSomething`发出请求并在提到的情况下提交突变 1. 如果您的应用程序执行的不是`GET`请求,则可以使用以下示例: * `POST` : `createSomething` * `PUT` : `updateSomething` * `DELETE` : `deleteSomething` 结果,我们可以从该组件调度`fetchNamespace`操作,它将负责提交`REQUEST_NAMESPACE` , `RECEIVE_NAMESPACE_SUCCESS`和`RECEIVE_NAMESPACE_ERROR`突变. > 以前,我们是从`fetchNamespace`操作中调度操作,而不是提交突变,所以如果您在代码库的较早部分中找到了不同的模式,请不要感到困惑. 但是,无论何时您编写新的 Vuex 商店,我们都鼓励利用新模式 通过遵循这种模式,我们保证: 1. 所有应用程序都遵循相同的模式,从而使任何人都更容易维护代码 2. 应用程序中的所有数据都遵循相同的生命周期模式 3. 单元测试更容易 #### Updating complex state[](#updating-complex-state "Permalink") 有时,尤其是当状态复杂时,实际上很难遍历该状态以精确更新突变需要更新的内容. 理想情况下, `vuex`状态应尽可能标准化/解耦,但这并非总是如此. 重要的是要记住,当在突变本身中选择`portion of the mutated state`并对其进行突变时,代码更易于阅读和维护. 给定此状态: ``` export default () => ({ items: [ { id: 1, name: 'my_issue', closed: false, }, { id: 2, name: 'another_issue', closed: false, } ] }); ``` 像这样写一个突变可能很诱人: ``` // Bad export default { [types.MARK_AS_CLOSED](state, item) { Object.assign(item, {closed: true}) } } ``` 尽管此方法有效,但它具有以下依赖性: * 正确的选择`item`中的组件/动作. * `item`属性已经在`closed`状态下声明. * 新的`confidential`财产将不会产生反应. * 他指出, `item`是通过引用`items` 这样写的突变更难维护,更容易出错. 我们宁可这样写一个变异: ``` // Good export default { [types.MARK_AS_CLOSED](state, itemId) { const item = state.items.find(i => i.id == itemId); Vue.set(item, 'closed', true) state.items.splice(index, 1, item) } } ``` 这种方法更好,因为: * 它选择并更新突变中的状态,这种状态更易于维护. * 它没有外部依赖性,如果传递了正确的`itemId`则状态将正确更新. * 它没有反应性警告,因为我们生成了一个新`item`以避免耦合到初始状态. 这样写的变异更容易维护. 另外,我们避免了由于反应系统的限制而导致的错误. ### `getters.js`[](#gettersjs "Permalink") 有时我们可能需要根据存储状态获取派生状态,例如针对特定道具进行过滤. 使用 getter 还将由于依赖关系而缓存结果,这取决于[计算的 props 的工作方式.](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods)这可以通过`getters`来完成: ``` // get all the users with pets export const getUsersWithPets = (state, getters) => { return state.users.filter(user => user.pet !== undefined); }; ``` 要从组件访问吸气剂,请使用`mapGetters`帮助器: ``` import { mapGetters } from 'vuex'; { computed: { ...mapGetters([ 'getUsersWithPets', ]), }, }; ``` ### `mutation_types.js`[](#mutation_typesjs "Permalink") 来自[vuex 突变文档](https://vuex.vuejs.org/guide/mutations.html) :>在各种 Flux 实现中,将常数用于突变类型是一种常见的模式. 这使代码可以利用像 linters 这样的工具,并将所有常量放在一个文件中,使您的协作者可以快速了解整个应用程序中可能发生的变异. ``` export const ADD_USER = 'ADD_USER'; ``` ### Initializing a store’s state[](#initializing-a-stores-state "Permalink") Vuex 存储通常需要一些初始状态才能使用其`action` . 通常,这些数据包括 API 端点,文档 URL 或 ID 之类的数据. 要设置此初始状态,请在安装 Vue 组件时将其作为参数传递给商店的创建函数: ``` // in the Vue app's initialization script (e.g. mount_show.js) import Vue from 'vue'; import Vuex from 'vuex'; import { createStore } from './stores'; import AwesomeVueApp from './components/awesome_vue_app.vue' Vue.use(Vuex); export default () => { const el = document.getElementById('js-awesome-vue-app'); return new Vue({ el, store: createStore(el.dataset), render: h => h(AwesomeVueApp) }); }; ``` 然后,存储功能可以将此数据传递给州的创建功能: ``` // in store/index.js import * as actions from './actions'; import mutations from './mutations'; import createState from './state'; export default initialState => ({ actions, mutations, state: createState(initialState), }); ``` 状态函数可以接受此初始数据作为参数并将其烘焙到返回的`state`对象中: ``` // in store/state.js export default ({ projectId, documentationPath, anOptionalProperty = true }) => ({ projectId, documentationPath, anOptionalProperty, // other state properties here }); ``` #### Why not just …spread the initial state?[](#why-not-just-spread-the-initial-state "Permalink") 精明的读者将从上面的示例中看到切出几行代码的机会: ``` // Don't do this! export default initialState => ({ ...initialState, // other state properties here }); ``` 我们已经做出有意识的决定,避免使用这种模式,以帮助我们的前端代码库实现可发现性和可搜索性. 在[此讨论中](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/56#note_302514865)描述了[这样做的原因](https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/56#note_302514865) : > 考虑在存储状态中使用了`someStateKey` . 如果仅由`el.dataset`提供,则*可能*无法直接对其进行 grep. 相反,您必须 grep 以获得`some_state_key` ,因为它可能来自 rails 模板. 反之亦然:如果您正在查看 Rails 模板,您可能想知道是什么使用了`some_state_key` ,但是您*必须* grep 为`someStateKey` ### Communicating with the Store[](#communicating-with-the-store "Permalink") ``` <script> import { mapActions, mapState, mapGetters } from 'vuex'; export default { computed: { ...mapGetters([ 'getUsersWithPets' ]), ...mapState([ 'isLoading', 'users', 'error', ]), }, methods: { ...mapActions([ 'fetchUsers', 'addUser', ]), onClickAddUser(data) { this.addUser(data); } }, created() { this.fetchUsers() } } </script> <template> <ul> <li v-if="isLoading"> Loading... </li> <li v-else-if="error"> {{ error }} </li> <template v-else> <li v-for="user in users" :key="user.id" > {{ user }} </li> </template> </ul> </template> ``` ### Vuex Gotchas[](#vuex-gotchas "Permalink") 1. 不要直接调用突变. 始终使用动作进行突变. 这样做将在整个应用程序中保持一致性. 从 Vuex 文档: > Why don’t we just call store.commit(‘action’) directly? Well, remember that mutations must be synchronous? Actions aren’t. We can perform asynchronous operations inside an action. ``` // component.vue // bad created() { this.$store.commit('mutation'); } // good created() { this.$store.dispatch('action'); } ``` 2. 使用变异类型而不是对字符串进行硬编码. 这将减少出错的可能性. 3. 在实例化商店的用途之后的所有组件中,都可以访问 State. ### Testing Vuex[](#testing-vuex "Permalink") #### Testing Vuex concerns[](#testing-vuex-concerns "Permalink") 有关测试操作,获取器和突变的信息,请参考[vuex 文档](https://vuex.vuejs.org/guide/testing.html) . #### Testing components that need a store[](#testing-components-that-need-a-store "Permalink") 较小的组件可能会使用`store`属性来访问数据. 为了编写这些组件的单元测试,我们需要包括商店并提供正确的状态: ``` //component_spec.js import Vue from 'vue'; import Vuex from 'vuex'; import { mount, createLocalVue } from '@vue/test-utils'; import { createStore } from './store'; import Component from './component.vue' const localVue = createLocalVue(); localVue.use(Vuex); describe('component', () => { let store; let wrapper; const createComponent = () => { store = createStore(); wrapper = mount(Component, { localVue, store, }); }; beforeEach(() => { createComponent(); }); afterEach(() => { wrapper.destroy(); wrapper = null; }); it('should show a user', async () => { const user = { name: 'Foo', age: '30', }; // populate the store await store.dispatch('addUser', user); expect(wrapper.text()).toContain(user.name); }); }); ``` ### Two way data binding[](#two-way-data-binding "Permalink") 在 Vuex 中存储表单数据时,有时需要更新存储的值. 绝对不应直接更改存储,而应使用操作. 为了在我们的代码中仍然使用`v-model` ,我们需要以这种形式创建计算属性: ``` export default { computed: { someValue: { get() { return this.$store.state.someValue; }, set(value) { this.$store.dispatch("setSomeValue", value); } } } }; ``` 另一种方法是使用`mapState`和`mapActions` : ``` export default { computed: { ...mapState(['someValue']), localSomeValue: { get() { return this.someValue; }, set(value) { this.setSomeValue(value) } } }, methods: { ...mapActions(['setSomeValue']) } }; ``` 添加其中一些属性变得很麻烦,并使代码重复性更高,并需要编写更多的测试. 为了简化此操作, `~/vuex_shared/bindings.js`有一个帮助器. 可以像这样使用助手: ``` // this store is non-functional and only used to give context to the example export default { state: { baz: '', bar: '', foo: '' }, actions: { updateBar() {...} updateAll() {...} }, getters: { getFoo() {...} } } ``` ``` import { mapComputed } from '~/vuex_shared/bindings' export default { computed: { /** * @param {(string[]|Object[])} list - list of string matching state keys or list objects * @param {string} list[].key - the key matching the key present in the vuex state * @param {string} list[].getter - the name of the getter, leave it empty to not use a getter * @param {string} list[].updateFn - the name of the action, leave it empty to use the default action * @param {string} defaultUpdateFn - the default function to dispatch * @param {string} root - optional key of the state where to search fo they keys described in list * @returns {Object} a dictionary with all the computed properties generated */ ...mapComputed( [ 'baz', { key: 'bar', updateFn: 'updateBar' } { key: 'foo', getter: 'getFoo' }, ], 'updateAll', ), } } ``` 然后, `mapComputed`将生成适当的计算属性,这些属性从存储中获取数据并在更新时调度正确的操作.