1.0.1 • Published 4 years ago

vuex-types v1.0.1

Weekly downloads
1
License
MIT
Repository
-
Last release
4 years ago

VuexTypes 使用文档

VuexTypes 是什么?

VuexTypes 是一个基于 Vuex 二次开发的 Vue 插件。它的主要作用是对 Vuex 中带命名空间的模块的绑定辅助函数(mapStatemapGettersmapMutationsmapActions)进行改造,简化 多模块场景下 原生绑定辅助函数的写法。VuexTypes 适用于多模块场景下的状态管理

我们先来看一下 Vuex 官方提供的关于 带名命空间的绑定函数 的代码片段:

computed: {
    ...mapState({
        a: state => state.some.nested.module.a,
        b: state => state.some.nested.module.b
    })
},
methods: {
    ...mapActions([
        'some/nested/module/foo', // -> this['some/nested/module/foo']()
        'some/nested/module/bar' // -> this['some/nested/module/bar']()
    ])
}

为了简化上述写法,官方提供了两种形式来绑定命名空间:

  1. 通过将模块的空间名称字符串作为第一个参数传递给辅助函数,会自动绑定到对应命名空间
computed: {
    ...mapState('some/nested/module', {
        a: state => state.a,
        b: state => state.b
    })
},
methods: {
    ...mapActions('some/nested/module', [
        'foo', // -> this.foo()
        'bar' // -> this.bar()
    ])
}
  1. 通过 createNamespacedHelpers 创建基于某个命名空间辅助函数
import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
    computed: {
        // 在 `some/nested/module` 中查找
        ...mapState({
            a: state => state.a,
            b: state => state.b
        })
    },
    methods: {
        // 在 `some/nested/module` 中查找
        ...mapActions([
            'foo',
            'bar'
        ])
    }
}

这两种方式在单模块场景下有了一定的简化,但是如果一个组件使用到了多个模块下的状态管理,那就会有繁琐的重复代码:

// 方式一:
computed: {
    ...mapState('some/nested/moduleA', {
        a: state => state.a,
        b: state => state.b
    }),
    ...mapState('some/nested/moduleB', {
        a: state => state.a,
        b: state => state.b
    })
}

// 方式二:
import { createNamespacedHelpers } from 'vuex'

const { mapState: mapStateA, mapActions: mapActionsA } = createNamespacedHelpers('some/nested/moduleA')
const { mapState: mapStateB, mapActions: mapActionsB } = createNamespacedHelpers('some/nested/moduleB')

export default {
    computed: {
        ...mapStateA({
            a: state => state.a,
            b: state => state.b
        }),
        ...mapStateB({
            a: state => state.a,
            b: state => state.b
        })
    },
    methods: {
        ...mapActionsA(['foo', 'bar']),
        ...mapActionsB(['foo', 'bar']),
    }
}

VuexTypes 主要就是为了简化多模块场景下的状态管理写法,不需要引入 辅助函数 或者 createNamespacedHelpers,直接在组件中定义模块状态就可以:

export default {
    _mappedState: {
        moduleA: {
            a: state => state.a,
            b: state => state.b
        },
        moduleB: {
            a: state => state.a,
            b: state => state.b
        }
    },
    _mappedActions: {
        moduleA: () => ['foo', 'bar'],
        moduleB: () => ['foo', 'bar']
    }
}

此外,在日常开发中,对接口加载数据的处理也是很常见的:为接口数据创建 state 状态,然后定义相应的 mutation 去触发状态变更,同时还会处理接口加载的状态(加载中、加载成功、加载失败)。这些操作 VuexTypes 统统帮你处理了,只需要进行一些配置。话不多说,我们来看下如何使用 VuexTypes 吧。

开始

VuexTypes 接收一些自定义的模块配置,经过插件的处理,最终转换成 Vuex 创建 store 应用的标准格式参数。

下面我们来创建一个简单的 store。创建过程直截了当——仅需要提供一个 root 根模块配置:

import Vue from 'vue';
// 目前还未发布至 npm 仓库,可以先引入本地文件
import * as VuexTypes from 'vuex-types';

