Help us learn about your current experience with the documentation. Take the survey.

Vuex

已废弃

Vuex 在 GitLab 中已废弃,不应再创建新的 Vuex store。 你仍然可以维护现有的 Vuex store,但我们强烈建议完全迁移出 Vuex

本页包含的其他信息在官方 Vuex 文档中有更详细的说明。

关注点分离

Vuex 由 State、Getters、Mutations、Actions 和 Modules 组成。

当用户选择一个 action 时,我们需要 dispatch 它。这个 action commit 一个 mutation 来改变状态。action 本身不更新状态;只有 mutation 应该更新状态。

文件结构

在 GitLab 中使用 Vuex 时,将这些关注点分离到不同的文件中以提高可读性:

└── store
  ├── index.js          # 我们在这里组装模块并导出 store
  ├── actions.js        # actions
  ├── mutations.js      # mutations
  ├── getters.js        # getters
  ├── state.js          # state
  └── mutation_types.js # mutation types

下面的示例展示了一个列出用户并将用户添加到状态的应用程序。(查看存储在此 repository 中的安全应用程序,以获得更复杂的示例实现。)

index.js

这是我们的 store 入口点。你可以使用以下内容作为指南:

// eslint-disable-next-line no-restricted-imports
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,
  });

state.js

在编写任何代码之前,你应该首先设计状态。

我们经常需要从 HAML 向 Vue 应用程序提供数据。让我们将其存储在状态中以方便访问。

  export default () => ({
    endpoint: null,

    isLoading: false,
    error: null,

    isAddingUser: false,
    errorAddingUser: false,

    users: [],
  });

访问 state 属性

你可以在组件中使用 mapState 来访问 state 属性。

actions.js

action 是一个信息负载,用于将数据从我们的应用程序发送到 store。

action 通常由 typepayload 组成,它们描述了发生了什么。与 mutations 不同,action 可以包含异步操作 - 这就是为什么我们总是在 action 中处理异步逻辑的原因。

