构建vuex环境

3.3k 词

构建 vuex(多组件数据共享)环境#

基于脚手架创建项目,构建 vuex 多组件数据共享环境
以简易组件通信实例为例
036构建vuex环境_cut_1703164265486
效果是三个组件,共享一份数据,且:

  • 任意一个组件都可以修改数据
  • 三个组件的数据是同步的

创建一个空仓库#

  1. 安装 vuex

安装 v3 版本,vue/vue-router/vuex 的版本对应一般是 2/3/3 或 3/4/4

1
npm add vuex@3
  1. 新建 vuex 模块文件

新建store/index.js专门存放 vuex

  1. 创建仓库
1
2
3
Vue.use(Vuex);
// 创建仓库:
new Vuex.Store();
  1. main.js 导入挂载

仓库导入挂载完毕后所有组件都能访问到它

核心概念- state 状态#

提供数据#

state提供唯一的公共数据源,所有共享的数据都要统一放到Store中的state对象中存储
state对象中可以添加需要共享的数据

1
2
3
4
5
6
7
8
9
10
// 创建仓库(空仓库)
const store = new Vuex.Store({
// state状态,即数据,类似于vue组件中的data
// 区别:
// 1.data是组件自己的数据
// 2.state是所有组件共享的数据
state: {
count: 101,
},
});

使用数据#

  1. 通过 store 直接访问

获取store: 1. this.$store 2. import 导入 store
模板中:{{ $store.state.xxx }}
组件逻辑中:this.$store.state.xxx
JS模块中:store.state.xxx

  1. 通过辅助函数(简化)

辅助函数- mapState#

mapState是辅助函数,帮助我们将store中的数据自动映射到组件的计算属性
若直接将state中的数据定义在组件内的计算属性中:

1
2
<!-- 使用: -->
{{ count }}
1
2
3
4
5
computed:{
count(){
return this.$store.state.count
}
}

这样写,虽然在使用时不那么繁琐,但若数据较多时再这样写就太麻烦了
mapState就可以简化这一过程

  1. 导入mapState
1
import { mapState } from 'vuex';
  1. 数组方式引入state
1
mapState(['count']);
  1. 展开运算符映射

使用展开运算符...是为了不让 mapState 占据整个计算属性空间
数组中写要使用的共享的数据,以逗号隔开

1
2
3
computed: {
...mapState(['count','title'])
}

核心概念- mutations#

严格模式#

vuex 遵循单向数据流,组件中不能直接修改仓库中的数据
在实例中,要实现对数据count的修改,若使用以下写法是不规范的

1
2
// 错误写法
this.$store.state.count++;

这样写一般不会报错,但是这样写是不规范的,要想给出错误提示,可以通过strict:true开启严格模式
注意:严格模式,上线时需要移除,会消耗性能

1
2
3
4
5
6
7
8
9
10
// 创建仓库
const store = new Vuex.Store({
// 开启严格模式
strict: true,
state: {
count: 100,
title: 'xxxx',
// ...
},
});

mutations 修改数据#

state数据的修改只能通过mutations

  1. 仓库中定义mutations对象,对象中存放修改state的方法
1
2
3
4
5
6
7
8
9
10
11
12
const store = new Vuex.Store({
state: {
count: 100,
},
// 定义mutations
mutations: {
// 第一个参数是当前store的state属性
addCount(state) {
state.count += 1;
},
},
});
  1. 组件中提交调用mutations
1
this.$store.commit('addCount');

mutations 传参#

count加一写一个方法,若是加二加三…也再单独写一个方法吗,可以但是不建议
对于同样的操作,只需要传递不同的参数即可

1
this.$store.commit('方法名', 参数);
  1. 提供mutation函数(带参数-提交载荷 payload)
1
2
3
4
5
6
mutations:{
// ...
addCount(state,n){
state.count+=n
}
}
  1. 页面中提交调用 mutation
1
this.$store.commit('addCount', 10);

注意参数只能提交一个,若想要传递多个参数,可以包装成一个对象传递
eg:

1
2
3
4
5
this.$store.commit('addCount', {
count: 5,
msg: '加五',
// ...
});

练习- 实时输入,实时更新#

  1. 输入框内容渲染

注意遵循单向数据流,不能使用v-model绑定仓库的共享数据
使用:value绑定数据

  1. 监听输入获取内容

@input监听输入数据

1
<input :value="count" @input="handleIpt" type="text" />
  1. 封装mutation处理函数

App.vue:

1
2
3
4
5
6
7
8
methods: {
handleIpt (e) {
// 实时获取输入值
const newNum = +e.target.value
// 提交mutation
this.$store.commit('changeCount', newNum)
}
}
  1. 调用传参

store/index.js:

