Vuex

原文:https://docs.gitlab.com/ee/development/fe_guide/vuex.html

Vuex

如果将状态管理与组件分离有明显的好处(例如,由于状态复杂性),我们建议使用Vuex 而不是其他任何 Flux 模式. 否则,请随时管理组件中的状态.

在以下情况下,应强烈考虑 Vuex:

  • 您期望应用程序的多个部分对状态变化做出反应
  • 需要在多个组件之间共享数据
  • 与后端的交互非常复杂,例如多个 API 调用
  • 该应用程序涉及通过传统 REST API 和 GraphQL 与后端进行交互(尤其是将 REST API 移至 GraphQL 时,这是一项待处理的后端任务)

注意:以下所有内容在Vuex官方文档中有更详细的说明 .

Separation of concerns

Vuex 由状态,获取器,变异,动作和模块组成.

当用户点击一个动作时,我们需要dispatch它. 此操作将commit将更改状态的突变. 注意:动作本身不会更新状态,只有突变可以更新状态.

File structure

在 GitLab 上使用 Vuex 时,请将这些问题分为不同的文件以提高可读性:

  1. └── store
  2. ├── index.js # where we assemble modules and export the store
  3. ├── actions.js # actions
  4. ├── mutations.js # mutations
  5. ├── getters.js # getters
  6. ├── state.js # state
  7. └── mutation_types.js # mutation types

下例显示了一个列出用户并将其添加到状态的应用程序. (有关更复杂的示例实现,请查看此处的安全应用程序商店)

index.js

这是我们商店的入口点. 您可以使用以下内容作为指导:

  1. import Vuex from 'vuex';
  2. import * as actions from './actions';
  3. import * as getters from './getters';
  4. import mutations from './mutations';
  5. import state from './state';
  6. export const createStore = () =>
  7. new Vuex.Store({
  8. actions,
  9. getters,
  10. mutations,
  11. state,
  12. });

注意:在实施此RFC之前,以上内容将需要禁用import/prefer-default-export ESLint 规则.

state.js

在编写任何代码之前,您应该做的第一件事就是设计状态.

通常,我们需要将数据从 haml 提供给 Vue 应用程序. 让我们将其存储在状态中以便更好地访问.

  1. export default () => ({
  2. endpoint: null,
  3. isLoading: false,
  4. error: null,
  5. isAddingUser: false,
  6. errorAddingUser: false,
  7. users: [],
  8. });

Access state properties

您可以使用mapState访问组件中的状态属性.

actions.js

An action is a payload of information to send data from our application to our store.

动作通常由typepayload ,它们描述发生了什么. 与变种不同,动作可以包含异步操作-这就是为什么我们始终需要在动作中处理异步逻辑.

在此文件中,我们将编写将调用突变的操作以处理用户列表:

  1. import * as types from './mutation_types';
  2. import axios from '~/lib/utils/axios_utils';
  3. import createFlash from '~/flash';
  4. export const fetchUsers = ({ state, dispatch }) => {
  5. commit(types.REQUEST_USERS);
  6. axios.get(state.endpoint)
  7. .then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data))
  8. .catch((error) => {
  9. commit(types.RECEIVE_USERS_ERROR, error)
  10. createFlash('There was an error')
  11. });
  12. }
  13. export const addUser = ({ state, dispatch }, user) => {
  14. commit(types.REQUEST_ADD_USER);
  15. axios.post(state.endpoint, user)
  16. .then(({ data }) => commit(types.RECEIVE_ADD_USER_SUCCESS, data))
  17. .catch((error) => commit(types.REQUEST_ADD_USER_ERROR, error));
  18. }

Dispatching actions

要从组件调度动作,请使用mapActions帮助器:

  1. import { mapActions } from 'vuex';
  2. {
  3. methods: {
  4. ...mapActions([
  5. 'addUser',
  6. ]),
  7. onClickUser(user) {
  8. this.addUser(user);
  9. },
  10. },
  11. };

mutations.js

变异指定应用程序状态如何响应发送到商店的操作而改变. 更改 Vuex 存储中状态的唯一方法应该是通过提交突变.

在编写任何代码之前先考虑状态是一个好主意.

