vuex为小程序增加状态管理够用版

背景

主要是针对小程序开发中页面之间状态共享的问题,在涉及复杂的场景中比如用户身份状态、登录状态、支付情况等全局管理。

鉴于本人对Vue爱不释手,实现类似Vuex够用版。

遇到的问题

在使用百度小程序的 swan.navigateBack 进行回跳页面时,API中的方法参数不支持携带参数,只支持number参数。

所以就涉及了几个单独页面之间的通信问题。

传送门 swan.navigateBack

解决方法

主要有以下4种方法,实现各page之间通信。

解决方法一:利用app.js globalData,设置公共变量

利用app.js的公共特性,将变量挂在APP上。

1
2
3
4
5
6
7
8
9
// app.js 启动文件
App({
globalData: {
isLogin: false,
userInfo: null,
networkError: false,
networkType: 'none'
}
})

在其他页面Page上使用时,使用:

1
2
3
// test.js
const app = getApp();
const commonParams = app.globalData.isLogin;

但是存在的缺点也十分明显,当数据量比较大、数据关系比较复杂时,维护会比较复杂,逻辑会很混乱。

解决方法二:利用storage

利用小程序的全局storage,对数据进行存取,原理类似于解决方案一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 存储-异步
swan.setStorage({
key: 'key',
data: 'value'
});

// 存储-同步
swan.setStorageSync('key', 'value');

// 获取-异步
swan.getStorage({
key: 'key',
success: function (res) {
console.log(res.data);
},
fail: function (err) {
console.log('错误码:' + err.errCode);
console.log('错误信息:' + err.errMsg);
}
});

// 获取-同步
const result = swan.getStorageSync('key');

解决方法三: 利用事件总线EventBus

利用事件中心的进行订阅和发布。

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
// event.js 事件中心

class Event {
on(event, fn, ctx) {
if (typeof fn !== 'function') {
console.error('fn must be a function');
return;
}

this._stores = this._stores || {};
(this._stores[event] = this._stores[event] || []).push({
cb: fn,
ctx: ctx
});
}
emit(event, ...args) {
this._stores = this._stores || {};
let store = this._stores[event];
if (store) {
store = store.slice(0);
for (let i = 0, len = store.length; i < len; i++) {
store[i].cb.apply(store[i].ctx, args);
}
}
}
off(event, fn) {
this._stores = this._stores || {};
// all
if (!arguments.length) {
this._stores = {};
return;
}
// specific event
let store = this._stores[event];
if (!store) {
return;
}
// remove all handlers
if (arguments.length === 1) {
delete this._stores[event];
return;
}
// remove specific handler
let cb;
for (let i = 0, len = store.length; i < len; i++) {
cb = store[i].cb;
if (cb === fn) {
store.splice(i, 1);
break;
}
}
return;
}
}

module.exports = Event;

在app.js中进行声明和管理

1
2
3
4
5
6
// app.js
import Event from './utils/event';

App({
event: new Event()
})

订阅的页面中,使用on方法进行订阅

1
2
3
4
5
6
7
8
9
10
11
12
// view.js 阅读页进行订阅

Page({
// 页面在回退时,会调用onShow方法
onShow() {
// 支付成功的回调,调起下载弹层
app.event.on('afterPaySuccess', this.afterPaySuccess, this);
},
afterPaySuccess(e) {
// ....业务逻辑
}
})

发布的页面中,根据业务情况进行发布emit

1
2
3
4
5
6
7
8
// paySuccess.js

const app = getApp();

app.event.emit('afterPaySuccess', {
docId: this.data.tradeInfo.docId,
triggerFrom: 'docCashierBack'
});

根据事件中心的发布和订阅,实现了页面之间的通信,就能实现比如页面在支付成功后回退时,页面状态的改变的场景,同时利于维护页面之间的数据关系,能通过在发布时传递参数,实现数据之间的通信。

解决方法四:easy-store

小程序目录:

1
2
3
4
5
6
7
8
├─images
│ └─img
│ └─v
├─pages
│ ├─index
│ └─other
├─typings
└─vuex

easy-store

API

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
app.store.install(this);    // 注册使用 当前页面连接vuex

console.log(this.data.$state.counter);
console.log(app.store.getter); // 获取getter

app.store.commit('count', 1); // 触发同步事件
app.store.dispatch('countAsync', 1).then(() => { // 触发异步事件
app.store.setState({
counter: 955
});
});

