wx 小程序中的全局数据共享方案 MonX

什么是全局数据共享
全局数据共享(又叫做:状态管理)是为了解决组件之间数据共享的问题。
开发中常用的全局数据共享方案有: Vuex 、 Redux 、 MobX 等。 2. 小程序中的全局数据共享方案
在小程序中,可使用 mobx-miniprogram 配合 mobx-miniprogram-bindings 实现全局数据共享。其
中:
mobx-miniprogram 用来创建 Store 实例对象
mobx-miniprogram-bindings 用来把 Store 中的共享数据或方法,绑定到组件或页面中使用

在项目中运行如下的命令,安装 MobX 相关的包:

1
npm install --save mobx-miniprogram@4.13.2 mobx-miniprogram-bindings@1.2.1

注意: MobX 相关的包安装完毕之后,记得删除 miniprogram_npm 目录后,重新构建 npm 。

创建 MobX 的 Store 实例
在项目根目录下新建 store 文件夹,并且新建 store.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 按需导入第三方包的方法observable, action
import { observable, action } from "mobx-miniprogram";
// 创建 Store实例对象,并将其导出
export const store = observable({
// 数据字段
numA: 1,
numB: 2,
// 计算属性
// 在计算属性的方法前,必须加 get修饰符,代表sum的值是只读的,无法进行修改
// 计算属性sum 依赖于numA和numB的值,因此sum函数的返回值就是最终的值
get sum() {
return this.numA + this.numB;
},
// 定义actions方法, 用来修改 store中的数据
updateNum1: action(function (step) {
this.numA += step;
}),
updateNum2: action(function (step) {
this.numB += step;
}),
});

在页面中使用:
页面的 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
// 1.首先 导入第三方包,将数据绑定到页面
import { createStoreBindings } from "mobx-miniprogram-bindings";
// 2.其次 在页面的js文件的头部区域导入容器的数据
import { store } from "../../store/store";
// 3. 绑定操作:将仓库的东西绑定到当前的页面中,在页面的js文件的Page方法中
Page({
// 上面周期函数--监听页面的加载
onLoad: function () {
// 调用createStoreBindings方法
// 参数1: 绑定给谁:当前页面this
// 参数2: 对象{ store(容器), fields(数据), actions(修改方法)
this.storeBindings = createStoreBindings(this, {
// 映射容器的实例
store,
// 映射容器的数据字段
fields: ["numA", "numB", "sum"],
// 映射容器修改的方法
actions: ["updateNum1"],
});
},
// 生命周期函数--监听页面的卸载
onUnload: function () {
// 使用this.storeBindings,得到调用createStoreBindings方法的返回值
//调用destroyStoreBindings 方法,进行清理销毁的工作
this.storeBindings.destroyStoreBindings();
},
});

页面的 wxml 文件中

1
2
3
<!-- 使用仓库中的数据 -->
<view>{{numA}} + {{numB}} = {{sum}}</view>
<van-button type="primary" bindtap="btnHnadler1">numA+1</van-button>

监听函数 btnHandler1 的代码

1
2
3
4
5
6
// 页面的js文件中的 tap事件处理函数
btnHnadler1 (e) {
console.log(e)
// 使用仓库中的方法,并传递数据
this.updateNum1(100)
}

在组件中使用
准备工作
新建组件文件夹以及组件文件 numbers
全局注册这个组件
在 message 页面中使用子组件
组件的 js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
按需导入容器成员
2. 在组件的behaviors节点 实现自动绑定
3. 在storeBindings节点指定要绑定的store和要绑定的数据以及方法
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'
Component({
// 通过storeBindingsBehavior 来实现自动绑定
behaviors: [storeBindingsBehavior],
storeBindings: {
store, // 指定要绑定的store
fields: { // 指定要绑定的数据字段或计算属性
numA: () => store.numA, // 绑定字段的方式1:
numB: store => store.numB, // 绑定字段的方式2
sum: 'sum' // 绑定字段的方式3
},
actions: { // 指定要绑定的方法
updateNum2: 'updateNum2'
}
}
})
注意: fields中前面是在组件中的名称,可自定义,后面是容器中的名称,必须和仓库一致

组件的 wxml 文件

1
2
3
<!-- 组件的 .wxml结构 -->
<view>{{numA}} + {{numB}} = {{sum}}</view>
<van-button type="primary" bindtap="btnHnadler2">numB+1</van-button>