在这个文件中,我们编写调用 mutations 来处理用户列表的 actions:

  import * as types from './mutation_types';
  import axios from '~/lib/utils/axios_utils';
  import { createAlert } from '~/alert';

  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)
        createAlert({ message: '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));
  }

分发 actions

要从组件分发 action,使用 mapActions 辅助函数:

import { mapActions } from 'vuex';

{
  methods: {
    ...mapActions([
      'addUser',
    ]),
    onClickUser(user) {
      this.addUser(user);
    },
  },
};

mutations.js

mutations 指定了应用程序状态如何响应发送到 store 的 actions 而改变。 在 Vuex store 中改变状态的唯一方法是 commit 一个 mutation。

大多数 mutation 是通过 commit 从 action 中提交的。如果你没有任何异步操作,你可以使用 mapMutations 辅助函数从组件调用 mutations。

有关从组件 committing mutations from components 的示例,请参阅 Vuex 文档。

命名模式:REQUESTRECEIVE 命名空间

当我们发起请求时,我们经常想向用户显示加载状态。

与其创建一个 mutation 来切换加载状态,我们应该:

  1. 一个类型为 REQUEST_SOMETHING 的 mutation,用于切换加载状态
  2. 一个类型为 RECEIVE_SOMETHING_SUCCESS 的 mutation,用于处理成功回调
  3. 一个类型为 RECEIVE_SOMETHING_ERROR 的 mutation,用于处理错误回调
  4. 一个 fetchSomething action 来发起请求并在上述情况下 commit mutations
    1. 如果你的应用程序执行了不止一个 GET 请求,你可以使用这些示例:
      • POST: createSomething
      • PUT: updateSomething
      • DELETE: deleteSomething

结果,我们可以从组件分发 fetchNamespace action,它负责 commit REQUEST_NAMESPACERECEIVE_NAMESPACE_SUCCESSRECEIVE_NAMESPACE_ERROR mutations。

以前,我们从 fetchNamespace action 中分发 actions 而不是 commit mutation,所以如果你在代码库的旧部分发现不同的模式,不要感到困惑。然而,我们鼓励在编写新的 Vuex store 时使用新模式。

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

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

更新复杂状态

有时,特别是当状态复杂时,遍历状态以精确更新 mutation 需要更新的内容确实很困难。 理想情况下,vuex 状态应该尽可能规范化/解耦,但情况并非总是如此。

重要的是要记住,当在 mutation 本身中选择和变异 被变异状态的部分 时,代码更容易阅读和维护。

给定以下状态:

   export default () => ({
    items: [
      {
        id: 1,
        name: 'my_issue',
        closed: false,
      },
      {
        id: 2,
        name: 'another_issue',
        closed: false,
      }
    ]
});

可能会诱使你编写这样的 mutation:

// 不好的做法
export default {
  [types.MARK_AS_CLOSED](state, item) {
    Object.assign(item, {closed: true})
  }
}

虽然这种方法有效,但它有几个依赖项:

  • 在组件/action 中正确选择 item
  • item 属性已经在 closed 状态中声明。
    • 新的 confidential 属性不会是响应式的。
  • 注意到 itemitems 引用。

这样编写的 mutation 更难维护且更容易出错。我们应该编写这样的 mutation:

// 好的做法
export default {
  [types.MARK_AS_CLOSED](state, itemId) {
    const item = state.items.find(x => x.id === itemId);

    if (!item) {
      return;
    }

    Vue.set(item, 'closed', true);
  },
};

这种方法更好,因为:

  • 它在 mutation 中选择和更新状态,更具可维护性。
  • 它没有外部依赖,如果传递正确的 itemId,状态会正确更新。
  • 它没有响应式陷阱,因为我们生成一个新的 item 来避免与初始状态耦合。

这样编写的 mutation 更容易维护。此外,我们避免了由于响应式系统限制而导致的错误。

getters.js

有时我们可能需要基于 store 状态获取派生状态,比如过滤特定属性。 使用 getter 也会根据依赖项缓存结果,这得益于 计算属性的工作原理 这可以通过 getters 来实现:

// 获取所有有宠物的用户
export const getUsersWithPets = (state, getters) => {
  return state.users.filter(user => user.pet !== undefined);
};

要从组件访问 getter,使用 mapGetters 辅助函数:

import { mapGetters } from 'vuex';

{
  computed: {
    ...mapGetters([
      'getUsersWithPets',
    ]),
  },
};

mutation_types.js

Vuex mutations 文档

在各种 Flux 实现中,使用常量作为 mutation 类型是一种常见的模式。 这使代码能够利用 linters 等工具,并且将所有常量放在一个文件中 可以让你的合作者一目了然地了解整个应用程序中可能的 mutations。

export const ADD_USER = 'ADD_USER';

初始化 store 的状态

Vuex store 通常需要在其 action 可以使用之前有一些初始状态。这通常包括 API 端点、文档 URL 或 ID 等数据。

要设置这个初始状态,在挂载 Vue 组件时将其作为参数传递给 store 的创建函数:

// 在 Vue 应用程序的初始化脚本中(例如,mount_show.js)

import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
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,
    name: 'AwesomeVueRoot',
    store: createStore(el.dataset),
    render: h => h(AwesomeVueApp)
  });
};

反过来,store 函数可以将这些数据传递给状态的创建函数:

// 在 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 对象中:

// 在 store/state.js 中

export default ({
  projectId,
  documentationPath,
  anOptionalProperty = true
}) => ({
  projectId,
  documentationPath,
  anOptionalProperty,

  // 其他状态属性在这里
});

为什么不直接 …展开初始状态?

细心的读者看到可以减少上面示例中几行代码的机会:

// 不要这样做!

export default initialState => ({
  ...initialState,

  // 其他状态属性在这里
});

我们做出了有意识的决定来避免这种模式,以提高发现和搜索我们前端代码库的能力。当向 Vue 应用程序提供数据时也是如此。这样做的理由在this discussion中有描述:

考虑 someStateKey 正在用于 store 状态中。如果它仅由 el.dataset 提供,你可能 无法直接 grep 到它。相反,你必须 grep some_state_key,因为它可能来自 Rails 模板。反过来也是如此:如果你在看一个 rails 模板,你可能想知道 什么使用了 some_state_key,但你必须 grep someStateKey

