小程序中h5頁面onShow實現及跨頁面通訊方案

小程序webview的現狀

h5頁面在小程序中的交互(跳轉)場景

  • h5跳轉小程序native頁面(如:調用小程序地址選擇能力,而後返回對應的地址信息給h5頁面)
  • h5跳轉己方業務線的h5頁面(內部頁面交互,方式比較多樣)
  • h5跳轉其它業務線的h5頁面(如:交易流程,相關頁面可能有其餘業務線提供)

主要痛點

  • 在完成相關操做後,頁面狀態須要更新,目前常見的更新方式有以下兩種:
    • 第一種:經過url傳參(如:url中加入__isonshowrefresh=1,告訴webview再次onshow時候刷新),把須要傳遞的參數拼接到url中,從新打開url。
    • 第二種:須要跳轉到新的頁面進行數據更新(如:下單頁 - 地址選擇頁 - 新的下單頁)
  • 第一種方案,功能上沒有問題,但會致使頁面刷新,若是頁面操做複雜,須要屢次刷新
  • 第二種方案,正向操做時體驗比方案一好,但致使了另一個問題:操做跳轉層級過深,尤爲返回的時候簡直讓人崩潰。

小程序中,h5頁面打開新頁面方式

咱們先來看下小程序中常見的h5跳h5的方式:ios

  • 方式1:直接用location.href跳轉,返回時候各機型表現不一致,有的會刷頁面,從新執行js,有的會直接展現以前的緩存
  • 方式2:經過路由hash跳轉,返回觸發hashchange,頁面不刷新,js層面重現渲染
  • 方式3:跳轉頁面打開一個新的webview,至關於每一個頁面都是一個獨立的webview

咱們採用的是方式3,理由以下:web

  1. 打開新頁面時的效果更趨近於native間的跳轉(固然新打開的頁面也會從新加載靜態資源,同時這也有另外一個問題,一旦你打開10個層級後,再打開新的webview就沒反應了,這個是小程序10層限制)
  2. 返回的體驗也更趨近於native,同時保證頁面狀態統一(不會出現有的直接展現,有的會從新執行js)
  3. webview經過this.src拿到的連接即爲當前頁面連接,由於若是頁面自行經過路由和location.href跳轉,頁面連接變動後,webview並不會知曉,這種方案,webview經過this.src拿到的連接始終是當前頁面的連接。

因爲這種方案可能會達到小程序的10層限制。因此在一些重要頁面建議加入「回到首頁」的操做,經過這個操做來縮短小程序歷史棧小程序

回到首頁方案簡述

(若是不感興趣這部分能夠直接略過)微信小程序

wx.miniProgram.reLaunch({
  url: '/pages/webview/bridge?url=項目首頁地址'
})
複製代碼

先聲明,咱們webview的路徑是/pages/webview/webview數組

/pages/webview/bridge是個中轉頁,有以下特色:瀏覽器

  • 該頁面並不是最終打開h5頁面的webview頁,而是一個中轉頁
  • 主要用做返回處理
  • 頁面邏輯:
    • 若是是第一次展現,則跳轉/pages/webview/webview,同時把url傳過去,正常打開h5
    • 若是不是第一次展現,說明是從webview返回過來的,直接重定向到小程序首頁

這個中轉頁:主要保證reLaunch到某h5頁面後,用戶仍然能夠點擊返回到小程序首頁。緩存

該方案一般用於:小程序中內嵌了多個業務線的h5頁面這種場景。bash

一個內容發佈場景

咱們從首頁進入發佈頁,完成發佈後,跳轉至商品詳情頁微信

那麼對於一個新用戶來說,整個操做過程是這樣的:併發

  1. 首頁(點擊發布)
  2. 進入發佈頁面(選擇發佈商品的分類)
  3. 進入商品分類頁(選擇完成後)
  4. 將分類id拼入url,進入新的發佈頁面(選取件地址)
  5. 進入地址列表頁(若是新用戶是沒有地址的,點擊新增地址)
  6. 進入新增地址頁(添加完成後)
  7. 將地址id拼如url,進入又一個新的發佈頁面(編輯完信息後點擊發布)
  8. 進入發佈成功頁(點擊查看商品詳情)
  9. 進入商品詳情頁

這個場景就是同一個頁面,裏面不一樣的內容項須要跳轉不一樣的頁面去操做,而後再回到原來頁面更新狀態的問題。

假如商品詳情頁沒有「回到首頁」的入口,那麼這個用戶要想回到首頁。。。須要按8次「返回」 = =!

