小程序实现全局状态管理
# 前言
全局状态管理一直是前端项目中一个比较重要的模块,传统的前端框架基本都提供了全局状态管理的方案,如:
vuex
,redux
等。那我们在开发原生小程序的时候该如何实现全局状态管理呢?下面将以微信小程序为例,实现一个全局状态管理功能
# App.globalData
一说到小程序的全局状态,大家肯定会想到App.globalData
,这是官方提供的一个全局状态,我们在使用的过程中只需要调用对应的api
拿到app
对象就可以获取
const app = getApp()
console.log(app.globalData,'全局状态') // 输出 {}
app.globalData.name = 'xxx'
console.log(app.globalData.name,'更新全局状态') // 输出xxx
2
3
4
但是这个状态有一个弊端,我们都知道小程序必须是调用了setData
才会触发页面更新,那么意味着挂载在app
上的全局状态我们不能直接在页面中渲染,因为他并不会触发页面的更新
要想实现一个可以在任意地方使用,并且值发生变化后还会触发页面更新的全局状态,我们还是得另辟蹊径
# behaviors
不得不说,小程序的文档确实藏得够深,官方其实还是提供了很多好用的东西的,只不过文档太晦涩了。Behaviors (opens new window)
是官方提供的混入器,其用法类似于vue2
中的mixins
。可以用于将一些属性,方法,生命周期混入到使用者上,以此来实现逻辑,状态的抽离和复用。
而我们的全局状态刚好就可以借助这个工具来完成,将全局状态混入到页面自身,然后在更新值得时候调用setData
,就可以完美的触发页面更新。
# behaviorsStore
首先我们创建一个behaviors
,然后添加上最基本的属性
export default Behavior({
data: {
globalData: {}
},
lifetimes:{
},
pageLifetimes:{
},
methods:{
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到,他的语法和组件的js
文件简直一模一样。在组件中直接引入这个Behavior
就可以把Behavior
上的属性都混入进组件自身了
import behaviorsStore from '../../../../storePro/behaviorsStore'
Component({
behaviors:[behaviorsStore],
...
})
2
3
4
5
6
怎么样,是不是使用起来非常的简单。下面我们去完善一下Store
的功能
# 实现
首先实现一下stor
e的setData
功能:其实简单来说,我们只需要直接调用setData
然后传入globalData
的新值就可以更新状态了,像下面这样
this.setData({
globalData:{
...
}
})
2
3
4
5
但是这样调用显然不太合理,因为globalData
上我们会挂载很多属性,这样直接设置值的话会覆盖掉其他的值,除非每次设置新状态时我们都把不需要设置的状态都给他加上,例如globalData
里有name
和age
两个属性,如果我只想设置name
属性,那么我就需要把age
的旧属性也带上,如下
this.setData({
globalData:{
name:'新值',
age:this.data.globalData.name
}
})
2
3
4
5
6
如果有更多的值,那将会非常麻烦,所以我们需要封装一个函数,来专门更新全局状态的值
// setGlobalData 实现,主要内容为将 globalDataStore 的内容设置进页面的 data 的 globalData 属性中。
setGlobalData(obj) {
obj = obj || {};
let outObj = Object.keys(obj).reduce((sum, key) => {
let _key = "globalData." + key;
sum[_key] = obj[key];
return sum;
}, {});
this.setData(outObj);
}
2
3
4
5
6
7
8
9
10
在这个函数中,我们将旧值混合新值一起重新赋值给globalData
,使用方式如下
// 设置单个值
this.setGlobalData({
name:'新值'
})
// 同时设置多个值
this.setGlobalData({
name:'新值',
age:'18'
})
2
3
4
5
6
7
8
9
ok,接下来我们需要实现的就是跨页面使用的时候如何让状态同步。简单来说就是目前我们的behaviors
初始化时data
里是一个空的globalData
对象,当我们在A
页面对这个globalData
进行了某些操作后,在B
页面引入behaviors
其实拿到的并不是被A
页面处理过的,而是一个全新的。也就是说这个时候globalData
还是一个空对象。
所以我们需要在对globalData
进行操作的同时,把他的值保存一份出来,然后在初始化behaviors
的时候又给赋值上去
// globalDataStore 用来全局记录 globalData,为了跨页面同步 globalData 用
export let globalDataStore = {};
export default Behavior({
data: {
globalData: {}
},
lifetimes:{
attached() {
// 同步 globalDataStore 的内容
this.setData({
globalData: Object.assign(
{},
this.data.globalData || {},
globalDataStore
)
});
}
},
pageLifetimes:{
},
methods:{
setGlobalData(obj) {
obj = obj || {};
let outObj = Object.keys(obj).reduce((sum, key) => {
let _key = "globalData." + key;
sum[_key] = obj[key];
return sum;
}, {});
this.setData(outObj, () => {
globalDataStore = this.data.globalData;
});
},
}
})
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
上述代码中主要做了以下几点:
- 创建一个全局变量
globalDataStore
用来同步globalData
- 在
setData
的时候更新globalDataStore
- 在组件的生命周期
attached
中将globalDataStore
合并进globalData
解决了状态同步的问题,那么接下来还需要考虑一个问题,attached
生命周期是组件被创建时执行的钩子,如果我在B
页面设置了新状态,然后回到A
页面,这时A
页面的组件是不会再触发attached
的,那此时该如何去同步最新状态到A
页面呢?
细心的小伙伴肯定已经想到了,既然attached
只会执行一次,那我把同步状态的代码放在一个每次切换页面都会执行的钩子里不就行了嘛。
// globalDataStore 用来全局记录 globalData,为了跨页面同步 globalData 用
export let globalDataStore = {};
export default Behavior({
data: {
globalData: {}
},
lifetimes:{
attached() {
// 同步 globalDataStore 的内容
this.setData({
globalData: Object.assign(
{},
this.data.globalData || {},
globalDataStore
)
});
}
},
pageLifetimes:{
show() {
// 同步 globalData
this.setGlobalData(Object.assign({}, globalDataStore));
}
},
methods:{
...
}
})
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
对此我们还可以再进行一次性能上的优化,如果B
页面并没有对全局状态做变动,那么A
页面就不需要去同步状态了,毕竟多调用一次setData
就会多造成一次性能开销。所以这里我们引入了一个新的变量,来标识setData
调用了多少次,再onshow
里判断如果次数发生了变化,那么就需要重新设置装填
// globalDataStore 用来全局记录 globalData,为了跨页面同步 globalData 用
export let globalDataStore = {};
// setGlobalCount 用来全局记录 setGlobalData 的调用次数,为了在 B 页面回到 A 页面的时候,
// 检查页面 __setGlobalDataCount 和 setGlobalCount 是否一致来判断在 B 页面是否有 setGlobalData,
// 以此来同步 globalData
let setGlobalCount = 0;
export default Behavior({
data: {
globalData: {}
},
lifetimes:{
attached() {
// 页面 onLoad 的时候同步一下 setGlobalCount
this.__setGlobalDataCount = setGlobalCount;
// 同步 globalDataStore 的内容
this.setData({
globalData: Object.assign(
{},
this.data.globalData || {},
globalDataStore
)
});
}
},
pageLifetimes:{
show() {
// 为了在 B 页面回到 A 页面的时候,检查页面 __setGlobalDataCount 和 setGlobalCount 是否一致来判断在 B 页面是否有 setGlobalData
if (this.__setGlobalDataCount != setGlobalCount) {
// 同步 globalData
this.__setGlobalDataCount = setGlobalCount;
this.setGlobalData(Object.assign({}, globalDataStore));
}
}
},
methods:{
setGlobalData(obj){
setGlobalCount = setGlobalCount + 1;
this.__setGlobalDataCount = this.__setGlobalDataCount + 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
至此我们的全局状态功能就已经非常完善了,在页面中只需要直接渲染```globalData.xxx就可以了。
vuex``提供了持久化的功能,原理就是把状态同步存储在本地缓存中,下次初始化的时候判断缓存中是否存在,存在的话则直接赋值上去。这个功能还是比较实用的,下面就一起来实现以下
// 获取本地的 gloabalData 缓存
try {
const gloabalData = wx.getStorageSync("gloabalData");
// 有缓存的时候加上
if (gloabalData) {
globalDataStore = { ...gloabalData };
}
} catch (error) {
console.error("gloabalData getStorageSync error", "e =", error);
}
methods:{
// setGlobalDataAndStorage 实现,先调用 setGlobalData,然后存到 storage 里
setGlobalDataAndStorage(obj,cb=null) {
this.setGlobalData(obj,cb);
try {
let gloabalData = wx.getStorageSync("gloabalData");
// 有缓存的时候加上
if (gloabalData) {
gloabalData = { ...gloabalData, ...obj };
} else {
gloabalData = { ...obj };
}
wx.setStorageSync("gloabalData", gloabalData);
} catch (e) {
console.error("gloabalData setStorageSync error", "e =", e);
}
},
}
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
这样一来,每次初始化的时候都会带上缓存里的值了,现在我们的全局状态使用方法如下
<!--模板中使用-->
<view>{{gloabalData.name}}</view>
2
// 更新状态
this.setGlobalData({
name:'新名字'
})
// 更新状态并持久化
this.setGlobalDataAndStorage({
name:'新名字'
})
2
3
4
5
6
7
8
9
虽然这样用起来已经没什么毛病了,但是并不太好区分哪些是全局状态里的哪些是组件自身的,渲染状态的时候还可以从globalData
看出来,但是在调用全局方法的时候就根本分不清了,所以我们还可以进行一点小优化,将方法的调用也区分开来。
在全局状态里新增一个函数用来派发全局状态的事件
const fun_name_map = {
'setData':'setGlobalData',
'setDataStorage':'setGlobalDataAndStorage'
}
// 调用全局事件
$dispatch(fun_name = '',payload,cb){
if(!fun_name){
console.error("gloabalData dispatch error", "fun_name is undefined", );
return
}
const f = fun_name_map[fun_name]||fun_name
if(!f){
console.error("gloabalData dispatch error", `${fun_name} is not defined`, );
return
}
this[f](payload,cb)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用
this.$dispatch(
'setData',
{name:'新名字'},
()=>{
console.log('setData的回调')
})
2
3
4
5
6
这样用起来就非常方便了。下面是完整的代码
// globalDataStore 用来全局记录 globalData,为了跨页面同步 globalData 用
export let globalDataStore = {};
// 获取本地的 gloabalData 缓存
try {
const gloabalData = wx.getStorageSync("gloabalData");
// 有缓存的时候加上
if (gloabalData) {
globalDataStore = { ...gloabalData };
}
} catch (error) {
console.error("gloabalData getStorageSync error", "e =", error);
}
// setGlobalCount 用来全局记录 setGlobalData 的调用次数,为了在 B 页面回到 A 页面的时候,
// 检查页面 __setGlobalDataCount 和 setGlobalCount 是否一致来判断在 B 页面是否有 setGlobalData,
// 以此来同步 globalData
let setGlobalCount = 0;
const fun_name_map = {
'setData':'setGlobalData',
'setDataStorage':'setGlobalDataAndStorage'
}
const baseState = {
name:'全局name'
}
export default Behavior({
data: {
globalData: Object.assign({
...baseState
}, globalDataStore)
},
lifetimes: {
attached() {
// 页面 onLoad 的时候同步一下 setGlobalCount
this.__setGlobalDataCount = setGlobalCount;
// 同步 globalDataStore 的内容
this.setData({
globalData: Object.assign(
{},
this.data.globalData || {},
globalDataStore
)
});
}
},
pageLifetimes: {
show() {
// 为了在 B 页面回到 A 页面的时候,检查页面 __setGlobalDataCount 和 setGlobalCount 是否一致来判断在 B 页面是否有 setGlobalData
if (this.__setGlobalDataCount != setGlobalCount) {
// 同步 globalData
this.__setGlobalDataCount = setGlobalCount;
this.setGlobalData(Object.assign({}, globalDataStore));
}
}
},
methods: {
// setGlobalData 实现,主要内容为将 globalDataStore 的内容设置进页面的 data 的 globalData 属性中。
setGlobalData(obj,cb=null) {
setGlobalCount = setGlobalCount + 1;
this.__setGlobalDataCount = this.__setGlobalDataCount + 1;
obj = obj || {};
let outObj = Object.keys(obj).reduce((sum, key) => {
let _key = "globalData." + key;
sum[_key] = obj[key];
return sum;
}, {});
this.setData(outObj, () => {
globalDataStore = this.data.globalData;
cb&&cb()
});
},
// setGlobalDataAndStorage 实现,先调用 setGlobalData,然后存到 storage 里
setGlobalDataAndStorage(obj,cb=null) {
this.setGlobalData(obj,cb);
try {
let gloabalData = wx.getStorageSync("gloabalData");
// 有缓存的时候加上
if (gloabalData) {
gloabalData = { ...gloabalData, ...obj };
} else {
gloabalData = { ...obj };
}
wx.setStorageSync("gloabalData", gloabalData);
} catch (e) {
console.error("gloabalData setStorageSync error", "e =", e);
}
},
// 调用全局事件
$dispatch(fun_name = '',payload,cb){
if(!fun_name){
console.error("gloabalData dispatch error", "fun_name is undefined", );
return
}
const f = fun_name_map[fun_name]||fun_name
if(!f){
console.error("gloabalData dispatch error", `${fun_name} is not defined`, );
return
}
this[f](payload,cb)
}
}
});
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# Tips
页面如何使用 ``Behavior``
Component
是Page
的超集,因此可以使用Component
构造器构造页面。
看看官方文档 (opens new window) :事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用Component
构造器构造,拥有与普通组件一样的定义段与实例方法。
但此时要求对应json
文件中包含usingComponents
定义段。
# 结语
本文我们通过混入器实现了一个全局状态管理,
Behavior
非常的强大,还可以通过它来实现很多很实用的功能,例如自动打日志,计算属性,监听等等。大家可以自行探索