Vue 12 Vuex

Vue 12 Vuex

在之前组件间通信的时候提到过,可以用一个类似中介一样的东西收集和保存所有状态,让所有的组件通用。如果在Vue之外定义一个对象,然后在Vue中引用对象,这个对象也会变成响应式的。 在简单的应用中,一般就使用简单的一个对象集中存储一下数据即可,比如一个全局变量。但是在复杂的应用里,就需要用一个东西来集中

在之前组件间通信的时候提到过,可以用一个类似中介一样的东西收集和保存所有状态,让所有的组件通用。如果在Vue之外定义一个对象,然后在Vue中引用对象,这个对象也会变成响应式的。 在简单的应用中,一般就使用简单的一个对象集中存储一下数据即可,比如一个全局变量。但是在复杂的应用里,就需要用一个东西来集中保存状态。其实可以把Vuex认为是一个数据库,可以向其中写入数据,获取数据(自动的而且是响应式的),Vuex帮你把状态和改变都记录下来。 Vuex和Vue Router一样都是由官方维护的库,文档地址
  1. 安装Vuex和导入
  2. 初步配置Vuex和使用数据
  3. Vuex对象
  4. Vuex使用注意事项

安装Vuex和导入

Vuex和Vue Router一样,都是需要包含在最终发行版里的,所以可以无需安装-dev,直接安装普通依赖即可。和VueRouter很类似,也是Vue的一个插件,需要先安装再启用:
  1. 安装Vue-router本身:npm install vuex --save
  2. main.js中启用路由插件:
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
官方在这里还有一个小提示,就是这个库依赖于Promise,如果浏览器不支持Promise就需要引入一个库。 可以通过CDN引入比较方便:<script src="https://cdn.bootcss.com/es6-promise/4.1.1/es6-promise.auto.min.js"></script>,不过既然前端工程化,还是使用NPM安装和导入吧。

初步配置Vuex和使用数据

既然是一个数据库,就要使用Vuex通过一个对象新创建一个实例,其中可以传入一个option对象。这个Vuex实例就作为存储的库,被称为store
//配置Vuex
const store = new Vuex.Store({
    state:{
        count: 0
    }
});
这个store的数据都保存在state属性中。 然后需要在根Vue实例中将其引入:
const app = new Vue({
    router,
    //导入store属性
    store,
    render: h => h(App)
}).$mount("#app")
在根实例中导入之后,只要是作为根实例内部的所有组件,都可以通过this.$store.state访问到store对象,其中可以直接使用具体的数据作为名称获取数据,比如this.$store.state.countstore对象,在根实例内部的所有组件中,访问到的都是同一个对象,有点像Vue Router中的$router路由器对象。 直接访问全局store变量一般不推荐,因为会造成高耦合,一般的做法是使用一个计算属性。在组件内部,this.$store.state.count是只读的,即无法通过这个来设置store中的值。
<template>
    <div>
        <p>首页内容</p>
        <router-view></router-view>
        <p>Count的数值是: {{count}}</p>
    </div>
</template>

<script>
    export default {
        name: "about",
        computed:{
            count: function () {
                return this.$store.state.count;
            }
        }
    }
</script>

<style scoped>
    div {
        font-size: 3rem;
        text-align: center;
    }
</style>
可以看到,这样就读出来了其中的数据。 如果要修改数据怎么办。需要在store对象中配置mutations选项,所有的提交都要通过这个选项中的方法来操作。

Vuex对象

Vuex对象的选项有如下几种:
  1. state
  2. mutation
  3. getter
  4. action
  5. module

state

state选项中保存了所有的数据。在刚才的例子中已经使用过了。 既然一个Vuex对象保存了状态或者说是数据,不外乎两种方式,一种是存,一种是取。 取的方法已经说过了,通过在根Vue实例中注入store属性,所有的组件都可以访问到Vuex对象来获取具体的数据。 官方文档在这里还提供了一些简化的手段,方便导入很多状态数据,可以了解一下。

mutation

mutation选项中,所有的方法都是用来修改state数据中,外界想要修改状态,必须通过提交mutation中的方法名称来进行操作,无法直接操作。 接着上边的例子,给count定义一个加1和减1的mutation方法:
const store = new Vuex.Store({
    state:{
        count: 0
    },
    mutations:{
        increase: function (state) {
            state.count++;
        },
        decrease: function (state) {
            state.count--;
        }
    }
});
方法已经定义好了,现在看看如何通过组件来修改状态,添加两个按钮和对应的事件:
<template>
    <div>
        <p>首页内容</p>
        <router-view></router-view>
        <p>Count的数值是: {{count}}</p>
        <p>
            <button @click="increase">+1</button>
            <button @click="decrease">-1</button>
        </p>
    </div>
</template>