通過這個體驗後,我想通常的用戶是沒有勇氣再發布內容的。

固然也有另外一種這種折中方案

就是商品提到的,在鏈接中加入某個標誌位,好比在url中加入__isonshowrefresh=1,webview在打開鏈接時候,會去讀取這個參數,若是有,則每次在onShow時候,從新加載url,經過刷新頁面進行頁面狀態更新。

這個體驗也不爽,就是在複雜的頁面會屢次刷新。

聲明

我下面要講的這個方案並非停留在設想階段,它已經在線上跑了

想看效果的朋友,能夠在微信小程序中搜:

「轉轉二手交易網」-「0元免費領」-(底部)「送閒置賺星星」-進入到發佈頁後

  • 分類(跳轉h5,選中內容後返回,將參數傳給以前的h5)

  • 取件地址(跳轉native原生地址選擇,選中後返回,將參數傳給以前的h5)

OK,咱們進入今天的主題

小程序中h5頁面onShow和跨頁面通訊的實現

首先想到的就是onShow方法的實現,以前有人提議用visibilitychange來實現onShow方法。

但調研事後,這種方式在ios中表現符合預期,可是在安卓手機裏,是不能按預期觸發的。因此該方案被我否了。

因而就有了下面的方案

原理介紹

這個方案須要h5和小程序的webview都作處理。

核心思想:利用webview的hash特性

image

  • 小程序經過hash傳參,頁面不會更新(這個和瀏覽器同樣)
  • h5能夠經過hashchange捕獲最新參數,進行自定義邏輯處理
  • 最後執行window.history.go(-1)

爲何要執行window.history.go(-1)

這一步是整個方案的精髓

  • 由於hash變動會致使webview歷史棧長度+1,用戶須要多一次返回操做。但這一步明顯是多餘的。
  • 同時window.history.go(-1)後,會把webview在hash中添加的參數去掉,還能保證和以前的url一致。

方案延伸(跨頁面數據傳遞)

小程序裏另個一常見的場景就是調用第三業務(或者己方業務),在作完某些操做後須要把選中的數據帶回以前的頁面。

如前面提到的例子:發佈頁,須要選擇發佈類型,而後返回,發佈頁發佈類型局部更新

固然有些同窗會說:我能夠用setInterval,監控localStorage。在新頁面選中內容後,設置localStorage,而後在返回不就能夠了。

我這裏說的是通用方案。若是頁面都是由己方業務線維護的固然能夠隨便折騰。

可是一旦涉及到第三方業務線,尤爲不一樣域名頁面的業務調用,這種通訊方式就尷尬了。

那個人方案怎麼處理呢,我總結了一張圖

image

咱們來解讀一下這張圖:

  • webview1打開發布頁面,h5綁定hashchange事件(由於webview經過hash傳值時會觸發該事件)
  • 將自定義的onShow方法緩存。在hashchange觸發時,尋找指定參數,若是存在則觸發
  • 用戶點擊跳轉到類型選擇頁
  • 這時會打開一個新的webview2頁面實例,打開類型選擇頁
  • 用戶操做完成,調用wx.miniProgram.postMessage把數據發送給webview,並返回
  • webview因爲綁定了bindmessage事件,在返回時會接收到h5發送的數據
  • 同時將接收到的數據緩存在一個全局的store中,webview2銷燬,小程序執行返回
  • 從webview2返回到webview1,這時webview1的onShow鉤子會觸發
  • webview1讀取全局的store,將要發送的參數取出,拼接h5連接的hash部分,並從新打開該連接
  • 雖然從新打開連接,因爲僅僅是hash部分的變化,因此頁面不會刷新
  • 但會觸發h5頁面的hashchange,此時調用用戶自定義的onShow方法,讀取hash參數,進行頁面更新
  • h5頁面在執行完onShow方法後,調用window.history.go(-1),恢復歷史棧

整個過程就是這樣

代碼示意:

小程序

小程序webview要先作幾方面考慮:

  • 出於平滑接入的考慮,不能上來搞一刀切,要保證現有頁面再不作任何修改的狀況下繼續訪問。
  • 新能力要經過額外參數區分,如:檢測url中的query部分,帶有__isonshowpro=1再進行經過hash方式傳參。
  • 改造原有邏輯,讓__isonshowpro=1時,hash處理邏輯優先級最高
  • 參數定義,在前面加入了兩個下劃線,目的是爲了分區url中正常的參數

小程序端webview.wpy

<web-view wx:if="{{url}}" src="{{url}}" binderror="onError" bindload="onLoaded" bindmessage="onPostMessage"></web-view>