Vue.use(VuexTypes);

const store = VuexTypes.createStore({
    root: {
        mapDefinitionToModule() {
            return {
                state: {
                    count: 0
                },
                mutations: {
                    increment (state) {
                        state.count++
                    }
                }
            }
        }
    }
});

export default store;

注:在创建应用的时候,配置参数中根模块必须要以 root 为键名,其他模块则需要开启名命空间 (namespace: 'moduleName')

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

store.commit('increment');

console.log(store.state.count) // -> 1

创建好的 store 后,需要将其挂载到 Vue 组件实例上:

new Vue({
    el: '#app',
    store
});

现在我们可以从组件的方法提交一个变更:

methods: {
    increment() {
        this.$store.commit('increment');
        console.log(this.$store.state.count)
    }
}

接下来,我们将会对 Vuex 官方的购物车示例进行改造(最终请查看 购物车示例 ),结合示例代码更深入地探讨 VuexTypes 的核心概念。

核心概念

一、mapTypesToModule - 常量配置

ES2015 允许使用一个常量作为函数名,而 mapTypesToModule 的作用就是定义常量,它可以是数组或者函数形式:

数组形式

模块配置接收一个字符串数组:

// ...
mapTypesToModule: ['GET_ALL_PRODUCTS', 'DECREMENT_PRODUCT_INVENTORY']

函数形式

模块配置也允许接受一个函数,并且其返回值是字符串数组:

// ...
mapTypesToModule: function() {
    return ['GET_ALL_PRODUCTS', 'DECREMENT_PRODUCT_INVENTORY']
}

常量配置的使用

最终定义的常量会转化成键名和键值相同的 常量对象 - types

types: {
    'GET_ALL_PRODUCTS': 'GET_ALL_PRODUCTS',
    'DECREMENT_PRODUCT_INVENTORY': 'DECREMENT_PRODUCT_INVENTORY'
}

types 对象会作为参数传递给 模块配置模块辅助函数定义,后面的章节会介绍到。

二、mapTargetsToModule - 接口数据目标配置

日常业务中,对接口请求的处理通常包括 接口数据处理加载状态处理 以及 错误信息处理,相应的就需要在 state 状态树中创建对应的变量,然后定义 mutation 去触发状态变更,而 mapTargetsToModule 就是为了简化这类重复操作。它接收数组或函数两种形式:

数组形式

接口数据目标配置接收一个字符串数组:

// ...
mapTargetsToModule: ['products']

函数形式

接口数据目标配置也允许接受一个函数,并且其返回值是字符串数组:

// ...
mapTargetsToModule: function() {
    return ['products']
}

接口数据目标配置赋予的能力

1. 统一接口数据 state

根据 mapTargetsToModule 配置,字符串数组中的每个元素相应的都会自动创建出具有统一结构的 state,其中包含了 接口数据加载状态错误信息

// 模块的状态树
state = {
    // ...
    productsLoading: false,
    products: {
        data: [],
        pageIndex: 1,
        pageSize: 10,
        total: 0,
        loaded: false
    },
    productsError: null
}

2. 扩展常量对象

接口数据目标配置会结合 加载中加载成功加载失败 这三种接口通用状态生成具有固定形式的数组:

[
    'SET_PRODUCTS_LOADING',
    'SET_PRODUCTS_SUCCESS',
    'SET_PRODUCTS_FAILURE'
]

mapTypesToModule 常量配置相同,上面这个固定形式的数组也会转化成键名和键值相同的对象,并合并到 types 常量对象中:

types: {
    'SET_PRODUCTS_LOADING': 'SET_PRODUCTS_LOADING,
    'SET_PRODUCTS_SUCCESS': 'SET_PRODUCTS_SUCCESS,
    'SET_PRODUCTS_FAILURE': 'SET_PRODUCTS_FAILURE
}

3. 创建接口数据 mutations