<script>
    export default {
        name: "about",
        computed:{
            count: function () {
                return this.$store.state.count;
            }
        },
        methods:{
            increase: function () {
                this.$store.commit("increase");
            },
            decrease: function () {
                this.$store.commit("decrease");
            }
        }
    }
</script>
mutation方法还可以接受第二个参数,用于进行操作:
mutations:{
    increase: function (state, 3) {
        state.count = state.count + 3;
    }
}
第二个参数还可以是一个对象,包含了传入进来的所有参数,这样使用就很灵活:
mutations:{
    increase: function (state, params) {
        state.count = state.count + params.count;
    }
}
//commit的时候不使用方法名,而使用一个对象,其中的type=方法名,count=传给params的属性名称和值
this.$store.commit ({
    type :'increase',
    count : 10
}) .
推荐使用第二种方式,比较灵活。最好通过函数名的语义区分不同的方法,比如increaseBy3increaseByCount等。 在mutations里尽量不要异步修改数据,否则不知道何时会更新数据。

getter

Java里看到getter基本上就知道是怎么回事了。这里的getter也是这个意思,在取出数据的时候进行计算并缓存,就像是针对store的计算属性一样。 本小节里提到getter指的是这一类方法或者功能;提到getters指的是Vuex实例的getters属性。 如果很多组件都需要基于原始数据进行一些逻辑,比如上边例子里,一些组件希望获取count的5倍结果,一些组件希望获取count的10倍结果,与其把逻辑在每个组件的计算属性里写一遍,可以考虑统一写在Vuex的getters属性中。
getters:{
    count5:function (state) {
        return state.count * 5;
    },
    count10: function (state) {
        return state.count * 10;

    }
}
在取数据的时候,就需要通过this.$store.getters加上属性名来取值,修改index.vue
<template>
    <div>
        <p>首页内容</p>
        <router-view></router-view>
        <p>Count的数值是: {{count}}</p>
        <p>5倍Count的数值是: {{count5}}</p>
        <p>10倍Count的数值是: {{count10}}</p>
        <p>
            <button @click="increase">+1</button>
            <button @click="decrease">-1</button>
        </p>
    </div>
</template>

<script>
    export default {
        name: "about",
        computed:{
            count: function () {
                return this.$store.state.count;
            },
            count5: function () {
                return this.$store.getters.count5;
            },
            count10: function () {
                return this.$store.getters.count10;
            },
        },
        methods:{
            increase: function () {
                this.$store.commit("increase");
            },
            decrease: function () {
                this.$store.commit("decrease");
            }
        }
    }
</script>
getters中的方法可以接受第二个参数,第二个参数指向vuex实例的getters属性,这样就可以在一个getter方法内调用其他的getter方法。 比如count乘以5再加3,可以复用count5
count5plus3: function (state, getters) {
    return getters.count5 + 3;
}
当然,这个例子的业务逻辑过于简单,无需特意复用,可以直接计算。接受第二个参数的好处是可以使用其他getter方法像搭积木一样构建更复杂的业务逻辑。 还可以让getter方法返回一个函数,用于传递参数来取值,比如返回一个方法,参数是倍数:
countByParam:function (state) {
    return function (multi) {
        return state.count * multi;
    };
}
index.vue的计算属性中可以传递参数:
countByParam:function (state) {
    return function (multi) {
        return state.count * multi;
    };
}
这里返回一个函数,可以对其传入参数,计算指定的倍数,在index.vue中可以动态的传入参数:
<template>
    <div>
        <p>根据参数计算的结果是: {{countByParam}}</p>
        <input type="nubmer" v-model="multi">
    </div>
</template>