1
2
3
4
5
6
mutations: {
// ...
changeCount (state, newCount) {
state.count = newCount
}
}

辅助函数- mapMutations#

mapMutationsmapState很像,语法也类似
它是把位于mutations中的方法提取了出来,映射到组件methods
eg:
store/index.js:

1
2
3
4
5
mutations: {
subCount (state, num) {
state.count -= num
},
}

SonTwo.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { mapMutations } from 'vuex'
methods: {
// 映射
...mapMutations(['subCount']),
handleSub (num) {
// 调用
this.subCount(num)
}
}
// 相当于:
methods:{
subCount(n){
this.$store.commit('subCount',n)
}
}

甚至于可以不需要写handleSub事件处理函数,直接在按钮上绑定mutation处理函数
这样的处理极大简化了写法

1
2
3
<button @click="handleSub(5)">值-5</button>
<!-- 直接使用mutation处理函数 -->
<button @click="subCount(10)">值-10</button>

核心概念- actions#

actions处理异步操作
需求:一秒钟之后,修改statecount为 666
说明:mutations必须是同步的(便于检测数据变化,记录调试)

  1. 提供action方法
1
2
3
4
5
6
7
8
actions:{
setAsyncCount(context,num){
// 一秒后,给一个数,去修改num
setTimeout(()=>{
context.commit('changeCount',num)
},1000)
}
}
  1. 页面中dispatch调用
1
this.$store.dispatch('setAsyncCount', 666);

eg:
store/index.js:

1
2
3
4
5
6
7
8
9
10
11
// actions处理异步,注意,不能直接操作state,若要操作还是需要commit mutation
actions: {
// context 上下文(此处未分模块,可以当作store仓库来理解)
// context.commit('mutation方法',额外参数)
setAsyncCount (context, num) {
// 异步
setTimeout(() => {
context.commit('changeCount', num)
}, 1000)
}
}

SonOne.vue:

1
<button @click="changeAfterOneSec">一秒后count改为666</button>
1
2
3
4
5
6
7
8
methods: {
// ...
changeAfterOneSec () {
// 调用action
// this.$store.dispatch('action名字',额外参数)
this.$store.dispatch('setAsyncCount', 666)
}
}

当然,也可以手动传参

辅助函数- mapActions#

mapActions是把位于actions中的方法提取了出来,映射到组件methods
例:将上面的setAsyncCount方法映射到子组件一的methods

1
2
3
4
5
6
7
8
9
10
11
12
import { mapActions } from 'vuex';
methods:{
...mapActions(['setAsyncCount']),
// ...
changeAfterOneSec (num) {
// 调用action
// this.$store.dispatch('action名字',额外参数)
// this.$store.dispatch('setAsyncCount', num)
// 在使用辅助函数mapActions后即可简化写法为:
this.setAsyncCount(num)
}
}

核心概念- getters#

