Vue进阶之使用命名空间更优雅地规划store

时隔大半年,又回到 Vue 的怀抱。自以为对 Vue 还是有一定理解,但短短的两周的项目实践,让我还是有新的收获:使用命名空间规划设计 store,真是活到老,学到老。本文将基于 Vuex 探讨如何更优雅地规划设计 store。

1. 复习 Vuex 的基本使用

在 Vuex 中,一个 store 实例,由四个部分组成:

  • state:保存应用状态
  • getter:把状态挂载到特定的组件上供组件使用
  • mutation:用于修改应用状态
  • action:mutation 都是同步操作,引入了 action 处理异步改变应用状态的场景

1.1 mapState & getter

先看第一个问题:store 中保存的应用状态,如何挂载到组件上供组件使用?通常是基于 store 中的数据为组件生成计算属性,常见有 3 种方法:

  • 使用 this.$store
  • 通过 Vuex.mapState() 方法
  • 通过 Vuex.mapGetters() 方法
<div id="app">
    <ul><li v-for="item in fruits" :key="item.key">{{item.name}}</li></ul>
    <ul><li v-for="item in persons" :key="item.key">{{item.name}}</li></ul>
    <ul><li v-for="item in persons2" :key="item.key">{{item.name}}</li></ul>
    <ul><li v-for="item in persons3" :key="item.key">{{item.name}}</li></ul>
</div>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
<script>
    Vue.use(Vuex);
    let store = new Vuex.Store({
        state: {
            fruits: [
                {
                    name: '苹果',
                    id: 0
                },
                {
                    name: '香蕉',
                    id: 1
                },
                {
                    name: '西瓜',
                    id: 2
                }
            ],
            persons: [
                {
                    name: '刘能',
                    id: 0
                },
                {
                    name: '赵四',
                    id: 1
                },
                {
                    name: '广坤',
                    id: 2
                }
            ]
        },
        getters: {
            personsGetter(state) {
                return state.persons;
            }
        }
    });

    new Vue({
        el: '#app',
        store,
        computed: {
            // 直接通过 store 实例生成计算属性
            fruits() {
                return this.$store.state.fruits
            },
            // 使用 mapState 生成计算属性。不需要改变键值时使用数组参数
            ...Vuex.mapState([
                'persons'
            ]),
            // 需要改变键时使用对象参数
            ...Vuex.mapState({
                persons2: 'persons'
            }),
            // 通过 getter 生成计算属性
            ...Vuex.mapGetters({
                persons3: 'personsGetter'
            })
        }
    });
</script>

1.2 mutation

上面介绍了如何使用 store 中的状态,接下来的问题就是如何修改 store 了?在 Vuex 中通过 mutation 来修改应用状态,即 store。

<div id="app">
    <dl>
        <dt>{{personLength}}</dt>
        <dd v-for="item in persons" :key="item.key">{{item.name}}</dd>
    </dl>
    <button @click="addPerson">添加</button><button @click="delPerson">删除</button>
</div>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>

<script>
    Vue.use(Vuex);
    let store = new Vuex.Store({
        state: {
            persons: [
                {
                    name: '刘能',
                    id: 0
                },
                {
                    name: '赵四',
                    id: 1
                },
                {
                    name: '广坤',
                    id: 2
                }
            ]
        },
        getters: {
            persons(state) {
                return state.persons;
            }
        },
        mutations: {
            addPerson(state) {
                state.persons = state.persons.concat({
                    name: `名称${Date.now()}`,
                    id: state.persons.length + 1
                });
            },
            delPerson(state, delId) {
                state.persons = state.persons.filter(d => d.id !== delId);
            }
        }
    });

    new Vue({
        el: '#app',
        store,
        computed: {
            personLength() {
                return this.persons.length;
            },
            ...Vuex.mapGetters([
                'persons'
            ])
        },
        methods: {
            addPerson() {
                // 提交 mutation 修改 state
                this.$store.commit('addPerson');
            },
            delPerson() {
                // 对象方式提交 mutation
                this.$store.commit({
                    type: 'delPerson',
                    id: this.persons.length--
                });
            }
        }
    });
</script>

关于 mutation 函数的命名,最佳实践是使用字符串常量。这样一来,上面的 mutation 会变成大概下面的样子:

const ADD_PERSON = 'ADD_PERSON';
const DEL_PERSON = 'DEL_PERSON';

// ...

mutations: {
    // 最佳实践: 使用常量替代 Mutation 事件类型
    [ADD_PERSON](state) {
        state.persons = state.persons.concat({
            name: `名称${Date.now()}`,
            id: state.persons.length + 1
        });
    },
    [DEL_PERSON](state, delId) {
        state.persons = state.persons.filter(d => d.id !== delId);
    }
}

// ...

1.3 action

mutation 可以修改 store,但 mutation 中都是同步操作,不能包含异步操作.异步操作交给 action 来处理