事件处理函数 btnHandler2

1
2
3
4
5
6
7
8
Compoonent({
methods: {
btnHnadler2(e) {
// 直接使用this调用仓库中的方法
this.updateNum2(20);
},
},
});

结论: 无论是在页面中使用仓库中的东西, 还是在组件中使用仓库中的东西, 在绑定成功之后, 就可以像使
用自身数据和调用自身方法一样的对仓库的数据和方法进行操作。

实现 wx 小程序 API 的 Promise 化

  1. 基于回调函数的异步 API 的缺点
    默认情况下,小程序官方提供的异步 API 都是基于回调函数实现的,例如,网络请求的 API 需要按照
    如下的方式调用:

    1
    2
    3
    4
    5
    6
    7
    8
    wx.request({
    method: "",
    url: "",
    data: {},
    success: () => {}, // 成功的回调
    complate: () => {}, // 无论成功与否都会执行的回调
    fail: () => {}, // 失败的回调
    });

    这种代码的缺点是显而易见的, 容易造成回调地狱的问题,代码的可读性、维护性差!而我们就想将这

    种类型的代码使用 API Promise 化进行改造。

  2. 什么是 API Promise 化
    API Promise 化,指的是通过额外的配置,将官方提供的、基于回调函数的异步 API ,升级改造为基
    于 Promise 的异步 API ,从而提高代码的可读性、维护性,避免回调地狱的问题。

  3. 实现 API Promise 化
    在小程序中,实现 API Promise 化主要依赖于 miniprogram-api-promise 这个第三方的 npm 包。
    它的安装和使用步骤如下:

1
2
3
4
npm i --save miniprogram-api-promise@1.0.4
- 下载完成,我们不能直接使用,而是需要再次重新构建npm包
- 建议在构建前先删除原有的miniprogram_npm
- 然后再点击工具,构建npm

导入并执行:

1
2
3
4
5
6
7
8
9
10
11
// 在小程序入口文件中(app.js),只需要调用一次 promisifyAll()方法
// 即可实现异步API 的Promise化
// 按需导入一个方法
import { promisifyAll } from "miniprogram-api-promise";
// 声明一个常量,为一个空对象,
// 并在wx顶级对象下添加一个属性p也指向该空对象,使所有成员都可以使用该对象
const wxp = (wx.p = {});
// promisify all wx's api
// 参数1: wx顶级对象
// 参数2: wxp指向一个空对象
promisifyAll(wx, wxp);

解释上述代码:
promisifyAll : 做的事就是将 wx 拥有的属性方法都 copy 并改造了一份给了 wxp 这个对象。
然而, wxp 只是当前 js 文件的一个常量,只能在当前文件使用。
因此:我们在 wx 上挂载一个属性 p 让他和 wxp 指向同一个空对象。
在其他页面或者组件就可以通过全局对象 wx 点出 p 来访问到 wxp。
此时 wx.p 发起异步的请求时,得到的是一个 promise 对象。
那么我们就可以使用 async/await 简化 Promise 语法。

在原生开发小程序的过程中,发现有多个页面都使用了几乎完全一样的逻辑。由于小程序官方并没有提供 Mixins 这种代码复用机制,所以只能采用非常不优雅的复制粘贴的方式去“复用”代码。随着功能越来越复杂,靠复制粘贴来维护代码显然不科学,于是便寻思着如何在小程序里面实现 Mixins。

在小程序中实现 Mixins 方案

什么是 Mixins

Mixins 直译过来是“混入”的意思,顾名思义就是把可复用的代码混入当前的代码里面。熟悉 VueJS 的同学应该清楚,它提供了更强大了代码复用能力,解耦了重复的模块,让系统维护更加方便优雅。

先看看在 VueJS 中是怎么使用 Mixins 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// define a mixin object
var myMixin = {
created: function () {
this.hello();
},
methods: {
hello: function () {
console.log("hello from mixin!");
},
},
};

// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin],
});

var component = new Component(); // => "hello from mixin!"
123456789101112131415161718;

在上述的代码中,首先定义了一个名为 myMixin 的对象,里面定义了一些生命周期函数和方法。接着在一个新建的组件里面直接通过 mixins: [myMixin] 的方式注入,此时新建的组件便获得了来自 myMixin 的方法了。

