1. 首页
  2. 小程序

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

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

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

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

链接:https://www.javascriptc.com/4319.html

« Docsify CLI v4.4.2版本发布,增加初始化时重写文件问询
别光知道用console.log调试了,快来试试这些高效的调试方法!»
Flutter 中文教程资源

相关推荐

QR code