除了state之外,有时还需要从state派生出一些状态,这些状态是依赖state的(比如原本state中有数量和价格,就可以派生出一个新的状态:总价),此时会用到getters(类似于计算属性computed
例如:state中定义了 list,为 1-10 的数组,组件中,需要显示所有大于 5 的数据

1
2
3
state: {
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
}

定义getters#

1
2
3
4
5
6
7
8
getters: {
// 注意:
// 1)getters函数的第一个参数是state
// 2)getters函数必须要有返回值
filterList(state){
return state.list.filter(item=>item>5)
}
}

访问getters#

  1. 通过store访问
1
{{ $store.getters.filterList }}
  1. 通过辅助函数

辅助函数- mapGetters#

getters方法映射到组件computed

1
2
3
4
5
import { mapGetters } from 'vuex'

computed:{
...mapGetters(['filterList'])
}
1
{{ filterList }}

核心概念- 模块 mudule(进阶语法)#

模块的创建#

由于vuex使用单一状态树,应用的所有状态会集中到一个比较大的对象。
当应用变得非常复杂时,store对象就有可能变得相当臃肿。(当项目变得越来越大时,vuex 会变得越来越难以维护)
模块拆分,以用户模块为例:
user 模块:store/modules/user.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const state = {
userInfo: {
name: 'andy',
age: 18,
},
};
const mutations = {};
const actions = {};
const getters = {};
export default {
state,
mutations,
actions,
getters,
};

store/index.js:

1
2
3
4
5
6
7
import user from './modules/user';
const store = new Vuex.Store({
// ...
modules: {
user,
},
});

如此一来就会发现 vuex 上会挂载好这个模块
036构建vuex环境_cut_1703681398575
尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的state中,属性名就是模块名
036构建vuex环境_cut_1703682633466
从本质上看还是单一状态树,但从可维护的角度看,子模块已被拆分到不同的文件中,较原来可维护性高得多

模块中 state 的访问#

使用模块中 state 的数据:

  1. 直接通过模块名访问
1
2
3
$store.state.模块名.xxx;
// eg:
$store.state.user.userInfo.name;
  1. 通过mapState映射
  • 默认根级别的映射:mapState(['xxx'])
  • 子模块的映射:mapState('模块名',['xxx'])-需要开启命名空间
1
2
3
4
5
6
7
8
export default {
// 开启命名空间
namespaced: true,
state,
mutations,
actions,
getters,
};
1
mapState('user', ['userInfo']);

使用:

1
<div>{{ userInfo }}</div>

模块中 getters 的访问#

使用模块中getters的数据:

  1. 直接通过模块名访问
1
2
3
$store.getters['模块名/xxx'];
// eg:
$store.state.user.userInfo.name;
  1. 通过mapGetters映射
  • 默认根级别的映射:mapGetters(['xxx'])
  • 子模块的映射:mapGetters('模块名',['xxx'])-需要开启命名空间

模块中 mutation 的调用#

注意:默认模块中的 mutationactions 会被挂载到全局,需要开启命名空间,才会挂载到子模块
调用子模块中的 mutation

  1. 直接通过store调用:$store.commit('模块名/xxx',额外参数)
  2. 通过mapMutations映射
  • 默认根级别的映射:mapMutations(['xxx'])
  • 子模块的映射:mapMutations('模块名',['xxx'])-需要开启命名空间

模块中 action 的调用(类比 mutation)#

调用子模块中的 action

  1. 直接通过store调用:$store.dispatch('模块名/xxx',额外参数)
  2. 通过mapActions映射
  • 默认根级别的映射:mapActions(['xxx'])
  • 子模块的映射:mapActions('模块名',['xxx'])-需要开启命名空间

源码#

src/components/SonOne.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<template>
<div class="sonbox">
<h3>Son1子组件</h3>
<p>
从vuex中获取的值:
<label>{{ count }}</label>
</p>
<button @click="handleAdd(1)">值+1</button>
<button @click="handleAdd(5)">值+5</button>
<button @click="changeTitle">改小标题</button>
<button @click="changeAfterOneSec(666)">一秒后count改为666</button>
<hr />
<div>{{ $store.getters.filterList }}</div>
<hr />
<!-- 测试访问模块中的state -->
<!-- 原生 -->
<div>name:{{ $store.state.user.userInfo.name }}</div>
<button @click="updateUser">更新个人信息</button>
<button @click="updateUser2">一秒后更新信息</button>
<!-- 辅助函数 -->

<div>theme:{{ theme }}</div>
<button @click="updateTheme">更新主题</button>
<hr />
<!-- 测试访问模块中的getters -->
<!-- 原生 -->
<div>{{ $store.getters['user/UpperCaseName'] }}</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';

export default {
created() {
console.log(this.$store.getters);
},
computed: {
...mapState(['count']),
...mapState('user', ['userInfo']),
...mapState('setting', ['theme', 'desc']),
},
methods: {
...mapActions(['setAsyncCount']),
handleAdd(num) {
// 错误写法
// this.$store.state.count++
// console.log(this.$store.state.count++)
// 应通过mutations进行修改数据
this.$store.commit('addCount', num);
},
changeTitle() {
this.$store.commit('changeTitle');
},
changeAfterOneSec(num) {
// 调用action
// this.$store.dispatch('action名字',额外参数)
// this.$store.dispatch('setAsyncCount', num)
// 在使用辅助函数mapActions后即可简化写法为:
this.setAsyncCount(num);
},
updateUser() {
// $store.commit('模块名/mutation名',额外传参)
this.$store.commit('user/setUser', { name: 'Matt', age: 20 });
},
updateUser2() {
// 调用action dispatch
this.$store.dispatch('user/setUserSecond', {
name: 'John',
age: 28,
});
},
updateTheme() {
this.$store.commit('setting/setTheme', 'dark');
},
},
};
</script>
<style lang="less" scoped>
.sonbox {
padding: 10px;
margin: 10px 0;
width: 80%;
border: 1px solid #000;
}
</style>

src/components/SonTwo.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<template>
<div class="sonbox">
<h3>Son2子组件</h3>
<p>
从vuex中获取的值:
<label>{{ count }}</label>
</p>
<button @click="handleSub(1)">值-1</button>
<button @click="handleSub(5)">值-5</button>
<button @click="subCount(10)">值-10</button>
<button @click="changeTitle('SonTwo组件修改的标题')">改标题</button>
<hr />
<div>{{ filterList }}</div>
<hr />
<!-- 访问模块中的state -->
<div>{{ user.userInfo.name }}</div>
<div>{{ setting.theme }}</div>
<!-- 访问模块中的state -->
<div>user模块的数据:{{ userInfo }}</div>
<button @click="setUser({name:'lily',age:17})">更新个人信息</button>
<button @click="setUserSecond({name:'Tommy',age:15})">一秒后更新个人信息</button>
<div>setting模块的数据:{{ theme }}-{{ desc }}</div>
<button @click="setTheme('pink')">更新主题</button>
<hr />
<!-- 访问模块中的getters -->
<div>{{ UpperCaseName }}</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';

export default {
computed: {
...mapState(['count', 'user', 'setting']),
...mapState('user', ['userInfo']),
...mapState('setting', ['theme', 'desc']),
...mapGetters(['filterList']),
...mapGetters('user', ['UpperCaseName']),
},
methods: {
// 全局级别的映射
...mapMutations(['subCount', 'changeTitle']),

// 分模块的映射
...mapMutations('setting', ['setTheme']),
...mapMutations('user', ['setUser']),
...mapActions('user', ['setUserSecond']),
handleSub(num) {
this.subCount(num);
},
},
};
</script>
<style lang="less" scoped>
.sonbox {
padding: 10px;
margin: 10px 0;
width: 80%;
border: 1px solid #000;
}
</style>

src/router/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const routes = [];

const router = new VueRouter({
routes,
});

export default router;

src/store/index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 存放vuex的核心代码
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';
import setting from './modules/setting';

// 插件安装
Vue.use(Vuex);

// 创建仓库(空仓库)
const store = new Vuex.Store({
// 严格模式,上线时需要移除,会消耗性能
strict: true,
// state状态,即数据,类似于vue组件中的data
// 区别:
// 1.data是组件自己的数据
// 2.state是所有组件共享的数据
state: {
count: 100,
title: '小标题',
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
// 通过mutations提供修改数据的方法
mutations: {
// 所有mutation函数,第一个参数都是state
addCount(state, num) {
state.count += num;
},
subCount(state, num) {
state.count -= num;
},
changeTitle(state, newTitle) {
state.title = newTitle || '更改后的小标题';
},
changeCount(state, newCount) {
state.count = newCount;
},
},
// actions处理异步,注意,不能直接操作state,若要操作还是需要commit mutation
actions: {
// context 上下文(此处未分模块,可以当作store仓库来理解)
// context.commit('mutation方法',额外参数)
setAsyncCount(context, num) {
// 异步
setTimeout(() => {
context.commit('changeCount', num);
}, 1000);
},
},
// getters类似于计算属性
getters: {
filterList(state) {
return state.list.filter(item => item > 5);
},
},
modules: {
user,
setting,
},
});

// 导出给main.js使用
export default store;

src/store/module/setting.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const state = {
theme: 'light',
desc: '测试demo',
};
const mutations = {
setTheme(state, newTheme) {
state.theme = newTheme;
},
};
const actions = {};
const getters = {};
export default {
namespaced: true,
state,
mutations,
actions,
getters,
};

src/store/module/user.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const state = {
userInfo: {
name: 'andy',
age: 18,
},
score: 66,
};
const mutations = {
setUser(state, newUserInfo) {
state.userInfo = newUserInfo;
},
};
const actions = {
setUserSecond(context, newUserInfo) {
// 将异步在action中进行封装
setTimeout(() => {
// 调用mutation context上下文,默认提交的是自己模块的action和mutation
context.commit('setUser', newUserInfo);
}, 1000);
},
};
const getters = {
// 分模块后,state指代子模块的state
UpperCaseName(state) {
return state.userInfo.name.toUpperCase();
},
};
export default {
namespaced: true,
state,
mutations,
actions,
getters,
};

src/App.vue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div class="main">
<h2>根组件{{ count }}</h2>
<h3>{{ title }}</h3>
<input :value="count" @input="handleIpt" type="text" />
<SonOne></SonOne>
<SonTwo></SonTwo>
</div>
</template>
<script>
import SonOne from './components/SonOne.vue';
import SonTwo from './components/SonTwo.vue';
import { mapState } from 'vuex';
export default {
data() {
return {};
},
components: {
SonOne,
SonTwo,
},
created() {
console.log(this.$store);
console.log(this.$store.state.count);
},
computed: {
...mapState(['count', 'title']),
},
methods: {
handleIpt(e) {
// 实时获取输入值
const newNum = +e.target.value;
// 提交mutation
this.$store.commit('changeCount', newNum);
},
},
};
</script>
<style lang="less">
.main {
padding: 10px;
border: 1px solid #000;
}
</style>

src/main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from '@/store/index';
console.log(store.state.count);

Vue.config.productionTip = false;

new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');