Vue(六) Vuex

Vue现代化使用方法(四)--Vuex

在组件内可以通过data属性共享数据,父子组件也可以通过props进行数据共享,但如果是兄弟跨组件之间的数据共享,就要借助Vuex,Vuex类似大树的主干,各个组件类似一个个独立的树枝,组件之间通过Vuex形成一种联系,只要把需要共享的数据放到Vuex中,所有相关组件都可以访问引用,这样就能形成跨组件的数据共享。

通过npm进行安装

npm install --save vuex

基本组成

按Vuex的设计思路,其核心(Store)是包括下面四部分:

  • state
  • getters
  • mutations
  • actions

state

// 在index.js页面做如下改造
import Vuex from 'vuex'; // 引入Vuex
Vue.use(Vuex); // 启用Vuex

const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment (state) {
            state.count++
        }
    }
}); // 设定一个公共仓库

let vm = new Vue({
    el: "#app",
    store, // 引入建立的仓库
    render: h => h(app)
});
...
// 在index.vue的计算属性中添加下面代码
computed: {
    count () {
        return this.$store.state.count
    }
}
...
// 在index.vue的模板中添加如下内容,引入之前创建的header.vue组件

index.vue中的值:{{count}}

... // 在header.vue组件也一样添加相关计算属性,并修改模板中的内容

header.vue中的count值:{{count}}

... // 这时页面就渲染为

index.vue中的值:0

header.vue中的count值:0

通过上例我们可以看到在index.js设定的公共仓库(store)中的state内容,在index.vue和header.vue中共享,通过调用this.$store.state.count来引用,这样就做到了跨组件数据共享

把数据放到计算属性中,

在上例中,我们是通过this.$store.state.count对仓库中值进行访问,这种写法是显得有些冗余,Vuex在这里提供了mapState函数来优化了这个问题

// 利用mapState,可以简化对仓库中值的调用,计算属性中的其他值,正常使用不受影响,添加了这个方法只影响了对仓库中值的取用
// 先引入这个方法
import { mapState } from 'vuex';
...
// 在计算属性中用这个方法包裹
computed: mapState({
    count (state) {
        return state.count;
    },
    fullName () {
        return `${this.firstName}-${this.lastName}`;
    }
}),
...
// 利用箭头函数还可以在简化下当前代码
count: state => state.count,
fullName () {
    return `${this.firstName}-${this.lastName}`;
}
...
// 借助mapState可以使用字符串数组来再简化代码,调用this.count时 映射 store.sate.count
computed: mapState(['count']),
...
// 如果名词不一致可以使用对象形式,在模板中使用{{co}}
...mapState({
    co: 'count'
})
...
// 不过这时我们不好添加其他的计算属性了,在这里可以简化使用对象扩展来解决这个问题
computed: {
    fullName () {
        return `${this.firstName}-${this.lastName}`;
    },
    ...mapState(['count'])
},

getters

getters就是针对state的计算属性

// index.js中进行下面内容添加
const store = new Vuex.Store({
    state: {
        count: 0,
        name: 'lin ken',
        address: '深圳XX区XX街道',
    },
    getters: {
        userInfo (state) {
            return `姓名:${state.name}; 住址:${state.address}`;
        }
    }
});
...
// index.vue计算属性添加下面内容
userInfo () {
    return this.$store.getters.userInfo;
},
...
// index.vue模板添加如下内容

{{userInfo}}

与mapState类似,getters也有mapGetters使用方式与mapState类似

// 引入mapGetters
import { mapState,mapGetters } from 'vuex';
...
// 在计算属性中使用扩展对象写法把相关getters写到计算属性中
fullName () {
    return `${this.firstName}-${this.lastName}`;
},
...mapGetters(['userInfo']),
...mapState(['count']),
...
// 如果不希望使用userInfo这个名词,mapGetters可以使用对象形式,替换相关名词
...mapGetters({
    cusInfo: 'userInfo'
})
...
// 模板中可以使用

{{cusInfo}}

mutation

mutation是Vuex设定唯一可以进行对state值进行修改的地方,Vuex应该在这里对这些值的变化做了相关监听跟踪

// index.js中的公共仓库添加如下代码
mutations: {
    increment (state) {
        state.count++
    }
}
...
// index.vue的模板页做如下调整

index.vue中的值:{{co}}

... // index.vue中添加methods addCount () { this.$store.commit('increment'); // 通过commit调用mutations中的increment方法 }

这时点击页面就会发现在index.vue和header.vue中引用的count值一起发生了更改

