# 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`将生成适当的计算属性，这些属性从存储中获取数据并在更新时调度正确的操作.