明白了什么是 Mixins 以后,便可开始着手在小程序里面实现了。

Mixins 的机制

Mixins 也有一些小小的细节需要注意的,就是关于生命周期事件的执行顺序。在上一节的例子中,我们在 myMixin 里定义了一个 created() 方法,这是 VueJS 里面的一个生命周期事件。如果我们在新建组件 Component 里面也定义一个 created() 方法,那么执行结果会是如何呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
var Component = Vue.extend({
mixins: [myMixin],
created: function () {
console.log("hello from Component!");
},
});

var component = new Component();

// =>
// Hello from mixin!
// Hello from Component!
123456789101112;

可以看运行结果是先输出了来自 Mixin 的 log,再输出来自组件的 log。

除了生命周期函数以外,再看看对象属性的混入结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// define a mixin object
const myMixin = {
data() {
return {
mixinData: "data from mixin",
};
},
};

// define a component that uses this mixin
var Component = Vue.extend({
mixins: [myMixin],
data() {
return {
componentData: "data from component",
};
},
mounted() {
console.log(this.$data);
},
});

var component = new Component();
1234567891011121314151617181920212223;

在这里插入图片描述
在 VueJS 中,会把来自 Mixins 和组件的对象属性当中的内容(如 data, methods 等)混合,以确保两边的数据都同时存在。

经过上述的验证,我们可以得到 VueJS 中关于 Mixins 运行机制的结论:

生命周期属性,会优先执行来自 Mixins 当中的,后执行来自组件当中的。
对象类型属性,来自 Mixins 和来自组件中的会共存。
但是在小程序中,这套机制会和 VueJS 的有一点区别。在小程序中,自定义的方法是直接定义在 Page 的属性当中的,既不属于生命周期类型属性,也不属于对象类型属性。为了不引入奇怪的问题,我们为小程序的 Mixins 运行机制多加一条:

小程序中的自定义方法,优先级为 Page > Mixins,即 Page 中的自定义方法会覆盖 Mixins 当中的。

代码实现

在小程序中,每个页面都由 Page(options) 函数定义,而 Mixins 则作用于这个函数当中的 options 对象。因此我们实现 Mixins 的思路就有了——劫持并改写 Page 函数,最后再重新把它释放出来。

新建一个 mixins.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 保存原生的 Page 函数
const originPage = Page;

Page = (options) => {
const mixins = options.mixins;
// mixins 必须为数组
if (Array.isArray(mixins)) {
delete options.mixins;
// mixins 注入并执行相应逻辑
options = merge(mixins, options);
}
// 释放原生 Page 函数
originPage(options);
};
1234567891011121314;

原理很简单,关键的地方在于 merge() 函数。merge 函数即为小程序 Mixins 运行机制的具体实现,完全按照上一节总结的三条结论来进行。

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
// 定义小程序内置的属性/方法
const originProperties = ["data", "properties", "options"];
const originMethods = [
"onLoad",
"onReady",
"onShow",
"onHide",
"onUnload",
"onPullDownRefresh",
"onReachBottom",
"onShareAppMessage",
"onPageScroll",
"onTabItemTap",
];

function merge(mixins, options) {
mixins.forEach((mixin) => {
if (Object.prototype.toString.call(mixin) !== "[object Object]") {
throw new Error("mixin 类型必须为对象!");
}
// 遍历 mixin 里面的所有属性
for (let [key, value] of Object.entries(mixin)) {
if (originProperties.includes(key)) {
// 内置对象属性混入
options[key] = { ...value, ...options[key] };
} else if (originMethods.includes(key)) {
// 内置方法属性混入,优先执行混入的部分
const originFunc = options[key];
options[key] = function (...args) {
value.call(this, ...args);
return originFunc && originFunc.call(this, ...args);
};
} else {
// 自定义方法混入
options = { ...mixin, ...options };
}
}
});
return options;
}
1234567891011121314151617181920212223242526272829;

Mixins 使用

1、在小程序的 app.js 里引入 mixins.js

1
2
require("./mixins.js");
1;

2、撰写一个 myMixin.js

1
2
3
4
5
6
7
module.exports = {
data: { someData: "myMixin" },
onShow() {
console.log("Log from mixin!");
},
};

3、在 page/index/index.js 中使用

1
2
3
Page({
mixins: [require("../../myMixin.js")],
});