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组件
<p>index.vue中的值:{{count}}</p>
<cus-header></cus-header>
...
// 在header.vue组件也一样添加相关计算属性,并修改模板中的内容
 <div>
    <p>header.vue中的count值:{{count}}</p>
</div>
...
// 这时页面就渲染为
<p>index.vue中的值:0</p> 
<div>
    <p>header.vue中的count值:0</p>
</div>

通过上例我们可以看到在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模板添加如下内容
<p>{{userInfo}}</p>

与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'
})
...
// 模板中可以使用
<p>{{cusInfo}}</p>

mutation

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

// index.js中的公共仓库添加如下代码
mutations: {
    increment (state) {
        state.count++
    }
}
...
// index.vue的模板页做如下调整
<p>index.vue中的值:{{co}}</p>
<cus-header></cus-header>
<button @click="addCount">addCount</button>
...
// 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的模板中输入如下内容
<input v-model="userStoreInfo"/>
...
userStoreInfo () {
    return `${this.userStore.name}`;
},

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • 安装 npm npm install vuex --save 在一个模块化的打包系统中,您必须显式地通过Vue.u...
    萧玄辞阅读 2,901评论 0 7
  • vuex 场景重现:一个用户在注册页面注册了手机号码,跳转到登录页面也想拿到这个手机号码,你可以通过vue的组件化...
    sunny519111阅读 7,971评论 4 111
  • Vuex是什么? Vuex 是一个专为 Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件...
    萧玄辞阅读 3,070评论 0 6
  • vuex是什么鬼? 如果你用过redux就能很快的理解vuex是个什么鬼东西了。他是vuejs用来管理状态的插件。...
    麦子_FE阅读 6,819评论 3 37
  • 系列文章:Vue 2.0 升(cai)级(keng)之旅Vuex — The core of Vue applic...
    6ed7563919d4阅读 4,488评论 2 58