关于javascript:复杂场景下的h5与小程序通信

1次阅读

共计 7578 个字符,预计需要花费 19 分钟才能阅读完成。

简单场景下的 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 是否存在,进行不同的解决,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的利用,于是,我封装了上面这个 sdkminiAppBus,先来看看怎么用,还是下面的场景

    // 解决扫脸签到逻辑
        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
        })
    }

    应用起来也是得心顺畅,难受。

其余

类型齐备,应用时智能提醒,方便快捷。

正文完
 0