<div id="app">
    <dl v-show="personLength > 0">
        <dt>{{personLength}}</dt>
        <dd v-for="item in persons" :key="item.key">{{item.name}}</dd>
    </dl>
    <div v-show="personLength===0">加载中...</div>
</div>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>

<script>
    Vue.use(Vuex);

    const SET_QUERY_FLAG = 'SET_QUERY_FLAG';
    const SET_PERSONS = 'SET_PERSONS';

    let store = new Vuex.Store({
        state: {
            querying: false,
            persons: []
        },
        getters: {
            persons(state) {
                return state.persons;
            }
        },
        actions: {
            startQueryPersons({commit}) {
                // 设置loading    
                commit(SET_QUERY_FLAG, true);
            },
            queryPersons({dispatch, commit}) {
                // action 组合,在这里先调用了 startQueryPersons action
                dispatch('startQueryPersons').then(() => {
                    // 异步操作
                    setTimeout(() => {
                        commit(SET_PERSONS, [
                            {
                                name: '刘能',
                                id: 0
                            },
                            {
                                name: '赵四',
                                id: 1
                            },
                            {
                                name: '广坤',
                                id: 2
                            }
                        ])
                    }, 2000);
                });
            }
        },
        mutations: {
            [SET_PERSONS](state, persons) {
                state.persons = persons;
            },
            [SET_QUERY_FLAG](state, flag) {
                state.querying = flag;
            }
        }
    });

    new Vue({
        el: '#app',
        store,
        computed: {
            personLength() {
                return this.persons.length;
            },
            ...Vuex.mapGetters([
                'persons'
            ])
        },
        methods: {
            // 通过 mapActions 可以把 action 映射为方法
            ...Vuex.mapActions({
                doQueryPersons: 'queryPersons'
            })
        },
        created() {
            //this.$store.dispatch('queryPersons'); // 直接 dispatch action
            this.doQueryPersons();
        }
    });
</script>

如上面的代码,action 可以通过 dispatch 直接触发,也可以通过 Vuex.mapActions 变成方法来供组件调用。

2. 为什么 store 需要命名空间

复习完了 Vuex 的使用,我们来看一个真实的应用场景。假设我们要做一个商城应用,有商品列表,订单列表两个功能页。很快,我们实现了功能,代码如下:

<div id="app">
    <dl>
        <dt>商品列表</dt>
        <dd v-for="item in products">{{item.name}}</dd>
    </dl>

    <dl>
        <dt>订单列表</dt>
        <dd v-for="item in orders">{{item.name}}</dd>
    </dl>
</div>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
<script>
    Vue.use(Vuex);

    let store = new Vuex.Store({
        modules: {
            product: {
                state: {
                    list: [
                        {
                            name: '苹果电脑',
                            id: 0
                        },
                        {
                            name: 'IphoneX',
                            id: 1
                        }
                    ]
                },
                getters: {
                    productList(state) {
                        return state.list;
                    }
                },
                actions: {
                    queryList() {
                        console.log('product queryList');
                    }
                },
                mutations: {
                    setList() {
                        console.log('product setList')
                    }
                }
            },
            order: {
                state: {
                    list: [
                        {
                            name: '订单-苹果电脑',
                            id: 0
                        },
                        {
                            name: '订单-IphoneX',
                            id: 1
                        }
                    ]
                },
                getters: {
                    orderList: (state) => state.list
                },
                actions: {
                    queryList() {
                        console.log('order queryList');
                    }
                },
                mutations: {
                    setList() {
                        console.log('order setList')
                    }
                }
            }
        }
    });

    new Vue({
        el: '#app',
        store,
        computed: {
            ...Vuex.mapGetters({
                products: 'productList'
                orders: 'ordersList'
            })
        },
        created() {
            // this.$store.dispatch('queryList'); // product 和 order 中的 queryList 都被调用了
            this.$store.commit('setList'); // product 和 order 中的 setList 都被调用了
        }
    });
</script>

通常,像商城这样中大型项目,都是多人协作共同开发的。多人协作时,最怕的就是代码冲突。上面的 store 设计,有一个商品列表和一个订单列表,它们对应的 getters 分别是 productList 和 orderList。 如果我们定义 mutation-type 的话,大概率会是下面这样:

const ADD_PRODUCT = 'ADD_PRODUCT';
const DEL_PRODUCT = 'DEL_PRODUCT';
const UPDATE_PRODUCT = 'UPDATE_PRODUCT';

const ADD_ORDER = 'ADD_ORDER';
const DEL_ORDER = 'DEL_ORDER';
const UPDATE_ORDER = 'UPDATE_ORDER';

大家应该也注意到了,为了防止冲突,我们为每个 mutation 加了模块后缀,使原本 ADD,DEL 就可以很好表示 mutation 类型的硬生生变成了 ADD_PRODUCT, ADD_ORDER。

当然上面的代码,完全可以实现业务功能。但是在防止冲突或模块解藕上还是差了一步。同样,为了防止 actions 冲突,我们不得不这样来命名 actions:queryProductList, queryOrderList。