<script>
    export default {
        name: "about",
        data: function () {
            return {
                multi: 0
        },
        computed:{
            countByParam:function () {
                return this.$store.getters.countByParam(this.multi);
            }
        },
    ......
    }
</script>
在input中输入数字,就会结合当前的Vuex中的count和这个数字计算出倍数。 使用传参数的时候,不会缓存,每次都会重新计算。 还有一些辅助方法如mapGetters 辅助函数可以查看文档。

action

actions中也是一系列方法,这些方法不通过直接操作数据,而是通过commit mutation来操作。
const store = new Vuex.Store({
    state:{
        count: 0
    },
    mutations:{
        increase: function (state) {
            state.count++;
        },
        decrease: function (state) {
            state.count--;
        },
        reset: function (state) {
            state.count = 0;
        }
    },
    actions:{
        increaseBy1:function (context) {
            context.commit("increase");
        }
    }
});
index.vue里,改用action来让count+1:
methods:{
    increase: function () {
        this.$store.dispatch("increaseBy1");
    },
    decrease: function () {
        this.$store.commit("decrease");
    }
}
使用action的时候,调用Vuex对象的方法dispatch,参数名称为actions里定义的方法名称。 猛一看感觉好像吃饱了撑的,能直接去使用mutations,为何还要包一层actioncommit mutation,似乎又封装了一层,多此一举。 其实仔细的话,WebStorm已经在dispatch方法下边划出了波浪线,dispatch实际上返回的是一个Promise对象,还可以使用回调函数。 mutations内部不能执行异步操作,而actions就可以,如果直接就套一层,那就没意思了。核心在于异步操作,比如设置延迟一秒再去增加1,可以返回一个Promise对象:
actions:{
    increaseBy1:function (context) {
        return new Promise(resolve=>{
            setTimeout(() => {
                context.commit("increase");
                resolve();
            }, 1000); });
    }
}
将业务逻辑写在一个Promise对象中即可。 之后在index.vue里使用的时候可以加上回调函数:
increase: function () {
    this.$store.dispatch("increaseBy1").then(() => {
        console.log("成功完成+1")
    })
},
store.dispatch可以处理被触发的action的处理函数返回的Promise,并且store.dispatch也返回Promise对象,意味着actions能够连续调用,就像在一个getter方法里调用另一个getter方法一样(原理不同)。 这就是异步修改Vuex状态,而且修改成功与否还能够回调执行其他动作。actions实际上就是异步版本的mutations。 在之前commit mutations的时候,提到过可以使用对象形式,传入type和参数名称的对象,actions也一样支持,这是官网的例子:
// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

module

可能注意到,在actions中,方法的参数名称叫做context,虽然名称是任意起的,但是这暗示着这个不是普通的state对象。 如果项目很大,把所有的选项都写在一个store对象里并不好,modules就是把Vuex区分成几个部分,针对每一个部分只要进行命名,然后注册在Vuex实例化的过程中,就可以单独使用。 比如简单的例子:
const moduleA = {
    state: {
        name: "MA"
    }
};

const moduleB = {
    state: {
        name: "MB"
    }
};

const mainStore = new Vuex.Store({
    modules: {
        a: moduleA,
        b: moduleB
    }
});

//moduleA的state对象
console.log(mainStore.state.a);

//访问moduleA和moduleB的name属性
console.log(mainStore.state.a.name);
console.log(mainStore.state.b.name);
此时如果在moduleA或者moduleB中添加gettersmutations,其中的方法的第一个参数state是自己所属的模块的对象,第二个参数依然是实例的getters,还可以有第三个参数,就是根节点,由于modules也是一个选项,所以还可能会有根节点。 此外,还要注意命名空间的问题,默认情况下,各个模块里的方法名称,都会统一注册到Vuex实例内部的全局空间去。看一个例子:
const moduleA = {
    state: {
        name: "MA"
    },
    getters:{
        getname:function (state) {
            return "This is module " + state.name + "!";
        }
    }
};

const moduleB = {
    state: {
        name: "MB"
    },
    getters:{
        getname:function (state) {
            return "This is module " + state.name + "!";
        }
    }
};

const store = new Vuex.Store({
    state:{
    name: 'masterModule'
    },
    modules: {
        a: moduleA,
        b: moduleB
    }
});
这个例子如果运行起来,Vue会报错:
vuex.esm.js:765 [vuex] duplicate getter key: getname
这是因为Vuex会把模块A和模块B的getters函数都注册到store.getters下边,但是两个函数的名称相同,就有了冲突。在外部使用Vuex实例的时候,是不区分模块的,比如index.vue里是这么调用的:
computed:{
    getMA:function () {
        return this.$store.getters.getname;
    }
}
此外,根模块的name='masterModule'想要访问,就要通过getters的第三个参数根节点。 模块命名空间就是给模块对象再加上一个namespaced: true,之后其中的getteractionmutation,都会自动调整命名,这个可以参考官方的文档。 还记得本节开头的actions中函数的参数吗,和gettersmutations不同,模块内部的action,局部状态通过context.state暴露出来,根节点状态则为context.rootState。 最后写一个简单一点的完整例子,包含两个模块的Vuex实例:
const module1 = {
    namespaced:true,
    state: {
        name: "Module1",
        count: 0
    },

    getters:{
        //故意起的和另一个模块的方法名称一样
        //使用rootState访问根节点
        getname: function (state, getters, rootState) {
            return "This is " + state.name + "from " + rootState.name;
        },
        //故意起的和另一个模块的方法名称一样
        //使用rootState访问根节点
        gettotal: function (state, getters, rootState) {
            return state.count + rootState.count;
        }
    },

    //模块内部默认引用自己的state
    mutations:{
        increase1: function (state) {
            state.count += 1;
        }
    },

    //actions内部引用context也是自己的$store对象
    //context.state是当前模块的state
    //context.rootState是根模块的state
    actions: {
        asycnincrease: function (context) {
            return new Promise(resolve => {
                console.log(context.state.name);
                console.log(context.rootState.name);
                setTimeout(()=>{
                    context.commit("increase1");
                    resolve();
                },1000)
            })
        }
    }
};

const module2 = {
    namespaced:true,
    state: {
        name: "Module2",
        count: 0
    },
    getters:{
        getname: function (state, getters, rootState) {
            return "This is " + state.name + "from " + rootState.name;
        },
        gettotal: function (state, getters, rootState) {
            return state.count + rootState.count;
        }
    },
    mutations:{
        increaseby1: function (state) {
            state.count += 1;
        }
    },

    actions: {
        asycnincreaseb: function (context) {
            return new Promise(resolve => {
                console.log(context.state.name);
                console.log(context.rootState.name);
                setTimeout(()=>{
                    context.commit("increaseby1");
                    resolve();
                },1000)
            })
        }
    }

};

const store = new Vuex.Store({
    state:{
        name: 'masterModule',
        count: 0
    },
    //在Vuex实例中注册模块,模块的键就是对模块对象的引用
    modules: {
        ma: module1,
        mb: module2
    },
    //故意定义了一个和module1名称一样的方法
    mutations:{
        increase1:function (state) {
            state.count++;
        }
    }
});
定义好之后,关键是在带有命名空间的情况下如何使用这个Vuex实例,看index.vue
<template>
    <div>
        <p>A的名称: {{ getMA }} <br>A count: {{countA}}</p>
        <p>A count + Total Count = {{at}}</p>
        <p>
            <button @click="handle">A+1</button>
        </p>
        <p>
            <button @click="handle2">异步A+1</button>
        </p>
        <p>B的名称: {{ getMB }} <br>B count: {{countB}}</p>
        <p>B count + Total Count = {{bt}}</p>

        <p>
            <button @click="handleb">+1</button>
        </p>
        <p>
            <button @click="handleb2">B异步+1</button>
        </p>
        <p>全局模块的名称: {{mastername}} <br> 全局模块的count: {{mastercount}}</p>
        <p>
            <button @click="masterincrease">全局变量+1</button>
        </p>
    </div>
</template>

<script>
    export default {
        name: "about",
        computed: {
            //使用模块的getters,此时this.$store.getters是一个对象,通过命名空间找到具体的模块的getname函数。
            getMA: function () {
                return this.$store.getters['ma/getname'];
            },
            getMB: function () {
                return this.$store.getters['mb/getname'];
            },
            //访问state,通过在Vuex实例中注册的名称找到对应的state
            countA: function () {
                return this.$store.state.ma.count;
            },
            countB: function () {
                return this.$store.state.mb.count;
            },
            //访问Vuex根节点的属性,直接使用this.$store.state
            mastercount: function () {
                return this.$store.state.count;
            },
            mastername: function () {
                return this.$store.state.name;
            },
            at: function () {
                return this.$store.getters['ma/gettotal'];
            },
            bt: function () {
                return this.$store.getters['mb/gettotal'];
            },
        },
        methods: {
            //使用mutations,与getters类似,通过命名空间和函数名构成的键对应到具体函数
            handle: function () {
                return this.$store.commit('ma/increase1');
            },
            handleb: function () {
                return this.$store.commit('mb/increaseby1');
            },
            //使用action,与使用mutation类似,dispatch参数的值是命名空间构成的键
            handle2: function () {
                this.$store.dispatch('ma/asycnincrease').then(() => {
                    console.log("异步A+1完成");
                });
            },
            handleb2: function () {
                this.$store.dispatch('mb/asycnincreaseb').then(() => {
                    console.log("异步B+1完成");
                })
            },
            //使用根节点的amutations,无需命名空间,直接使用
            masterincrease: function () {
                this.$store.commit("increase1");
            }
        }
    }
</script>

<style scoped>
    div {
        font-size: 2rem;
        text-align: center;
        line-height: 0.9;
    }
</style>

Vuex使用注意事项

官网在这里有对项目结构的介绍,很不错,复制过来加点自己的想法:
  1. 应用层级的状态应该集中到单个store对象中。所谓应用层级,就是全局的状态,集中到一个store对象中,对于各个组件自己保存的状态,则无需都写入到store对象中。
  2. 提交mutation是更改状态的唯一方法,并且这个过程是同步的。凡是要同步修改数据,一定要通过mutation方法,不要采取其他的hack方法。我自己试了在mutation中也可以强行写异步的方法,但是估计会导致阻塞吧。
  3. 异步逻辑都应该封装到action里面。凡是需要异步修改状态的,必须要全部写在action中,不要写到外边来。
LICENSED UNDER CC BY-NC-SA 4.0
Comment