复杂场景下的h5与小程序通信

复杂场景下的h5与小程序通信

一、背景

在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:

  • 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
  • 小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。
    一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。

    二、在业务内的实践

    因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    export function injectMiniAppScript() {
    if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
    const s = document.createElement('script');

    s.src = 'https://appx/web-view.min.js';
    s.onload = () => {
    // 加载完成时触发自定义事件
    const customEvent = new CustomEvent('myLoad', { detail:'' });
    document.dispatchEvent(customEvent);
    };

    s.onerror = (e) => {
    // 加载失败时上传日志
    uploadLog({
    tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
    });
    };

    document.body.insertBefore(s, document.body.firstChild);
    }
    }
    加载脚本完成后,我们就可以调用my.postMessage和my.onMessage进行通信(统一约定h5发送消息给小程序时,必须带action,小程序根据action处理业务逻辑,同时小程序处理完成的结果必须带type,h5在不同的业务场景下通过my.onMessage处理不同type的响应),比如典型的,h5调用小程序签到:
    h5部分代码如下:
    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
    // 处理扫脸签到逻辑
    const faceVerify = (): Promise<AlipaySignResult> => {

    return new Promise((resolve) => {
    const handle = () => {
    window.my.onMessage = (result: AlipaySignResult) => {
    if (result.type === 'FACE_VERIFY_TIMEOUT' ||
    result.type === 'DO_SIGN' ||
    result.type === 'FACE_VERIFY' ||
    result.type === 'LOCATION' ||
    result.type === 'LOCATION_UNBELIEVABLE' ||
    result.type === 'NOT_IN_ALIPAY') {
    resolve(result);
    }
    };

    window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
    };

    if (window.my) {
    handle();
    } else {
    // 先记录错误日志
    sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
    // 监听load事件
    document.addEventListener('myLoad', handle);
    }
    });
    };
    实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus,先来看看怎么用,还是上面的场景
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 处理扫脸签到逻辑
    const faceVerify = (): Promise<AlipaySignResult> => {
    miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
    return miniAppBus.subscribeAsync<AlipaySignResult>([
    'FACE_VERIFY_TIMEOUT',
    'DO_SIGN',
    'FACE_VERIFY',
    'LOCATION',
    'LOCATION_UNBELIEVABLE',
    'NOT_IN_ALIPAY',
    ])
    };
    可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。

三、实现及背后的思考

为了满足不同场景和使用的方便,公开暴露的interface如下:

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
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber<T>} callback
* @memberof MiniAppEventBus
*/
subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise<MiniAppMessage<T>>}
* @memberof MiniAppEventBus
*/
subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise<void>}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise<void>;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise<unknown>}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise<unknown>;
}
  • subscribe:函数接收两个参数,
  • type:需要订阅的type,可以是字符串,也可以是数组。
  • callback:回调函数。
  • subscribeAsync:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。
  • unsubscribe:取消订阅。
  • postMessage:postMessage替代,无需关注环境变量。

完整代码:

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import { injectMiniAppScript } from './tools';

/**
* @description 小程序返回结果
* @export
* @interface MiniAppMessage
*/

interface MiniAppMessageBase {
type: string;
}

type MiniAppMessage<T extends unknown = {}> = MiniAppMessageBase & {
[P in keyof T]: T[P]
}
/**
* @description 小程序接收消息
* @export
* @interface MessageToMiniApp
*/
export interface MessageToMiniApp {
action: string;
[x: string]: unknown
}

