简单场景下的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    })}

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

其余

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