请记住,动作仅描述发生了某些事情,而没有描述应用程序状态如何变化.

切勿直接从组件提交突变

相反,您应该创建一个将导致突变的动作.

  1. import * as types from './mutation_types';
  2. export default {
  3. [types.REQUEST_USERS](state) {
  4. state.isLoading = true;
  5. },
  6. [types.RECEIVE_USERS_SUCCESS](state, data) {
  7. // Do any needed data transformation to the received payload here
  8. state.users = data;
  9. state.isLoading = false;
  10. },
  11. [types.RECEIVE_USERS_ERROR](state, error) {
  12. state.isLoading = false;
  13. },
  14. [types.REQUEST_ADD_USER](state, user) {
  15. state.isAddingUser = true;
  16. },
  17. [types.RECEIVE_ADD_USER_SUCCESS](state, user) {
  18. state.isAddingUser = false;
  19. state.users.push(user);
  20. },
  21. [types.REQUEST_ADD_USER_ERROR](state, error) {
  22. state.isAddingUser = false;
  23. state.errorAddingUser = error;
  24. },
  25. };

Naming Pattern: REQUEST and RECEIVE namespaces

发出请求时,我们通常希望向用户显示加载状态.

与其创建一个突变来切换加载状态,不如:

  1. 类型为REQUEST_SOMETHING的突变,以切换加载状态
  2. 类型为RECEIVE_SOMETHING_SUCCESS的突变,用于处理成功回调
  3. 类型为RECEIVE_SOMETHING_ERROR的突变,用于处理错误回调
  4. 动作fetchSomething发出请求并在提到的情况下提交突变
    1. 如果您的应用程序执行的不是GET请求,则可以使用以下示例:
      • POSTcreateSomething
      • PUTupdateSomething
      • DELETEdeleteSomething

结果,我们可以从该组件调度fetchNamespace操作,它将负责提交REQUEST_NAMESPACERECEIVE_NAMESPACE_SUCCESSRECEIVE_NAMESPACE_ERROR突变.

以前,我们是从fetchNamespace操作中调度操作,而不是提交突变,所以如果您在代码库的较早部分中找到了不同的模式,请不要感到困惑. 但是,无论何时您编写新的 Vuex 商店,我们都鼓励利用新模式

通过遵循这种模式,我们保证:

  1. 所有应用程序都遵循相同的模式,从而使任何人都更容易维护代码
  2. 应用程序中的所有数据都遵循相同的生命周期模式
  3. 单元测试更容易

Updating complex state

有时,尤其是当状态复杂时,实际上很难遍历该状态以精确更新突变需要更新的内容. 理想情况下, vuex状态应尽可能标准化/解耦,但这并非总是如此.

重要的是要记住,当在突变本身中选择portion of the mutated state并对其进行突变时,代码更易于阅读和维护.

给定此状态:

  1. export default () => ({
  2. items: [
  3. {
  4. id: 1,
  5. name: 'my_issue',
  6. closed: false,
  7. },
  8. {
  9. id: 2,
  10. name: 'another_issue',
  11. closed: false,
  12. }
  13. ]
  14. });

像这样写一个突变可能很诱人:

  1. // Bad
  2. export default {
  3. [types.MARK_AS_CLOSED](state, item) {
  4. Object.assign(item, {closed: true})
  5. }
  6. }

尽管此方法有效,但它具有以下依赖性:

  • 正确的选择item中的组件/动作.
  • item属性已经在closed状态下声明.
    • 新的confidential财产将不会产生反应.
  • 他指出, item是通过引用items

这样写的突变更难维护,更容易出错. 我们宁可这样写一个变异:

  1. // Good
  2. export default {
  3. [types.MARK_AS_CLOSED](state, itemId) {
  4. const item = state.items.find(i => i.id == itemId);
  5. Vue.set(item, 'closed', true)
  6. state.items.splice(index, 1, item)
  7. }
  8. }

这种方法更好,因为:

  • 它选择并更新突变中的状态,这种状态更易于维护.
  • 它没有外部依赖性,如果传递了正确的itemId则状态将正确更新.
  • 它没有反应性警告,因为我们生成了一个新item以避免耦合到初始状态.

