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 通常由 type 和 payload 组成,它们描述了发生了什么。与 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 文档。
命名模式:REQUEST 和 RECEIVE 命名空间
当我们发起请求时,我们经常想向用户显示加载状态。
与其创建一个 mutation 来切换加载状态,我们应该:
- 一个类型为
REQUEST_SOMETHING的 mutation,用于切换加载状态 - 一个类型为
RECEIVE_SOMETHING_SUCCESS的 mutation,用于处理成功回调 - 一个类型为
RECEIVE_SOMETHING_ERROR的 mutation,用于处理错误回调 - 一个
fetchSomethingaction 来发起请求并在上述情况下 commit mutations- 如果你的应用程序执行了不止一个
GET请求,你可以使用这些示例:POST:createSomethingPUT:updateSomethingDELETE:deleteSomething
- 如果你的应用程序执行了不止一个
结果,我们可以从组件分发 fetchNamespace action,它负责 commit REQUEST_NAMESPACE、RECEIVE_NAMESPACE_SUCCESS 和 RECEIVE_NAMESPACE_ERROR mutations。
以前,我们从
fetchNamespaceaction 中分发 actions 而不是 commit mutation,所以如果你在代码库的旧部分发现不同的模式,不要感到困惑。然而,我们鼓励在编写新的 Vuex store 时使用新模式。
通过遵循这个模式,我们保证:
- 所有应用程序都遵循相同的模式,使任何人更容易维护代码。
- 应用程序中的所有数据都遵循相同的生命周期模式。
- 单元测试更容易编写。
更新复杂状态
有时,特别是当状态复杂时,遍历状态以精确更新 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属性不会是响应式的。
- 新的
- 注意到
item被items引用。
这样编写的 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
在各种 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 到它。相反,你必须 grepsome_state_key,因为它可能来自 Rails 模板。反过来也是如此:如果你在看一个 rails 模板,你可能想知道 什么使用了some_state_key,但你必须 grepsomeStateKey。
与 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-utils 的
deprecated createLocalVue function
和 localVue.use(Vuex)。这是不必要的,应该避免或在可能时移除。
双向数据绑定
当在 Vuex 中存储表单数据时,有时需要更新存储的值。store
绝不应该被直接变异,而应该使用 action。
要在我们的代码中使用 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 中有一个辅助函数。
辅助函数可以这样使用:
// 这个 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,
),
}
}