// 通过commit调用mutations时,还可以传递参数
// 在index.js中的方法接收传入参数 
increment (state, payload) {
    state.count += payload.addNum;
}
...
// 在index.vue中的调用方法传递相关值
addCount () {
    this.$store.commit('increment', {addNum:100}); // 调用mutations
}
...
// 如果使用对象形式进行传值,还可以使用下面的简写方式
// commit方法自动识别type类型,所以也就不要使用type来传相关值
this.$store.commit({
    type: 'increment',
    addNum:100
})

在组件内通过commit触发mutations时,我们是使用字符串increment,这就隐含会有一些问题当项目比较大时,同时协助的人比较多时,页面中随便调用一个mutations可能是没人知道做什么用的

// 在项目目录建立mutation-types.js
export const INCREMENT = 'INCREMENT'; // 在index.js的公共仓库中针对count增加的mutations
...
// 在index.js引入这个文件
import { INCREMENT } from './mutation-types';
...
// 在store中的mutations做如下修改
mutations: {
    [INCREMENT] (state, payload) {
        state.count += payload.addNum;
    }
}
...
// 在index.vue中引入mutation-types.js,调用时也做相关修改
addCount () {
    this.$store.commit({
        type: INCREMENT,
        addNum:100
    })
}

这样在一个公共的地方管理mutations,如果注释写的清晰,项目人员可以很明晰知道各个mutations的作用。

同mapState,mapGetters,针对mutations,也有一个mapMutations