这样写的变异更容易维护. 另外,我们避免了由于反应系统的限制而导致的错误.

getters.js

有时我们可能需要根据存储状态获取派生状态,例如针对特定道具进行过滤. 使用 getter 还将由于依赖关系而缓存结果,这取决于计算的 props 的工作方式.这可以通过getters来完成:

  1. // get all the users with pets
  2. export const getUsersWithPets = (state, getters) => {
  3. return state.users.filter(user => user.pet !== undefined);
  4. };

要从组件访问吸气剂,请使用mapGetters帮助器:

  1. import { mapGetters } from 'vuex';
  2. {
  3. computed: {
  4. ...mapGetters([
  5. 'getUsersWithPets',
  6. ]),
  7. },
  8. };

mutation_types.js

来自vuex 突变文档 :>在各种 Flux 实现中,将常数用于突变类型是一种常见的模式. 这使代码可以利用像 linters 这样的工具,并将所有常量放在一个文件中,使您的协作者可以快速了解整个应用程序中可能发生的变异.

  1. export const ADD_USER = 'ADD_USER';

Initializing a store’s state

Vuex 存储通常需要一些初始状态才能使用其action . 通常,这些数据包括 API 端点,文档 URL 或 ID 之类的数据.

要设置此初始状态,请在安装 Vue 组件时将其作为参数传递给商店的创建函数:

  1. // in the Vue app's initialization script (e.g. mount_show.js)
  2. import Vue from 'vue';
  3. import Vuex from 'vuex';
  4. import { createStore } from './stores';
  5. import AwesomeVueApp from './components/awesome_vue_app.vue'
  6. Vue.use(Vuex);
  7. export default () => {
  8. const el = document.getElementById('js-awesome-vue-app');
  9. return new Vue({
  10. el,
  11. store: createStore(el.dataset),
  12. render: h => h(AwesomeVueApp)
  13. });
  14. };

然后,存储功能可以将此数据传递给州的创建功能:

  1. // in store/index.js
  2. import * as actions from './actions';
  3. import mutations from './mutations';
  4. import createState from './state';
  5. export default initialState => ({
  6. actions,
  7. mutations,
  8. state: createState(initialState),
  9. });

状态函数可以接受此初始数据作为参数并将其烘焙到返回的state对象中:

  1. // in store/state.js
  2. export default ({
  3. projectId,
  4. documentationPath,
  5. anOptionalProperty = true
  6. }) => ({
  7. projectId,
  8. documentationPath,
  9. anOptionalProperty,
  10. // other state properties here
  11. });

Why not just …spread the initial state?

精明的读者将从上面的示例中看到切出几行代码的机会:

  1. // Don't do this!
  2. export default initialState => ({
  3. ...initialState,
  4. // other state properties here
  5. });

我们已经做出有意识的决定,避免使用这种模式,以帮助我们的前端代码库实现可发现性和可搜索性. 在此讨论中描述了这样做的原因

考虑在存储状态中使用了someStateKey . 如果仅由el.dataset提供,则可能无法直接对其进行 grep. 相反,您必须 grep 以获得some_state_key ,因为它可能来自 rails 模板. 反之亦然:如果您正在查看 Rails 模板,您可能想知道是什么使用了some_state_key ,但是您必须 grep 为someStateKey

Communicating with the Store

  1. <script>
  2. import { mapActions, mapState, mapGetters } from 'vuex';
  3. export default {
  4. computed: {
  5. ...mapGetters([
  6. 'getUsersWithPets'
  7. ]),
  8. ...mapState([
  9. 'isLoading',
  10. 'users',
  11. 'error',
  12. ]),
  13. },
  14. methods: {
  15. ...mapActions([
  16. 'fetchUsers',
  17. 'addUser',
  18. ]),
  19. onClickAddUser(data) {
  20. this.addUser(data);
  21. }
  22. },
  23. created() {
  24. this.fetchUsers()
  25. }
  26. }
  27. </script> <template>
  28. <ul>
  29. <li v-if="isLoading">
  30. Loading...
  31. </li>
  32. <li v-else-if="error">
  33. {{ error }}
  34. </li>
  35. <template v-else>
  36. <li
  37. v-for="user in users"
  38. :key="user.id"
  39. >
  40. {{ user }}
  41. </li>
  42. </template>
  43. </ul> </template>