// 連接處理工具方法
import util from '@/lib/util';
// 全局數據存儲操做類
import routeParams from '@/lib/routeParams';
const urlReg = /^(https?\:\/\/[^?#]+)(\?[^#]*)?(#[^\?&]+)?(.+)?$/;
let messageData = {};

export default class extends wepy.page {
  data = {
    // 頁面展現次數
    pageShowCount: 0,
    // 頁面url中query部分的參數對象
    mQuery: {},
    ...
  }
  
  onShow(){
    ++this.pageShowCount;
    // 獲取其餘頁面通過操做後,須要傳遞給h5的參數
    let data = routeParams.getBackFromData() || {};
    // webview頁面狀態更新
    if(this.pageShowCount > 1 && this.mQuery.__isonshowpro && this.mQuery.__isonshowpro === '1' || data.refresh){
      // 獲取須要傳遞給h5頁面的參數
      let refreshParam = data.refreshParam;
      ...
      // 若是鏈接中帶有須要處理onShow邏輯的參數(經過url的hash和h5交互,而不是刷頁面)
      if (this.pageShowCount > 1 && this.mQuery.__isonshowpro === '1') {
        let [whole, mainUrl, queryStr, hashStr, hashQueryStr] = urlReg.exec(this.url);
        // 在url的hash中加入新的參數
        hashStr = (hashStr || '#').substring(1);
        if (refreshParam) {
          delete refreshParam.refresh;
        }
        const messageData = this.getNavigateMessageData();
        // 將須要更新的參數傳給頁面hash
        hashStr = util.addQuery(hashStr, Object.assign({
          // onshow標誌位
          __isonshow: 1,
          // wa主動觸發hashchange標誌位
          // 其實目前經過__isonshow就能夠判斷是wa主動觸發hashchange
          // 設置該字段是爲了明確功能,且之後擴展用
          __wachangehash: 1,
          // 時間戳刷新
          __hashtimestamp: Date.now()
        }, messageData, refreshParam));
        this.url = mainUrl + queryStr + '#' + hashStr;
        console.log('【webview-hashchange-url】', this.url);
        // 這裏要加個延遲,不然在webview返回到webview時,沒法觸發hashchange,應該是小程序bug
        setTimeout(()=> {
          this.$apply();
        }, 50);
      // 經過修改query參數,刷新webview
      } else {
        ...
      }
      ...
    }
  }
  
  /**
   * 獲取須要發送的消息數據
   */
  getNavigateMessageData(){
    let rst = {};
    for(let i in messageData){
      /* message結構:
        message: {
          key: 'xx',        // 消息名稱
          content: 'xx',    // 消息內容
          trigger: {        // 觸發條件
            type: '',       // 觸發類型 
                               - immediately 在下一次onshow或者打開頁面中馬上觸發,
                               - url 在找到指定h5連接時觸發
            content: ''     // 條件內容
                               - type=immediately 時爲空
                               - type=url 時候爲h5連接地址
          }
        }
      */
      const message = messageData[i];
      const trigger = message.trigger || {};
      // 馬上發送、路徑觸發
      if(trigger.type === 'immediately' || trigger.type === 'url' && this.url.indexOf(trigger.content) > -1){
        // 將key和content集合到一個對象中,便於hash直接設置
        rst[message.key] = message.content;
        // 消息通知後,從緩存中刪除
        delete messageData[message.key];
      }
    }
    console.log('【webview-get-message】', rst);
    console.log('【webview-message-cache】', messageData);
    return rst;
  }
  
  /**
   * 存儲消息數據
   */
  storeNavigateMessageData(message){
    if(message && message.key){
      console.log('【webview-store-message】', message)
      // 經過key設置每一條消息名稱
      messageData[message.key] = message;
      console.log('【webview-message-cache】', messageData);
    }
  }
  
  methods = {
    // 接收發送過來的消息
    onPostMessage(e){
      if(!e.detail.data)return;
      const detailData = e.detail.data;
      // 獲取消息數據
      let messageData = getValueFromMixedArray(detailData, 'messageData', true);
      if (messageData) {
        // 存儲
        this.storeNavigateMessageData(messageData);
      }
      ...
    }
  }
  
  ...
}
複製代碼

上面東西看着挺多,總結下來就是幾點:

  • 綁定bindmessage事件
  • 接收到頁面傳來的消息以後,須要按照必定規則存起來(我是按照key存儲的)
  • webview在觸發onShow鉤子時候,按照以前傳過來的觸發條件(condition),取出須要發送的消息數據
  • 將數據拼接到url的hash部分,並加入特有的標誌位,從新加載url

h5端

h5端在作修改時也要考慮幾點:

  • 最好能把這些交互邏輯封裝起來

  • 讓業務方比較簡單方便的調用

  • 這裏我新定義了2個方法

    • onShow(callback)
      • 描述:這個和小程序onShow鉤子同樣,只不過是給h5調用的
      • 參數:callback 回調方法

    例子:發佈頁面,須要選擇分類,返回時須要更新分類信息

    import { isZZWA, onShow } from '@/lib/sdk'
    import URL from '@/lib/url'
    
    ...
    created () {
    if (isZZWA()) {
      onShow(() => {
      // 地址信息
        const addressInfo = URL.getHashParam('zzwaAddress')
          console.log('addressInfo:', decodeURIComponent(addressInfo))
          ...
          // 分類信息
          const selecteCateInfo = URL.getHashParam('selecteCateInfo')
          console.log('selecteCateInfo:', selecteCateInfo)
          ...
      } else {
        ...
      }
    }
    ...
    複製代碼
    • serviceDone(data, condition)
      • 描述:業務結束,須要將數據傳遞給指定頁面
      • 參數:
        • data Object 須要傳遞的數據 {key: 'xx', content: 'xx'}
        • condition String|Number 觸發條件
          • String 指定url的路徑,當webview打開指定的url觸發onshow時,會發送該消息
          • Number 返回到指定的測試,相似history.go(-1),如: -1,-2

    例子:類型選擇頁

    import { isZZWA, serviceDone } from '@/lib/sdk'
    // 類型選擇點擊
    typeChooseClick (param, type) {
      ...
      if (isZZWA()) {
        // 須要返回的數據
        const data = {
          key: 'selecteCateInfo',
          content: JSON.stringify({...})
        }
        // 經過postMessage發送給小程序,-1表示返回上一頁面
        serviceDone(data, -1)
      } else {
        ...
      }  
    }
    複製代碼

ok,咱們來看看h5端的sdk是怎麼實現的

import util from './util';

class WASDK {
  /**
   * Create a instance.
   * @ignore
   */
  constructor(){
    // hashchang事件處理
    if('onhashchange' in window && window.addEventListener && !WASDK.hashInfo.isInit){
      // 更新標誌位
      WASDK.hashInfo.isInit = true;
      // 綁定hashchange
      window.addEventListener('hashchange', ()=>{
        // 若是小程序webview修改的hash,才進行處理
        if (util.getHash(window.location.href, '__wachangehash') === '1') {
          // 這塊有個坑:
          // ios小程序webview在修改完url的hash以後,頁面hashchange和更新均可以正常觸發
          // 可是:h5調用部分小程序能力會失敗(如:ios在設置完hash後,調用wx.uploadImg會失敗,須要從新設置wx.config)
          // 由於ios小程序的邏輯是,url只要發生變化,wx.config中的appId就找不到了
          // 因此須要從新進行wx.config配置
          // 這一步是獲取以前設置wx.config的參數(須要從服務端拿,由於以前已經獲取過了,這裏從緩存直接取)
          const jsticket = window.native && window.native.adapter && window.native.adapter.jsticket || null;
          const ua = navigator.userAgent;
          // 非安卓系統要從新設置wx.config
          if (jsticket && !(ua.indexOf('Android') > -1 || ua.indexOf('Adr') > -1)) {
            window.wx.config({
              debug: false,
              appId: jsticket.appId,
              timestamp: jsticket.timestamp,
              nonceStr: jsticket.noncestr,
              signature: jsticket.signature,
              jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ',
                'onMenuShareQZone', 'onMenuShareWeibo', 'scanQRCode', 'chooseImage', 'uploadImage', 'previewImage', 'getLocation', 'openLocation']
            })
          }
          // 觸發緩存數組的回調
          WASDK.hashInfo.callbackArr.forEach(callback=>{
            callback();
          })
          // 執行返回操做(這一步是重點!!)
          // 由於webview設置完hash參數後,會使webview歷史棧+1
          // 而實際並不須要此次多餘的歷史記錄,因此須要執行返回操做把它去掉
          // 即使是返回操做,也僅僅是hash層面的變動,因此不會觸發頁面刷新
          // 用setTimeout表示在下一次事件循環進行返回操做。若是後面有對dom操做能夠在當前次事件循環完成
          setTimeout(()=>{
            window.history.go(-1);
          }, 0);
        }
      }, false)
    }
  }

  /**
   * hash相關信息
   */
  static hashInfo = {
    // 是否已經初始化
    isInit: false,
    // hash回調香瓜數組
    callbackArr: []
  }
  
  /**
   * 頁面再次展現時鉤子方法
   * @param {Function} callback - 必填, callback回調方法, 回傳參數爲hash部分問號後面的參數解析對象
   */
  @execLog
  onShow(callback){
    if (typeof callback === 'function') {
      // 對回調方法進行onshow邏輯包裝,並推入緩存數組
      WASDK.hashInfo.callbackArr.push(function(){
        // 檢查是不是指定參數發生變化
        if(util.getHash(window.location.href, '__isonshow') === '1'){
          // 觸發onShow回調
          callback();
        }
      })
    } else {
      util.console.error(`參數錯誤,調用onShow請傳入正確callback回調`);
    }
  }
  
  /**
   * 業務處理完成併發送消息
   * @param {Object}           obj - 必填項,消息對象
   * @param {String}           obj.key - 必填項,消息名稱
   * @param {String}           obj.content - 可選項,消息內容,默認空串,若是是內容對象,請轉換成字符串
   * @param {String|Number}    condition - 可選項,默認僅進行postMessage
   *                              String - 能夠傳指定url的路徑,當小程序webview打開指定的url或者onshow時,會觸發該消息
   *                                       也可傳小程序path,這個爲之後預留
   *                              Number - 返回到指定的測試,相似history.go(-1),如: -1,-2
   */
  @execLog
  serviceDone(obj, condition){
    if(obj && obj.key){
      // 消息體
      const message = {
        // 消息名稱
        key: obj.key,
        // 消息體
        content: obj.content || '',
        // 觸發條件
        trigger: {
          // 類型 'immediately'在下一次onshow中馬上觸發, 'url',在找到指定h5連接時觸發,'path'在打開指定小程序路徑時觸發
          type: 'immediately',
          // 條件內容,immediately是爲空,url是爲h5連接地址,path是爲小程序路徑
          content: ''
        }
      };
      // 解析觸發條件
      condition = condition || 0;
      // 若是是路徑
      if(typeof condition === 'string' && (condition.indexOf('http') > -1 || condition.indexOf('pages/') > -1)){
        // 設置消息觸發條件
        message.trigger = {
          type: condition.indexOf('http') > -1 ? 'url' : 'path',
          content: condition
        }
      }
      // 發送消息
      wx.miniProgram.postMessage({
        data: {
          messageData: message
        }
      });
      // 若是不是url或者path觸發,則對conditon是否須要返回進行判斷
      if(message.trigger.type === 'immediately'){
        // 查看是否須要返回指定的層級,兼容傳入'-1'字符串這種類型的場景
        try{
          condition = parseInt(condition, 10);
        }catch(e){}
        // 保證返回級數的正確性
        if(condition && typeof condition === 'number' && !isNaN(condition)){
          this.handler.navigateBack({delta: Math.abs(condition)});
        }
      }
    }else{
      util.console.error(`參數錯誤,調用serviceDone方法,傳入的對象中不包含key值`);
    }
  }
  
  ...
}

window.native = new Native();

export default native;

複製代碼

這個看着也挺多,總結下來是兩點:

  • onShow方法的實現
    • 綁定一個hashchange事件(這裏作了防止重複綁定事件的處理)
    • 將傳入的onShow自定義事件緩存在一個數組中,hashchange觸發時,根據特有的標誌位__isonshow和__wachangehash肯定是否觸發
  • serviceDone方法的實現
    • 處理傳過來的數據
    • 處理該數據的觸發條件:immediately表示最近的一次onShow觸發,或者本身指定url
    • 經過wx.miniProgram.postMessage發送數據

ok,整個方案就介紹完了

結語

最先的方案並不徹底是這樣的,但原理是同樣的。在我實現的過程當中發現原始方案有不少問題

因而我又作了大量的改造和細節優化,因而造成了上面的最終方案。

這個方案屬於侵入式改造方案,須要各業務方改造本身的代碼。雖然有必定改形成本,但用戶體驗的收益很是明顯。

ps:咱們的QA在測試時都說「這用起來就爽多了」

注意:

採用這個方案須要注意幾點:

  • 若是採用這種方式通訊,須要在當前頁面url的query部分加入__isonshowpro=1,不然是不會經過hash通訊的
  • 同時要保證頁面確實調用了onShow方法,不然頁面也是不會刷新的
  • 若是第三方業務須要傳值,須要統一採用serviceDone方法通訊

好了,今天就介紹這麼多,你們一塊兒學習

相關文章
相關標籤/搜索