接口数据 state 需要通过 mutations 触发更新,而根据 mapTargetsToModule 配置会自动创建出三种接口通用状态相应的 mutations,其函数名的形式和上述的 types 常量对象的键值一致。我们分别来看下如何触发状态变更。

接口数据加载中:

// 更新 state.productsLoading 值
commit(types.SET_PRODUCTS_LOADING) // -> productsLoading = true

接口数据加载成功:

我们知道 commit 函数接收的第二个参数即为触发 mutation 的 载荷(payload),而这里我们需要将接口的目标数据(真实的载荷)传递给 payload.data

// 更新 state.products 值
// 默认情况 payload.data 会合并到 state.products 对象中
commit(types.SET_PRODUCTS_SUCCESS, {
    data: {
        data: ['foo', 'bar'],
        total: 2
    }
})

// 最终的状态值
state = {
    // ...
    productsLoading: false,
    products: [
        data: ['foo', 'bar'],
        pageIndex: 1,
        pageSize: 10,
        total: 2,
        loaded: false
    ],
    productsError: null
}

除了传递给 payload.data 对象之外,还可以通过 payload.selector 函数来自定义数据传递:

// 通过 payload.selector 的形式提交变更,最终结果和上述写法相同
commit(types.SET_PRODUCTS_SUCCESS, {
    selector: function(payload, state) {
        return {
            data: ['foo', 'bar'],
            total: 2
        }
    }
})

接口数据加载失败:

commit(types.SET_PRODUCTS_FAILURE, {
    error: '接口报错信息xxx'
})

// 最终的状态值
state = {
    // ...
    productsLoading: false,
    productsError: '接口报错信息xxx'
}

通过 payload.error 可以传递接口报错信息,同样的也可以采用 payload.selector 的形式:

commit(types.SET_PRODUCTS_FAILURE, {
    selector: function(payload, state) {
        return '接口报错信息xxx'
    }
})

三、mapDefinitionToModule - 模块配置

mapDefinitionToModule 是最核心的配置,它对应的其实就是 Vuex 中五个核心概念(StateGetterMutationActionModule)的配置。它接收对象或函数两种形式:

对象形式

当模块配置以对象形式定义时,它与 Vuex 的核心配置并无差异:

// ...
mapDefinitionToModule: {
    state: {},
    getters: {},
    mutations: {},
    actions: {},
    modules: []
}

函数形式

模块配置也可以采用函数形式,此时它接收一个对象作为参数,这个对象包含了 types 常量对象命名空间名称,所以在定义 Vuex 核心配置的时候可以访问这两个变量:

// ...
namespace: 'products'
mapDefinitionToModule({ types, namespace }) {
    state: {},
    getters: {},
    mutations: {
        [types.DECREMENT_PRODUCT_INVENTORY](state, payload) {
            // ...
        }
    },
    actions: {
        [types.GET_ALL_PRODUCTS]({ commit, state }, payload) {
            // ...
        }
    },
    modules: []
}

在组件中使用绑定了命名空间的辅助函数

Vuex 官方提供了四个辅助函数,分别是 mapStatemapGettersmapMutationsmapActions,当使用这些函数来绑定带命名空间的模块时,写起来可能比较繁琐,我们对其进行了改造,简化了写法。

改造后的辅助函数名称分别为 _mappedState_mappedGetters_mappedMutations_mappedActions。它们都是对象形式,以 模块路径 作为键名,便可以访问指定模块下的应用。我们来看下改造后的辅助函数具体是如何使用的:

  • _mappedState:
export default {
    // ...
    _mappedState: {
        // 模块 A 命名空间下的状态值
        moduleA: {
            // 箭头函数可使代码更简练
            count: state => state.count,
            
            // 传字符串参数 'count' 等同于 `state => state.count`
            countAlias: 'count',
            
            // 为了能够使用 `this` 获取局部状态,必须使用常规函数
            countPlusLocalState (state) {
              return state.count + this.localCount
            }
        },
        // 模块 A 中嵌套的模块 C 命名空间下的状态值
        'moduleA/moduleC': ['some_nested_module_state'] // -> 映射名称和 state 子节点名称相同时,可以传递字符串数组
    }
}
  • _mappedGetters
