复杂场景下的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