与 Store 通信

<script>
// eslint-disable-next-line no-restricted-imports
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

测试 Vuex 关注点

有关测试 Actions、Getters 和 Mutations,请参考 Vuex 文档

测试需要 store 的组件

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

//component_spec.js
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { createStore } from './store';
import Component from './component.vue'

Vue.use(Vuex);

describe('component', () => {
  let store;
  let wrapper;

  const createComponent = () => {
    store = createStore();

    wrapper = mount(Component, {
      store,
    });
  };

  beforeEach(() => {
    createComponent();
  });

  it('should show a user', async () => {
    const user = {
      name: 'Foo',
      age: '30',
    };

    // 填充 store
    await store.dispatch('addUser', user);

    expect(wrapper.text()).toContain(user.name);
  });
});

一些测试文件可能仍在使用来自 @vue/test-utilsdeprecated createLocalVue functionlocalVue.use(Vuex)。这是不必要的,应该避免或在可能时移除。

双向数据绑定

当在 Vuex 中存储表单数据时,有时需要更新存储的值。store 绝不应该被直接变异,而应该使用 action。 要在我们的代码中使用 v-model,我们需要创建如下形式的计算属性:

export default {
  computed: {
    someValue: {
      get() {
        return this.$store.state.someValue;
      },
      set(value) {
        this.$store.dispatch("setSomeValue", value);
      }
    }
  }
};

另一种方法是使用 mapStatemapActions

export default {
  computed: {
    ...mapState(['someValue']),
    localSomeValue: {
      get() {
        return this.someValue;
      },
      set(value) {
        this.setSomeValue(value)
      }
    }
  },
  methods: {
    ...mapActions(['setSomeValue'])
  }
};

添加几个这样的属性变得很麻烦,并且使代码更加重复,需要编写更多测试。为了简化这一点,~/vuex_shared/bindings.js 中有一个辅助函数。

辅助函数可以这样使用:

// 这个 store 是非功能性的,仅用于为示例提供上下文
export default {
  state: {
    baz: '',
    bar: '',
    foo: ''
  },
  actions: {
    updateBar() {...},
    updateAll() {...},
  },
  getters: {
    getFoo() {...},
  }
}
import { mapComputed } from '~/vuex_shared/bindings'
export default {
  computed: {
    /**
     * @param {(string[]|Object[])} list - 匹配 state 键的字符串列表或对象列表
     * @param {string} list[].key - 与 vuex state 中存在的键匹配的键
     * @param {string} list[].getter - getter 的名称,留空则不使用 getter
     * @param {string} list[].updateFn - action 的名称,留空则使用默认 action
     * @param {string} defaultUpdateFn - 要分发的默认函数
     * @param {string|function} root - 可选的 state 键,用于在 list 中描述的键
     * @returns {Object} 包含所有生成的计算属性的字典
    */
    ...mapComputed(
      [
        'baz',
        { key: 'bar', updateFn: 'updateBar' },
        { key: 'foo', getter: 'getFoo' },
      ],
      'updateAll',
    ),
  }
}

mapComputed 然后生成适当的计算属性,这些属性从 store 获取数据并在更新时分发正确的 action。

如果键的 root 超过一级深度,你可以使用函数来获取相关的 state 对象。

例如,使用这样的 store:

// 这个 store 是非功能性的,仅用于为示例提供上下文
export default {
  state: {
    foo: {
      qux: {
        baz: '',
        bar: '',
        foo: '',
      },
    },
  },
  actions: {
    updateBar() {...},
    updateAll() {...},
  },
  getters: {
    getFoo() {...},
  }
}

root 可以是:

import { mapComputed } from '~/vuex_shared/bindings'
export default {
  computed: {
    ...mapComputed(
      [
        'baz',
        { key: 'bar', updateFn: 'updateBar' },
        { key: 'foo', getter: 'getFoo' },
      ],
      'updateAll',
      (state) => state.foo.qux,
    ),
  }
}