Vuex Gotchas

  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.

    1. // component.vue
    2. // bad
    3. created() {
    4. this.$store.commit('mutation');
    5. }
    6. // good
    7. created() {
    8. this.$store.dispatch('action');
    9. }
  2. 使用变异类型而不是对字符串进行硬编码. 这将减少出错的可能性.

  3. 在实例化商店的用途之后的所有组件中,都可以访问 State.

Testing Vuex

Testing Vuex concerns

有关测试操作,获取器和突变的信息,请参考vuex 文档 .

Testing components that need a store

较小的组件可能会使用store属性来访问数据. 为了编写这些组件的单元测试,我们需要包括商店并提供正确的状态:

  1. //component_spec.js
  2. import Vue from 'vue';
  3. import Vuex from 'vuex';
  4. import { mount, createLocalVue } from '@vue/test-utils';
  5. import { createStore } from './store';
  6. import Component from './component.vue'
  7. const localVue = createLocalVue();
  8. localVue.use(Vuex);
  9. describe('component', () => {
  10. let store;
  11. let wrapper;
  12. const createComponent = () => {
  13. store = createStore();
  14. wrapper = mount(Component, {
  15. localVue,
  16. store,
  17. });
  18. };
  19. beforeEach(() => {
  20. createComponent();
  21. });
  22. afterEach(() => {
  23. wrapper.destroy();
  24. wrapper = null;
  25. });
  26. it('should show a user', async () => {
  27. const user = {
  28. name: 'Foo',
  29. age: '30',
  30. };
  31. // populate the store
  32. await store.dispatch('addUser', user);
  33. expect(wrapper.text()).toContain(user.name);
  34. });
  35. });

Two way data binding

在 Vuex 中存储表单数据时,有时需要更新存储的值. 绝对不应直接更改存储,而应使用操作. 为了在我们的代码中仍然使用v-model ,我们需要以这种形式创建计算属性:

  1. export default {
  2. computed: {
  3. someValue: {
  4. get() {
  5. return this.$store.state.someValue;
  6. },
  7. set(value) {
  8. this.$store.dispatch("setSomeValue", value);
  9. }
  10. }
  11. }
  12. };

另一种方法是使用mapStatemapActions

  1. export default {
  2. computed: {
  3. ...mapState(['someValue']),
  4. localSomeValue: {
  5. get() {
  6. return this.someValue;
  7. },
  8. set(value) {
  9. this.setSomeValue(value)
  10. }
  11. }
  12. },
  13. methods: {
  14. ...mapActions(['setSomeValue'])
  15. }
  16. };

添加其中一些属性变得很麻烦,并使代码重复性更高,并需要编写更多的测试. 为了简化此操作, ~/vuex_shared/bindings.js有一个帮助器.

可以像这样使用助手:

  1. // this store is non-functional and only used to give context to the example
  2. export default {
  3. state: {
  4. baz: '',
  5. bar: '',
  6. foo: ''
  7. },
  8. actions: {
  9. updateBar() {...}
  10. updateAll() {...}
  11. },
  12. getters: {
  13. getFoo() {...}
  14. }
  15. }
  1. import { mapComputed } from '~/vuex_shared/bindings'
  2. export default {
  3. computed: {
  4. /**
  5. * @param {(string[]|Object[])} list - list of string matching state keys or list objects
  6. * @param {string} list[].key - the key matching the key present in the vuex state
  7. * @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
  8. * @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
  9. * @param {string} defaultUpdateFn - the default function to dispatch
  10. * @param {string} root - optional key of the state where to search fo they keys described in list
  11. * @returns {Object} a dictionary with all the computed properties generated
  12. */
  13. ...mapComputed(
  14. [
  15. 'baz',
  16. { key: 'bar', updateFn: 'updateBar' }
  17. { key: 'foo', getter: 'getFoo' },
  18. ],
  19. 'updateAll',
  20. ),
  21. }
  22. }

然后, mapComputed将生成适当的计算属性,这些属性从存储中获取数据并在更新时调度正确的操作.