app.store.replaceState({ // 替换store
counter: 955
});

app.postMessage('other', { // 跨页面通信,事件总线精简版
type: 'msg',
data: 'message from page1'
});

var subscribe = app.store.subscribe((type, state) => { // 订阅commit日志打印
console.log(type, state);
});

subscribe(); // 取消subscribe 订阅

模板取值

1
2
3
4
5
state和getter前用$

<view>{{$state.counter}}</view>
<view>{{$state.arr}}</view>
<view>{{$getter.arrLength}}</view>

实现方式

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/*
* 类似vuex中的接口进行状态管理
* 利用postMessage和onMessage的方式进行跨页面间通信,类似浏览器的postMessage
*/

function getShortRoute(route) {
return route.match(/\/(.+)\//)[1];
}

function genericSubscribe(fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn);
}
return () => { // 闭包,取消订阅
const i = subs.indexOf(fn);
if (i > -1) {
subs.splice(i, 1);
}
}
}

export default class Store {
constructor(config) {
this.state = config.state || {};
this.mutations = config.mutations || {};
this.actions = config.actions || {};
this.getters = config.getters || {};

this._pages = [];
this._messages = [];
this._subscribers = [];
this.$getter = {};
}

get getter() {
return this.$getter;
}

set getter(v) {
throw new Error('getter is read-only');
}

install(page) { // let index = this._pages.indexOf(page) 无效
page._shortRoute = getShortRoute(page.route);
let index = this._pages.findIndex((item) => {
return item.route === page.route;
});
if (index > -1) {
this._pages.splice(index, 1);
}
this._pages.unshift(page);
this.setState();
}

uninstall(page) {
let index = this._pages.findIndex((item) => {
return item.route === page.route;
});
if (index > -1) {
this._pages.splice(index, 1);
}
}

replaceState(data) {
this.setState(data);
}

setState(data) {
if (typeof data === 'object') {
Object.assign(this.state, data);
}
if (Object.keys(this.getters).length > 0) {
Object.keys(this.getters).map((item, index) => {
this.$getter[item] = this.getters[item](this.state);
});
}
this._pages.forEach(page => {
page.setData({
$state: this.state,
$getter: this.$getter
});
});
}

commit(type, payload) {
let mutation = this.mutations[type];
let result = mutation && mutation(this.state, payload);
this.setState();
// 触发订阅
this._subscribers.forEach(sub => sub(type, this.state));
return result;
}

dispatch(type, payload) {
let action = this.actions[type];
return action && action(this, payload);
}

// 若设置lazy, 则在页面显示时才会运行onMessage钩子
postMessage(routes, data, lazy) {
let routeAlive = false;
if (!Array.isArray(routes)) {
routes = [routes];
}
routes.forEach(route => {
if (!lazy) {
routeAlive = false;
this._pages.forEach(page => {
if (page._shortRoute === route) {
routeAlive = true;
page.onMessage && page.onMessage(data);
}
});
}
// lazy或页面不存在则存入消息队列
if (!routeAlive || lazy) {
this._messages.push({
route,
data
});
}
});
}

_messageQueue(page) {
if (!this._messages.some(msg => msg.route === page._shortRoute)) return;
let messages = [];
this._messages.forEach(msg => {
if (msg.route === page._shortRoute) {
page.onMessage && page.onMessage(msg.data);
} else {
messages.push(msg);
}
});
this._messages = messages;
}

update(page) {
// onShow时读取缓存的消息
this._messageQueue(page);
}

subscribe(fn) {
return genericSubscribe(fn, this._subscribers)
}
}

全局注册:

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
import Store from './vuex/index';

const store = new Store({
state: {
counter: 0,
num: 110
},
mutations: {
count(state, payload) {
return state.counter += payload;
},
count1(state, payload) {
return state.num += payload;
},
},
actions: {
countAsync(store, payload) {
return new Promise(resolve => {
setTimeout(() => {
store.commit('count1', payload);
resolve();
}, 1000);
});
},
}
});

App({
store,
// 简化postMessage调用
postMessage: store.postMessage.bind(store),
globalData: {
name: '百度小程序',
version: 12.01,
mob: 'ios'
},
onLaunch(options) {
// console.log(options);
console.log(this);
if (swan.canIUse('showFavoriteGuide')) {
swan.showFavoriteGuide({
type: 'bar',
content: '一键添加到我的小程序',
success(res) {
console.log('添加成功:', res);
},
fail(err) {
console.log('添加失败:', err);
}
});
}
},
onShow(options) {
// console.log(1);
},
onHide() {
// console.log(222);
}
});