export default {
    // ...
    _mappedGetters: {
        // 模块 A 命名空间下的计算属性
        moduleA: {
            // 把 `this.doubleCountAlias` 映射为模块 A 下的 `doubleCount`
            doubleCountAlias: 'doubleCount'
        }
        // 模块 B 命名空间下的计算属性
        moduleB: ['doneTodosCount'] // -> 映射名称和 getters 子节点名称相同时,可以传递字符串数组
    }
}
  • _mappedMutations
export default {
    // ...
    _mappedMutations: {
        // 模块 A 命名空间下的 mutations
        moduleA({ types }) {
            return {
                // 把 `this.add` 映射为模块 A 下的 `INCREMENT`
                add: types.INCREMENT
            }
        },
        // 模块 B 命名空间下的 mutations
        moduleB({ types }) {
            return [types.ADD_TODOS] // -> 映射名称和 mutations 子节点名称相同时,可以传递字符串数组
        }
    }
}

模块定义除了提供 types 局部常量对象之外,还通过 rootTypesrootCommitrootDispatch 暴露出根模块的 常量对象commitdispatch

export default {
    // ...
    _mappedMutations: {
        moduleA({ types, rootTypes, rootCommit, rootDispatch }) {
            return {
                add(commit, payload) {
                    // 根模块下的 ROOT_INCREMENT
                    rootCommit(rootTypes.ROOT_INCREMENT)
                    // 模块 A 下的 INCREMENT
                    commit(types.INCREMENT)
                }
            }
        }
    }
}
  • _mappedActions

同 _mappedMutations 一样,也提供了 typesrootTypesrootCommitrootDispatch 四个参数,但是更常用的写法是直接定义根模块,在其他模块直接使用根模块的方法即可:

export default {
    // ...
    _mappedActions: {
        root({ types }) {
            return {
                incrementIfOddOnRootSum: types.INCREMENT_IF_ODD
            }  
        },
        // 模块 A 命名空间下的 actions
        moduleA({ types, rootTypes, rootCommit, rootDispatch }) {
            return {
                // 把 `this.getList` 映射为模块 A 下的 `GET_LIST`
                getList: types.GET_LIST
            }
        }
    }
}

自定义辅助函数名称

在初始化插件时,可以通过配置一个前缀 mapPrefix 来自定义辅助函数名称:

Vue.use(VueTypes, {
    mapPrefix: '_myCustom'
}) // -> 改造后的辅助函数名称:_myCustomState、_myCustomGetters、_myCustomMutations、_myCustomActions

四、mapMergeStrategy - 接口数据目标更新策略

接口数据目标配置 这一章节中我们知道了这一配置赋予了三种能力,其中一个是 创建接口数据 mutations,在接口数据加载成功时,会将 真实载荷 更新到 state 状态树中,默认的更新策略采用 merge 合并的方式,即将新状态合并到旧状态中。

根据 mapMergeStrategy 配置,我们可以自定义接口数据加载成功时的更新策略。mapMergeStrategy 配置接收一个对象形式,键名需要和 接口数据目标配置 对应,键值为函数形式,接收的第一个参数表示旧状态,第二个参数表示新状态:

// ...
mapTargetsToModule: ['products'],
mapMergeStrategy: {
    // 处理 products 接口数据模块的策略
    products: function(prevState, nextState) {
        // 将新状态直接覆盖掉旧状态
        return nextState
    }
}

接口数据目标 products 具有的初始 state 状态如下:

products: {
    data: [],
    pageIndex: 1,
    pageSize: 10,
    total: 0,
    loaded: false
}

我们来触发 products 状态变更:

commit(types.SET_PRODUCTS_SUCCESS, {
    data: {
        data: ['foo', 'bar'],
        total: 2
    }
})

根据上面的例子,我们定义的更新策略是采用覆盖的方式,最终更新之后 products 状态如下:

// 覆盖了旧的 products 状态值
products: {
    data: ['foo', 'bar'],
    total: 2
}