Vuex
原文:https://docs.gitlab.com/ee/development/fe_guide/vuex.html
- Separation of concerns
- File structure
Vuex
如果将状态管理与组件分离有明显的好处(例如,由于状态复杂性),我们建议使用Vuex 而不是其他任何 Flux 模式. 否则,请随时管理组件中的状态.
在以下情况下,应强烈考虑 Vuex:
- 您期望应用程序的多个部分对状态变化做出反应
- 需要在多个组件之间共享数据
- 与后端的交互非常复杂,例如多个 API 调用
- 该应用程序涉及通过传统 REST API 和 GraphQL 与后端进行交互(尤其是将 REST API 移至 GraphQL 时,这是一项待处理的后端任务)
Separation of concerns
Vuex 由状态,获取器,变异,动作和模块组成.
当用户点击一个动作时,我们需要dispatch
它. 此操作将commit
将更改状态的突变. 注意:动作本身不会更新状态,只有突变可以更新状态.
File structure
在 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
下例显示了一个列出用户并将其添加到状态的应用程序. (有关更复杂的示例实现,请查看此处的安全应用程序商店)
index.js
这是我们商店的入口点. 您可以使用以下内容作为指导:
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之前,以上内容将需要禁用import/prefer-default-export
ESLint 规则.
state.js
在编写任何代码之前,您应该做的第一件事就是设计状态.
通常,我们需要将数据从 haml 提供给 Vue 应用程序. 让我们将其存储在状态中以便更好地访问.
export default () => ({
endpoint: null,
isLoading: false,
error: null,
isAddingUser: false,
errorAddingUser: false,
users: [],
});
Access state
properties
您可以使用mapState
访问组件中的状态属性.
actions.js
An action is a payload of information to send data from our application to our store.
动作通常由type
和payload
,它们描述发生了什么. 与变种不同,动作可以包含异步操作-这就是为什么我们始终需要在动作中处理异步逻辑.
在此文件中,我们将编写将调用突变的操作以处理用户列表:
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
要从组件调度动作,请使用mapActions
帮助器:
import { mapActions } from 'vuex';
{
methods: {
...mapActions([
'addUser',
]),
onClickUser(user) {
this.addUser(user);
},
},
};
mutations.js
变异指定应用程序状态如何响应发送到商店的操作而改变. 更改 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
发出请求时,我们通常希望向用户显示加载状态.
与其创建一个突变来切换加载状态,不如:
- 类型为
REQUEST_SOMETHING
的突变,以切换加载状态 - 类型为
RECEIVE_SOMETHING_SUCCESS
的突变,用于处理成功回调 - 类型为
RECEIVE_SOMETHING_ERROR
的突变,用于处理错误回调 - 动作
fetchSomething
发出请求并在提到的情况下提交突变- 如果您的应用程序执行的不是
GET
请求,则可以使用以下示例:POST
:createSomething
PUT
:updateSomething
DELETE
:deleteSomething
- 如果您的应用程序执行的不是
结果,我们可以从该组件调度fetchNamespace
操作,它将负责提交REQUEST_NAMESPACE
, RECEIVE_NAMESPACE_SUCCESS
和RECEIVE_NAMESPACE_ERROR
突变.
以前,我们是从
fetchNamespace
操作中调度操作,而不是提交突变,所以如果您在代码库的较早部分中找到了不同的模式,请不要感到困惑. 但是,无论何时您编写新的 Vuex 商店,我们都鼓励利用新模式
通过遵循这种模式,我们保证:
- 所有应用程序都遵循相同的模式,从而使任何人都更容易维护代码
- 应用程序中的所有数据都遵循相同的生命周期模式
- 单元测试更容易
Updating complex state
有时,尤其是当状态复杂时,实际上很难遍历该状态以精确更新突变需要更新的内容. 理想情况下, 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
有时我们可能需要根据存储状态获取派生状态,例如针对特定道具进行过滤. 使用 getter 还将由于依赖关系而缓存结果,这取决于计算的 props 的工作方式.这可以通过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
来自vuex 突变文档 :>在各种 Flux 实现中,将常数用于突变类型是一种常见的模式. 这使代码可以利用像 linters 这样的工具,并将所有常量放在一个文件中,使您的协作者可以快速了解整个应用程序中可能发生的变异.
export const ADD_USER = 'ADD_USER';
Initializing a store’s state
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?
精明的读者将从上面的示例中看到切出几行代码的机会:
// Don't do this!
export default initialState => ({
...initialState,
// other state properties here
});
我们已经做出有意识的决定,避免使用这种模式,以帮助我们的前端代码库实现可发现性和可搜索性. 在此讨论中描述了这样做的原因 :
考虑在存储状态中使用了
someStateKey
. 如果仅由el.dataset
提供,则可能无法直接对其进行 grep. 相反,您必须 grep 以获得some_state_key
,因为它可能来自 rails 模板. 反之亦然:如果您正在查看 Rails 模板,您可能想知道是什么使用了some_state_key
,但是您必须 grep 为someStateKey
Communicating with the Store
<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 文档:
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');
}
使用变异类型而不是对字符串进行硬编码. 这将减少出错的可能性.
- 在实例化商店的用途之后的所有组件中,都可以访问 State.
Testing Vuex
Testing Vuex concerns
有关测试操作,获取器和突变的信息,请参考vuex 文档 .
Testing components that need a store
较小的组件可能会使用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
在 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
将生成适当的计算属性,这些属性从存储中获取数据并在更新时调度正确的操作.