indnx页面注册使用

1
2
3
4
5
6
7
8
9
10
11
const app = getApp()
onLoad() {
app.store.install(this); // 注册使用
},


....


<view>{{$state.counter}}</view>
<view>{{$state.num}}</view>
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
const app = getApp()
const My = require('my.js');

Page({
data: {
number: 0,
name: 'SWAN',
userInfo: {},
hasUserInfo: false,
canIUse: swan.canIUse('button.open-type.getUserInfo'),
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
flag: true,
person: { name: 'Lebron James', pos: 'SF', age: 33 },
teams: ['Cleveland Cavaliers', 'Miami Heat', 'Los Angeles Lakers'],
tag: 'basketball'
},
onLoad() {
app.store.install(this); // 注册使用
console.log(this.data);
console.log(app);
// 监听页面加载的生命周期函数
// console.log(getCurrentPages()); // [{...}]
// console.log(My.B(100, 9));
},
loadMore(e) {
// console.log(this.data.$state.counter);

app.store.commit('count', 1);

app.store.dispatch('countAsync', 5);

// app.store.setState({
// counter: 955
// });

// setTimeout(() => {
// app.postMessage('other', {
// type: 'msg',
// data: 'message from page1'
// });
// }, 2000);



return;
console.log(this);
console.log('加载更多被点击');
console.log(app);
console.log(this.getData('items'));
this.setData({
items: [...this.data.items, 123],
flag: false
}, () => {
console.log(swan);
this.doSth();
swan.showToast({
title: '我是标题'
});
})
},
onTabItemTap(item) {
// console.log(item.index);
// console.log(item.pagePath);
// console.log(item.text);
},
onPullDownRefresh() {
console.log(122);
setTimeout(() => {
swan.stopPullDownRefresh(300);
}, 2000)
},
onShareAppMessage(res) {
if (res.from === 'button') {
console.log(res.target); // 来自页面内转发按钮
}
return {
title: '智能小程序示例',
content: '世界很复杂,百度更懂你',
path: '/pages/openShare/openShare?key=value'
};
},
getUserInfo(e) {
swan.login({
success: () => {
swan.getUserInfo({
success: (res) => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
});
},
fail: () => {
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
});
}
});
},
fail: () => {
swan.showModal({
title: '未登录',
showCancel: false
});
}
});
},
doSth() {
this.setData({ number: 1 }) // 直接在当前同步流程中执行

swan.nextTick(() => {
this.setData({ number: 3 }) // 在当前同步流程结束后,下一个时间片执行
})

this.setData({ number: 2 }) // 直接在当前同步流程中执行
}
})

其他页面类似

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
const app = getApp();
Page({
onMessage(data) {
console.log(data);
},
onLoad() {
app.store.install(this);
console.log(this.data.$state.counter);

return;
swan.getSystemInfo({
success: res => {
// 更新数据
console.log(res);
},
fail: err => {
swan.showToast({
title: '获取失败'
});
}
});
},
previewImage() {
swan.previewImage({
current: 'https://test.com/1-1.png', // 当前显示图片的http链接
urls: ['https://test.com/1-1.png', 'https://test.com/1-2.png'], // 需要预览的图片http链接列表
success: function (res) {
console.log('previewImage success', res);
},
fail: function (err) {
console.log('previewImage fail', err);
}
});
},
previewOriginImage() {
swan.previewImage({
urls: ['https://test.com/img/swan-preview-image.jpg',
'https://test.com/img/swan-preview-image-2.png'
], // 需要预览的图片http链接列表
images: [
{
"url": 'https://test.com/img/swan-preview-image.jpg', //图片预览链接
"origin_url": 'https://test.com/img/swan-preview-image-origin.jpg' //图片的原图地址
},
{
"url": "https://test.com/img/swan-preview-image-2.png",//图片预览链接
"origin_url": "https://test.com/img/swan-preview-image-2-origin.png" //图片的原图地址
}
],
success: function (res) {
console.log('previewImage success', res);
},
fail: function (err) {
console.log('previewImage fail', err);
}
});
}
});