interface MiniAppMessageSubscriber<T extends unknown = {}> {
(params: MiniAppMessage<T>): void
}
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber<T>} callback
* @memberof MiniAppEventBus
*/
subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise<MiniAppMessage<T>>}
* @memberof MiniAppEventBus
*/
subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise<void>}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise<void>;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise<unknown>}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise<unknown>;
}
class MiniAppEventBus implements MiniAppEventBus{

/**
* @description: 监听函数
* @type {Map<string, MiniAppMessageSubscriber[]>}
* @memberof MiniAppEventBus
*/
listeners: Map<string, MiniAppMessageSubscriber[]>;
constructor() {
this.listeners = new Map<string, Array<MiniAppMessageSubscriber<unknown>>>();
this.init();
}

/**
* @description 初始化
* @private
* @memberof MiniAppEventBus
*/
private init() {
if (!window.my) {
// 引入脚本
injectMiniAppScript();
}

this.startListen();
}

/**
* @description 保证my变量存在的时候执行函数func
* @private
* @param {Function} func
* @returns
* @memberof MiniAppEventBus
*/
private async ensureEnv(func: Function) {
return new Promise((resolve) => {
const promiseResolve = () => {
resolve(func.call(this));
};

// 全局变量
if (window.my) {
promiseResolve();
}

document.addEventListener('myLoad', promiseResolve);
});
}

/**
* @description 监听小程序消息
* @private
* @memberof MiniAppEventBus
*/
private listen() {
window.my.onMessage = (msg: MiniAppMessage<unknown>) => {
this.dispatch<unknown>(msg.type, msg);
};
}

private async startListen() {
return this.ensureEnv(this.listen);
}

/**
* @description 发送消息,必须包含action
* @param {MessageToMiniApp} msg
* @returns
* @memberof MiniAppEventBus
*/
public postMessage(msg: MessageToMiniApp) {
return new Promise((resolve) => {
const realPost = () => {
resolve(window.my.postMessage(msg));
};

resolve(this.ensureEnv(realPost));
});
}

/**
* @description 订阅消息,支持单个或多个
* @template T
* @param {(string|string[])} type
* @param {MiniAppMessageSubscriber<T>} callback
* @returns
* @memberof MiniAppEventBus
*/
public subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>) {
const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber<T>) => {
let listeners = this.listeners.get(type) || [];

listeners.push(cb);
this.listeners.set(type, listeners);
};

this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
}

private forEach(type:string | string[],cb:(type:string)=>void){
if (typeof type === 'string') {
return cb(type);
}

for (const key in type) {
if (Object.prototype.hasOwnProperty.call(type, key)) {
const element = type[key];

cb(element);
}
}
}

/**
* @description 异步订阅
* @template T
* @param {(string|string[])} type
* @returns {Promise<MiniAppMessage<T>>}
* @memberof MiniAppEventBus
*/
public async subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>> {
return new Promise((resolve, _reject) => {
this.subscribe<T>(type, resolve);
});
}

/**
* @description 触发事件
* @param {string} type
* @param {MiniAppMessage} msg
* @memberof MiniAppEventBus
*/
public async dispatch<T = {}>(type: string, msg: MiniAppMessage<T>) {
let listeners = this.listeners.get(type) || [];

listeners.map(i => {
if (typeof i === 'function') {
i(msg);
}
});
}

public async unSubscribe(type:string | string[]){
const unsubscribeSingle = (type: string) => {
this.listeners.set(type, []);
};

this.forEach(type,(type:string)=>unsubscribeSingle(type));
}
}

export default new MiniAppEventBus();

class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。

四、小程序内部的处理

定义action handle,通过策略模式解耦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const actionHandles = {
async FACE_VERIFY(){},
async GET_STEP(){},
async UPLOAD_HASH(){},
async GET_AUTH_CODE(){},
...// 其他action
}
....
// 在webview的消息监听函数中
async startProcess(e) {
const data = e.detail;
// 根据不同的action调用不同的handle处理
const handle = actionHandles[data.action];
if (handle) {

return actionHandles[data.action](this, data)
}
return uploadLogsExtend({
tip: STRING_CONTANT.UNKNOWN_ACTIONS,
data
})
}

使用起来也是得心顺畅,舒服。

原文:https://segmentfault.com/a/1190000023360940