import { mapState, mapGetters, mapMutations } from 'vuex';
...
// 在methods中添加相关内容,mapMutations要用在methods中
// 如果不需要传值,直接使用this.INCREMENT();
methods: {
    ...mapMutations([INCREMENT]),
    addCount () {
        this.INCREMENT({addNum: 100});
    }

mutations必须是同步函数

看官方的解释似乎是因为使用回调函数后无法明确的监听到state的变化,所以针对mutations的函数只能是同步函数,针对这个问题Vuex设定了actions

actions

  • actions提交的是mutations,通过dispatch触发
  • actions支持异步函数
// 在index.js中的公共仓库添加如下代码
// context是一个与实例对象一致的对象,可以通过 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
actions: {
    increment (context, payload) {
        context.commit(INCREMENT, payload);
    }
} 
...
// 借用参数解构,代码可以做如下优化
// 获取state,getters可以一样使用参数解构
increment ({commit}, payload) {
    commit(INCREMENT, payload);
}
...
// 在index.vue的页面方法做如下改造,actionst通过dispatch触发
addCount () {
    this.$store.dispatch('increment', {addNum: 100});
}
...
// 与commit类似,也可以使用下面的简写方式
this.$store.dispatch({type: 'increment', addNum: 100});
...
// 可以使用mapActions简化代码
// 引入mapActions函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
...
// 在methods下做下面修改
...mapActions(['increment']),
addCount () {
    this.increment({addNum: 100});
}

在我的理解中,我认为actions是对mutations的增强,所以在实际使用中,更加习惯在actions中添加相关逻辑,并触发mutations来更新state

Modules

上面例子中我们都是在index.js的公共仓库中store来进行相关操作,可以预期到,当项目非常大时,把所有组件用到的state都放在一个stroe上会造成这个store过大,为了解决这个问题,可以把stroe按功能进行拆分多个小的stroe然后在组合到一起进行使用。

// 在index.js,新建两个对象userStore和schoolStroe用来假定是用来存放用户信息和学校信息
const userStore = {
    state: {
        name: 'lin ken from userStore',
        address: '深圳XX区XX街道',
    }
};

const schoolStore = {
    state: {
        department: '软件学院',
        classroom: 'C509'
    }
};
// 在Vuex.Store的modules下,使用modules添加刚刚新建的子仓库
const store = new Vuex.Store({
    modules: {
        userStore,
        schoolStore
    },
    state: {
        count: 0,
    },
    getters: {
        userInfo (state) {
            return `姓名:${state.name}; 住址:${state.address}`;
        }
    },
    mutations: {
        [INCREMENT] (state, payload) {
            state.count += payload.addNum;
        }
    },
    actions: {
        increment ({commit}, payload) {
            commit(INCREMENT, payload);
        }
    }
});
...
// 在index.vue的计算属性中做如下改造,引入新建的子仓库
userStoreInfo () {
    return `姓名:${this.userStore.name};地址:${this.userStore.address}`;
},
schoolStoreInfo () {
    return `学院:${this.schoolStore.department};班级:${this.schoolStore.classroom}`;
},
...mapState(['count', 'schoolStore', 'userStore'])

在实际项目中,我们可能会把子仓库放到不同的文件下进行管理

// 在项目目录下新建两个文件userStore.js和schoolStore.js
// userStore.js内容如下
export const userStore = {
    state: {
        name: 'lin ken from userStore',
        address: '深圳XX区XX街道',
    }
};
...
// schoolStore.js内容如下
export const schoolStore = {
    state: {
        department: '软件学院',
        classroom: 'C509'
    }
};
...
// 在index.js目录下引入刚刚新建的子仓库
import {userStore} from './userStore';
import {schoolStore} from './schoolStore';

通过这种拆分管理,可以很方便的把各个子仓库组合在一起,易于维护和使用。Vuex在实现子仓库时只会把state按模块名词进行划分,其他的getters、actions是会合并在一起使用不会按模块划分。

// userStore.js做如下改造
export const userStore = {
    state: {
        name: 'lin ken from userStore',
        address: '深圳XX区XX街道',
    },
    getters: {
        rootCount (state, getters, rootState) {
            return rootState.count;
        }
    },
    mutations: {
        changeName (state, payload) {
            state.name = payload.name;
        }
    },
    actions: {
        changeName ({commit}, payload) {
            commit('changeName', payload);
        }
    }
};
...
// index.vue的计算属性中改造如下
...mapGetters(['rootCount']),
userStoreInfo () {
    return `姓名:${this.userStore.name};地址:${this.userStore.address};from rootState:${this.rootCount}`;
}

...
// index.vue的方法改造如下
...mapActions(['increment', 'changeName']),
addCount () {
    this.changeName({name: 'Rede'});
    this.increment({addNum: 100});
}

通过上例子可以看出如果我们要调用子仓库的相关state需要添加仓库名:this.userStore.name/this.schoolStore.classroom,但是如果我们要调用子仓库的getters、mutations、actions时无须添加子仓库名,这时信息会合并在一起:
this.rootCount/this.changeName,这样会造成子仓库的getters,mutations,actions重名的情况

// 在schoolStore.js做如下修改
export const schoolStore = {
    state: {
        department: '软件学院',
        classroom: 'C509'
    },
    getters: {
        rootCount (state) {
            return state.classroom;
        }
    },
    mutations: {
        changeName (state, payload) {
            state.department = payload.name;
        }
    },
    actions: {
        changeName ({commit}, payload) {
              console.log('school store');
            commit('changeName', payload);
        }
    }
};
...
// 在userStore.js做如下修改
actions: {
    changeName ({commit}, payload) {
        console.log('user store');
        commit('changeName', payload);
    }
}

这时页面会提示rootCount是一个重复的getters关键字,但是针对重复的mutations和actions却无法检测出来,这个时候你点击页面的按钮会发现,我们在index.vue设定的要执行changeName的方法,本意上你可能想要执行userStore.js中actions,但因为无意间在schoolStore.js中也声明了一个同名actions,这样就造成了两个actions同时触发(通过控制台我们能验证这一点),类似这种没有明确报错,完全因为人为原因造成的无意错误,在开发过程中是极其难维护的,尤其是在多人协作的项目中,所以总是建议针对mutations和actions定义的名词(尤其是mutations,因为一般是在组件内直接触发actions,actions重名的可能性要比mutations小)要存放在一个独立的常量列表中,类似mutations-type.js。

除了把相关信息名独立声明外,还可以利用命名空间来解决这个问题,因为默认getters,mutations和actions这些值是绑定在全局声明中,这样可以方便调用。

// 在userStore.js中添加namespced为true,开启这个命名空间
export const userStore = {
    namespaced: true,
...
// 在index.vue页面要做如下调整
// 这样页面使用的this.rootCount就指向userStore下的rootCount
...mapGetters({
    'rootCount': 'userStore/rootCount'
}),  
...
// 如果不像使用对象的形式还是保持使用字符串数组,可以如此引用
...mapActions(['increment', 'userStore/changeName']),
addCount () {
    this['userStore/changeName']({name: 'Rede'});
    this.increment({addNum: 100});
}  

虽然this['userStore/changeName']使用起来并不是很方便,但是这样的用法可以明确的指出这个方法的所属,如果不想使用这么麻烦的写法,还可以指定命名空间:

// 把所有绑定都绑定到userStore这个子仓库下
...mapActions('userStore/', ['increment', 'changeName']),
addCount () {
    this.changeName({name: 'Rede'});
    this.increment({addNum: 100});
}

这样对changeName的调用就可以直接调用userStore下的内容,还可以借助createNamespacedHelpers来在全局自动添加

import { mapState, mapGetters, mapMutations } from 'vuex';
import { createNamespacedHelpers } from 'vuex';
const { mapActions } = createNamespacedHelpers('userStore/');
...
// 下面的代码和最开始没有加入命名空间时一致,看起来是改动量最小的
...mapActions(['increment', 'changeName']),
addCount () {
    this.changeName({name: 'Rede'});
    this.increment({addNum: 100});
}

不过因为这种添加是针对所有的绑定信息,所以increment也会指定到userStore这个子仓库中,如果该子仓库没有这个方法就会报错,如何使用这个内容需要好好考虑下,不过如果想一起都要,也可以借助对象的形式来指定扩展信息。

// 把头部信息的引用改回去
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
...
// 相关内容做如下改造
// 这样使用即保证了不会影响全局命名,又能指定特定方法调用指定子仓库,而且也方便随时切换
...mapActions({
    increment: 'increment',
    changeName: 'userStore/changeName'
}),
addCount () {
    this.changeName({name: 'Rede'});
    this.increment({addNum: 100});
}

另外也可以针对这个同名的changeName,我们可以根据文件名的不同,分别定义为

USER_CHANGENAME,
SCHOOL_CHANGENAME

如何使用,就看你怎么考虑了。

在子仓库中,如果要获取主仓库中的state,getters可以传rootState,rootGetters这个参数

// getters下,rootState是第三个参数,rootGetters是第四个参数
rootCount (state, getters, rootState, rootGetters) 
// actions中,可以直接使用参数解构的形式,设定这个值
changeName ({commit, rootState, rootGetters}, payload) 

动态创建子仓库

除了直接声明好使用的子仓库,还可以动态的生成

// 在index.vue通过registerModule创建一个子仓库
mounted () {
    this.$store.registerModule('myModule', {
        state: {
            info: 'from new module'
        }
    });
},
...
// 在相关方法中可以直接调用
addCount () {
    this.changeName({name: 'Rede'});
    this.increment({addNum: 100});
    this.info = this.$store.state.myModule.info;
    this.$store.unregisterModule('myModule');
    console.log(this.$store.state.myModule);
}

这种临时的子仓库在子组件之间用来临时通信是非常有用的,使用完成还可以调用unregisterModule直接删除,为了避免临时创建的子仓库影响到静态仓库,还可以在指定子仓库创建下一级仓库

mounted () {
    this.$store.registerModule('nested', {
        state: {
            info: 'nested'
        }
    });
    this.$store.registerModule(['nested', 'myModule'], {
        state: {
            info: 'from new module'
        }
    });
},
...
// 直接调用
console.log(this.$store.state.nested.info);
console.log(this.$store.state.nested.myModule.info);

动态仓库是临时创建的,碰到同名的会直接覆盖。

// 第二个myModule会覆盖第一个,myModule中的state只包含一个name值
mounted () {
    this.$store.registerModule('nested', {
        state: {
            info: 'nested'
        }
    });
    this.$store.registerModule(['nested', 'myModule'], {
        state: {
            info: 'from new module'
        }
    });
    this.$store.registerModule(['nested', 'myModule'], {
        state: {
            name: 'myModule'
        }
    });
},

如果希望这些动态创建的子仓库可以叠加,可以添加preserveState参数

// 这时创建的myModule的state就包含info和name两个值
this.$store.registerModule(['nested', 'myModule'], {
    state: {
        info: 'from new module'
    }
});
this.$store.registerModule(['nested', 'myModule'], {
    state: {
        name: 'myModule'
    }
}, { preserveState: true });

仓库的复用

如果我们对一个组件进行复用,

插件

Vuex支持自定义插件,通过插件可以监听状态的变化。

// 在index.js中新建一个插件
// store是Vuex插件的唯一参数
// subscribe是在mutations发生变化时调用
// subscribeAction是action发生变化时调用
const myPlugin = store => {
    // 当 store 初始化后调用
    store.subscribe((mutation, state) => {
        console.log('from myPlugin');
        console.log(mutation);
    });
    store.subscribeAction((action, state) => {
        console.log('from myPlugin');
        console.log(action);
    })
};
...
// 在初始化方法中通过plugins直接引用
const store = new Vuex.Store({
    ...
    plugins: [myPlugin]
});

针对input的优化

Vuex中会强制要求只通过mutations去修改state的值,如果我们把某个input的value值绑定为一个针对state的计算属性,那如何去修改对应在state中的值?

// 在index.vue的模板中输入如下内容

...
userStoreInfo () {
    return `${this.userStore.name}`;
},

这时如果我们尝试修改input中的内容,控制台会直接报错提示我们没有设置setter,解决方案就是设置set方法,并通过actions去修改这个对应的state

userStoreInfo: {
    get () {
        return `${this.userStore.name}`;
    },
    set (value) {
        this.changeName({name: value});
    }
},

官网上还提到另外一种绑定value去监听input方法,再更新的方案,感觉没这样直接
d

你可能感兴趣的:(Vue(六) Vuex)