getter重复

那么,怎么让各模块的 store 完全解藕,在为 getter, mutation 或 action 命名时不用小心翼翼地加前后缀来防止冲突呢?Vuex 提供了命名空间来解决这个问题。

3. 基于命名空间的 store 设计

同样是上面讲的商城应用,使用了 namespace 的 store 代码大概如下:

<div id="app">
    <dl>
        <dt>商品列表</dt>
        <dd v-for="item in products">{{item.name}}</dd>
    </dl>

    <dl>
        <dt>订单列表</dt>
        <dd v-for="item in orders">{{item.name}}</dd>
    </dl>
</div>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
<script>
    Vue.use(Vuex);
    let store = new Vuex.Store({
        modules: {
            product: {
                namespaced: true, // 添加命名空间
                state: {
                    list: [
                        {
                            name: '苹果电脑',
                            id: 0
                        },
                        {
                            name: 'IphoneX',
                            id: 1
                        }
                    ]
                },
                getters: {
                    list(state) {
                        return state.list;
                    }
                },
                actions: {
                    queryList() {
                        console.log('product queryList');
                    }
                },
                mutations: {
                    setList() {
                        console.log('product setList')
                    }
                }
            },
            order: {
                namespaced: true, // 添加命名空间
                state: {
                    list: [
                        {
                            name: '订单-苹果电脑',
                            id: 0
                        },
                        {
                            name: '订单-IphoneX',
                            id: 1
                        }
                    ]
                },
                getters: {
                    list: (state) => state.list
                },
                actions: {
                    queryList() {
                        console.log('order queryList');
                    }
                },
                mutations: {
                    setList() {
                        console.log('order setList')
                    }
                }
            }
        }
    });

    new Vue({
        el: '#app',
        store,
        computed: {
            // 把 product 模块中的 list getter 映射为 products
            ...Vuex.mapGetters('product', {
                products: 'list'
            }),
            // 把 order 模块中的 list getter 映射为 orders
            ...Vuex.mapGetters('order', {
                orders: 'list'
            }),
        },
        mounted() {
            this.$store.dispatch('product/queryList'); // 调用 product模块的 getList action
            this.$store.commit('order/setList'); // 调用order模块的 setList mutation
        }
    });
</script>

大家会发现,虽然 product 和 order 模块的 state, getter, action, mutation 的名称都是一样的,但却因为加了 namespaced: true 这个配置而被很好的隔离开来。

关于 store 的设计,我个人建议至少要有2级结构,这样能保证最大的扩展性。基于上面的商城应用,其 store 的建议设计是这样的:

商城store

概要代码如下:

<div id="app">
    <h1>{{appName}}</h1>
    <dl>
        <dt>商品列表</dt>
        <dd v-for="item in products">{{item.name}}</dd>
    </dl>
</div>
<script src="https://unpkg.com/vue@2.5.17/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
<script>
    Vue.use(Vuex);
    let store = new Vuex.Store({
        modules: {
            // 全局模块
            app: {
                namespaced: true,
                state: {
                    name: 'XYZ商城'
                },
                getters: {
                    name: state => state.name
                },
                actions: {},
                mutations: {}
            },
            // 商品模块
            product: {
                namespaced: true,
                modules: {
                    // 商品列表
                    list: {
                        namespaced: true, // 添加命名空间
                        state: {
                            list: [
                                {
                                    name: '苹果电脑',
                                    id: 0
                                },
                                {
                                    name: 'IphoneX',
                                    id: 1
                                }
                            ]
                        },
                        getters: {
                            list(state) {
                                return state.list;
                            }
                        },
                        actions: {
                            queryList() {
                                console.log('product queryList');
                            }
                        },
                        mutations: {
                            setList() {
                                console.log('product setList')
                            }
                        }
                    },
                    // 商品详情
                    detail: {
                        namespaced: true,
                        state: {},
                        getters: {},
                        actions: {},
                        mutations: {}
                    },
                    // 其它商品页面...
                }
            },
            // 订单
            order: {
                namespaced: true,
                modules: {
                    // 订单列表
                    list: {
                        namespaced: true,
                        state: {},
                        getters: {},
                        actions: {},
                        mutations: {}
                    },
                    // 订单详情
                    detail: {
                        namespaced: true,
                        state: {},
                        getters: {},
                        actions: {},
                        mutations: {}
                    },
                    // ... 其它订单模块页面
                }
            }
        }
    });

    new Vue({
        el: '#app',
        store,
        computed: {
            ...Vuex.mapGetters('app', {
                appName: 'name'
            }),
            ...Vuex.mapGetters('product/list', {
                products: 'list'
            })
        },
        mounted() {
            this.$store.dispatch('product/list/queryList'); // 调用 product/list 模块的 getList action
        }
    });
</script>

4. 小结

建议大家在项目实践中为 store 加上命名空间,以上!

留言列表

    发表评论: