复杂场景下的h5与小程序通信
一、背景
在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
- 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
- 小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。
- 一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。
二、在业务内的实践
- 因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:
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部分代码如下:
// 处理扫脸签到逻辑
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是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdk`miniAppBus`,先来看看怎么用,还是上面的场景
// 处理扫脸签到逻辑
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如下:
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替代,无需关注环境变量。
完整代码:
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,通过策略模式解耦:
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
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com