小程序webview通信(单向)

4,624 阅读3分钟

最近在做微信小程序相关开发,需要将原有的h5页面在小程序中展示。由于h5页面比较多,不太适合重新使用小程序开发。发现微信小程序有组件webview,可以将h5页面内嵌到webview中。实现和小程序页面类似的体验。

但是原有的h5页面调用微信sdk部分,需要重新调用小程序的相关功能或使用小程序页面实现。一些通用的功能打算使用小程序页面重新实现,如支付相关的功能,打算通过wx.miniProgram.navigateTo({url:'/pages/xxx/pay'})。支付完成后,webview的页面需要获取到支付相关的事件,需要有小程序向webview通信的能力。

通过查看webview文档,发现小程序提供了一个消息通知的机制postMessage,但是,该方法只有页面被关闭或调起分享等时候,才会触发。对于目前的需求不适用。而webview仅提供了url的设置功能,想要用url来通知webview中的页面,同时要保证页面无刷新,则需要使用hash来修改url。

原理

webview中的h5页面监听hashchange事件,当监听到hash中有_iswebviewpush字符串时,表示是webview推送的事件,做相关处理,并且马上history.back()一次,使history堆栈保持跟事件推送前一致。这种方式如果原h5页面是vue或react页面,且使用了hash方式的Router的话,也可以兼容(改造下hashchange,不对有_iswebviewpush字段的hash处理)。

小程序中的页面提供webview组件的渲染,根据页面参数to显示对应的h5页面,当H5页面跳转到类似小程序支付页面,进行支付时,页面设置下hashEvent,通知H5WebView,在onShow(Taro.componentDidShow)事件周期的时候,设置webview的url,将Hash值传给h5。

webview 页面跳转

// h5页面跳转到另外一个h5页面
function goToH5Page(url,title){
    wx.miniProgram.navigateTo({url:`/pages-taro/xxx/H5WebView?to=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`)
}

// h5页面跳转到小程序功能页面
function goToH5Page(url,title){
    wx.miniProgram.navigateTo({url:`/pages-taro/xxx/pay?type=needHashEvent`)
}

h5页面改造

import { unpackUrlSearch } from "../core/UrlHelper";
import Event from "../core/Event"; 

const HASH_EVENT_TAG = '_iswebviewpush';
/**
 * 和webview的hash通信工具类
 * 注: 进行跳转前一定要先初始化
 */
export default class WebViewHashPushChannel extends Event{
    private static Instance: WebViewHashPushChannel;

    static getInstance() {
        if (!WebViewHashPushChannel.Instance) {
            WebViewHashPushChannel.Instance = new WebViewHashPushChannel();
        }
        return WebViewHashPushChannel.Instance;
    }

    private constructor() {
        super();
        this.init();
    }
    
    private isInited = false;
    private init() {
        if (this.isInited) {
            return;
        }
        this.isInited = true;
        // TODO:小程序支持该形式才做处理 if 平台判断
        console.log('开始监听hashchange');
        window.addEventListener("hashchange",this.onHashChange.bind(this));
    }

    private dealHashEvent() { 
        let hash = location.hash;
        console.log('收到Hash通知', hash);
        if (!hash) {
            return;
        }
        if (hash) {
            hash = hash.substr(1);// remove #
        }
        let queryObj = unpackUrlSearch(hash);
        if (!queryObj.type) {
            return;
        }
        this.emit(queryObj.type, queryObj);
    }

    private isHashEvent() {
        return location.hash.indexOf(HASH_EVENT_TAG) > 0;
    }

    private onHashChange() {
        this.dealHashEvent();
        if (this.isHashEvent()) {// 事件推送,恢复堆栈
            history.back();
        }
    }
}

小程序webview页面改造

项目使用Taro框架,如果用微信小程序原生代码开发,将componentWillMount和componentDidShow 换成小程序的生命周期 onLoad和onShow即可。

import Taro from "@tarojs/taro";
import { WebView } from "@tarojs/components";
import { getCurrentChannelEventHash, resetChannelHash } from './WebviewHashChannelData';
import { unpackUrl } from '../../src/core/UrlHelper';

const DEFAULT_WEB_PATH = 'https://xxx.x.com/xxx';// to 参数默认的域名前缀
/**
 * 小程序Page页面
 * @param to 要跳转的h5页面地址 e.g:/xxx/xxx or 完整地址 https://xxx....
 * @param title 要跳转的h5页面标题
 */
export default class H5WebView extends Taro.Component {
  config = {
    navigationBarTitleText: " "
  };

  state = {
    url: "", // url不支持Hash
    hash: ""
  };

  isBack = false;
  componentWillMount() {
    this.isBack = false;
    const to = decodeURIComponent(this.$router.params.to || "");
    const urlObj = unpackUrl(to);
    let url = urlObj.pathWithSearch;
    let hash = urlObj.hash;
    let title = this.$router.params.title;
    if (title) {
      title = decodeURIComponent(title);
      Taro.setNavigationBarTitle({ title: title });
    }
    this.setState({
      url,
      hash
    });
    console.log("[H5WebView] mount", url, hash);
  }

  componentDidShow() {// onShow
    if (this.isBack) {
      // 小程序页面回退,通知h5
      let channelEventHash = getCurrentChannelEventHash();
      // if (!channelEventHash) {// TODO 仅白名单的页面增加backPush 通过postMessage??
      //     channelEventHash = getBackEventHash();// 通知游戏,上层的Page 移走了
      // }
      this.setState({
        hash: channelEventHash
      });
      // 通知完毕 重置
      resetChannelHash();
      console.log("[H5WebView] show set hash", channelEventHash, this.state.url);
    } else {
      console.log("[H5WebView] show first");
    }
    this.isBack = true;
  }

  _normalizeTo() {
    let to = this.state.url;
    if (!to) {
      return null;
    }
    if (to.substr(0, 4) !== "http") {
      to = DEFAULT_WEB_PATH + to;
    }
    if (this.state.hash) {
      to += "#" + this.state.hash;
    }
    return to;
  }

  render() {
    let to = this._normalizeTo();
    if (!to) {
      return null;
    }
    return (
      <WebView src={to}/>
    );
  }
}

现有问题

目前只对navigateTo出来的页面进行事件推送,而有navigateTo的相关页面可以保证一定做了historyBack处理。

代码地址

github.com/huhm/webvie…