小程序(六)內嵌H5的終極解決方案

這是我參與更文挑戰的第3天,活動詳情查看: 更文挑戰html

1、小程序和H5的區別

1.1 運行環境方面

從運行環境方面開看,H5 的宿主環境是瀏覽器,只要有瀏覽器,就可使用,包括APP中的 web-view 組件,以及小程序提供的 web-view 組件小程序就不同了,它運行於特定的移動軟件平臺 (Wechat / 支付寶 / 字節跳動 / 百度 / QQ 等)。react

拿微信小程序來講,它是基於瀏覽器內核重構的內置解析器,它並非一個完整的瀏覽器,官方文檔中重點強調了腳本內沒法使用瀏覽器中經常使用的 window 對象和 document 對象,就是沒有 DOM 和 BOM 的相關的 API,這一條就幹掉了 JQ 和一些依賴於 BOM 和 DOM 的NPM包。android

1.2 運行機制方面

H5 的運行就是一個網頁的運行,這裏不過多敘述,小程序仍是以微信小程序舉例。ios

1.2.1 啓動

若是用戶已經打開過某小程序,在必定時間內再次打開該小程序,此時無需從新啓動,只需將後臺態的小程序切換到前臺,整個過程就是所謂的 熱啓動。若是用戶首次打開或小程序被微信主動銷燬後再次打開的狀況,此時小程序須要從新加載啓動,就是 冷啓動web

1.2.2 銷燬

當小程序進入後臺必定時間,或系統資源佔用太高,或者是你手動銷燬,纔算真正的銷燬正則表達式

1.3 系統權限方面

H5最被詬病的地方在哪?系統權限不夠,好比網絡通訊狀態、數據緩存能力、通信錄、或調用硬件的,如藍牙功能等等一些APP有的功能,H5就沒有這些系統權限,由於它重度依賴瀏覽器能力,依舊是微信小程序舉例,微信客戶端的這些系統級權限均可以和微信小程序無縫銜接,官方宣稱擁有 Native App 的流暢性能。json

1.4 開發編碼層面

H5 開發你們都知道,標準的 HTML、CSS、JavaScript ,萬變不離其三劍客小程序不一樣, (Wechat / 支付寶 / 字節跳動 / 百度 / QQ 等)不一樣的小程序都有本身定義獨特的語言最經常使用的微信小程序,自定義的 WXML、WXSS,WXML 中所有是微信自定義的標籤,WXSS、JSON 和 JS 文件中的寫法都稍有限制,官方文檔中都有明確的使用介紹,雖容易上手,但仍是有區別的。小程序

1.5 更新機制方面

H5 的話想怎麼更新就怎麼更新,更新後拋開CDN/瀏覽器緩存啥的,基本上更新結束刷新就能夠看到效果 小程序不一樣,仍是微信舉例,嘿嘿,微信小程序更新啥的是須要經過審覈的。並且開發者在發佈新版本以後,沒法馬上影響到全部現網用戶,要在發佈以後 24 小時以內才下發新版本信息到用戶 小程序每次 冷啓動 時,都會檢查有無更新版本,若是發現有新版本,會異步下載新版本代碼包,並同時用客戶端本地包進行啓動,因此新版本的小程序須要等下一次 冷啓動 纔會應用上,固然微信也有 wx.getUpdateManager 能夠作檢查更新windows

1.6 渲染機制方面

H5就是 web 渲染,瀏覽器渲染。微信小程序的宿主環境是微信,宿主環境爲了執行小程序的各類文件:wxml文件、wxss文件、js文件,提供了雙線模型。後端

2、小程序環境分析

小程序的渲染層和邏輯層分別由兩個線程管理:

image.png

  • 渲染層的界面使用 WebView 進行渲染,一個小程序存在多個界面,因此渲染層存在多個 WebView。
  • 邏輯層採用 JSCore 線程運行 JavaScript 腳本。

這兩個線程間的通訊經由小程序 Native 側中轉,邏輯層發送網絡請求也經由 Native 側轉發。如此設計的初衷是爲了管控和安全,微信小程序阻止開發者使用一些瀏覽器提供的,諸如跳轉頁面、操做 DOM、動態執行腳本的開放性接口。將邏輯層與視圖層進行分離,視圖層和邏輯層之間只有數據的通訊,能夠防止開發者隨意操做界面,更好的保證了用戶數據安全。

三端的腳本執行環境以及用於渲染非原生組件的環境是各不相同的:

運行環境 邏輯層 渲染層
Android V8 Chromium 定製內核
IOS JavaScriptCore WKWebView
小程序開發者工具 NWJS Chrome WebView

運行環境邏輯層渲染層 AndroidV8Chromium 定製內核IOS JavaScriptCoreWKWebView 小程序開發者工具NWJSChrome WebView小程序的視圖是在WebView裏渲染的,那搭建視圖的方式天然就須要用到HTML語言。可是HTML語言標籤衆多,增長了理解成本,並且直接使用HTML語言,開發者能夠利用<a>標籤實現跳轉到其餘在線網頁,也能夠動畫執行 JAVAScript,前面所提到的爲解決管控與安全而創建的雙線程模型就成擺設了。

所以,小程序設計一套組件框架—— Exparser 。基於這個框架,內置了一套組件,以涵蓋小程序的基礎功能,便於開發者快速搭建出任何界面。同時也提供了自定義組件的能力,開發者能夠自行擴展更多的組件,以實現代碼複用。值得一提的是,內置組件有一部分較複雜組件是用客戶端原生渲染的,以提供更好的性能。

3、H5瀏覽器環境分析

你們都知道,瀏覽器緩存是個很是有用的特性,它可以提高性能、減小延遲,還能夠減小帶寬、下降網絡負荷。關於瀏覽器的緩存機制能夠總結成下面 2 句話:

  • 瀏覽器每次發起請求,都會先在瀏覽器緩存中查找該請求的結果以及緩存標識
  • 瀏覽器每次拿到返回的請求結果都會將該結果和緩存標識存入瀏覽器緩存中

更進一步,咱們能夠粗略瞭解一下強制緩存和協商緩存的運行機理。若強制緩存(Expires 和 Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified/If-Modified-Since 和 Etag/If-None-Match),協商緩存由服務器決定是否使用緩存,若協商緩存失效,那麼表明該請求的緩存失效,返回 200,從新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回 304,繼續使用緩存。這段文字是想讓讀者拓展一下知識面,若是想要更輸入瞭解,能夠經過上面的一些關鍵字(強緩存、協商緩存、Expire、Cache-Control 等)去查找更詳細的資料。 微信的 web-view 組件就是一個嵌在小程序裏的瀏覽器,它在緩存上並無徹底遵守上述的規則,也即它的緩存並不能及時獲得清理。想必下面的操做你們都有嘗試過:

  • 手動退出小程序,再次進入;
  • 將微信從後臺退出再打開並從新進入小程序;
  • 修改 Nginx 關於 Cache-Control 的配置;
  • 用 debugx5.qq.com 手動清除安卓微信瀏覽器緩存;
  • iOS 利用微信自帶清楚緩存功能。

沒法及時刷新緩存會致使發佈了最新的頁面,而小程序裏仍然是之前的頁面,從而會帶來許多問題,如先後端的數據不一致,新的特性沒法及時起做用,修改的問題沒有獲得解決等等。這裏須要說明一下:web-view 在過一段時間(時間不定,一天或者幾小時,無明顯規律)是能夠進行緩存刷新的,而本 Chat 要解決的是及時刷新的問題。

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

首先想到的就是onShow方法的實現,以前有人提議用visibilitychange來實現onShow方法。但調研事後,這種方式在ios中表現符合預期,可是在安卓手機裏,是不能按預期觸發的。因而就有了下面的方案,這個方案須要h5和小程序的webview都作處理。

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

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

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

3.2 注意點

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

瀏覽器訪問資源是經過 URL 地址,若是內嵌 H5 的地址不發生變化,那麼 web-view 訪問資源會從緩存裏取,而緩存裏並無最新的數據,這就致使了服務端的最新資源根本沒法到達瀏覽器,這也就解釋了爲何修改 Nginx 的 Cache-Control 配置也沒法生效的緣由。因此,要想完全解決及時刷新,必須讓 web-view 去訪問新的地址。咱們假定小程序訪問的 URL 地址爲:https://www.yourdomain.com/101/#/index其中 101 就是構建的一個版本號,每次遞增,保證次次不一樣便可。

3.4 如何判斷小程序當前頁面所處的環境

這部分須要在H5頁面種下一個sdk,好比名字就叫bridge.js,下面是我作了幾年小程序總結出來的經常使用方法:

// bridge.js
let ua = window.navigator.userAgent.toLowerCase();
const globalObj = {
    testDataArr: [],
    doJSReadyFuncExecuted: false,
    errorInfo: '',
    miniappSDK: null,
    miniappType: '',
    actionQueue: [],
    MINIAPP_TYPE: {
        WECHATMINIAPP:  'WECHATMINIAPP',// miniprogram
        WECHATAPP:      'WECHATAPP',    // miniprogram + offiaccount
        OLDQUICKAPP:    'OLDQUICKAPP',  // old
        NEWQUICKAPP:    'NEWQUICKAPP',  // new
        ALIPAYAPP:      'ALIPAYAPP',
        BAIDUAPP:       'BAIDUAPP',
        TOUTIAOAPP:     'TOUTIAOAPP',
        QQAPP:          'QQAPP'         // No longer maintained
    },
    JSSDK_URL_OBJ: {
        WECHATMINIAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        WECHATAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        OLDQUICKAPP: 'https://xxxxxxxx/amsweb/quickApp/mixBridge.js',
        NEWQUICKAPP: 'https://quickapp/jssdk.webview.min.js',
        ALIPAYAPP: 'https://appx/web-view.min.js',
        BAIDUAPP: 'https://b.bdstatic.com/searchbox/icms/searchbox/js/swan-2.0.21.js',
        TOUTIAOAPP: 'https://s3.pstatp.com/toutiao/tmajssdk/jssdk-1.0.1.js',
        QQAPP: 'https://qqq.gtimg.cn/miniprogram/webview_jssdk/qqjssdk-1.0.0.js'
    },
    bversion: '1.0.0'
}

if(typeof window['__bfi'] == 'undefined') {
    window['__bfi'] = [];
};

window['__bfi'].push([
    '_tracklog', 
    '174537', 
    `ua=${ua}&pageId=\${page_id}`
]);

function isAndroid () {
    return ua.includes('android');
}

function isWechatMiniapp () {
    // @source https://developers.weixin.qq.com/community/develop/doc/00022e37c78b802f186750b5751000
    // in wechat && (in android || in ios)
    return isWechat() && (ua.includes('miniprogram') || window.__wxjs_environment === 'miniprogram');
}

function isWechat () {
    // in wechat-web-browser
    // https://blog.csdn.net/daipianpian/article/details/86543080
    // @source blog ( https://www.jianshu.com/p/6a10f833b099 )
    return /micromessenger/i.test(ua) || /windows phone/i.test(ua);
}

function isOldQuickapp () {
    return (/(hap|OPPO\/Hybrid)\/\d/i.test(ua)) && !isNewQuickapp();
}

function isNewQuickapp () {
    // @source 2020.04.10, Vivo( Li Chunjiao ) has confirmed that this method is feasible
    return ua.includes('mode-quickapp');
}

function isAlipay () {
    // @source 2020.06.15, Alipay has confirmed that this method is feasible
    let isAli = (/APXWebView/i.test(ua)) || (/MiniProgram/i.test(ua) && !ua.includes('micromessenger'));
    // @source 2020.11.17, https://www.yuque.com/books/share/6d822c34-9121-47d8-a805-4c57b0b2d2f0/hiv1tn
    let isUCKuake = ua.includes('aliapp') && (ua.includes('uc') || ua.includes('quark'));
    // @source 2021.03.26
    let isGaode = ua.includes('aliapp') && ua.includes('amapclient');
    return isAli || isUCKuake || isGaode;
}

function isBaidu () {
    // @source 2020.11.05, baidu's doc ( https://smartprogram.baidu.com/docs/develop/component/open_web-view/ )
    return /swan\//.test(ua) || /^webswan-/.test(window.name);
}

function isToutiao () {
    // @source 2020.11.05, toutiao's doc ( https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/component/open-capacity/web-view/ )
    return ua.includes("toutiaomicroapp");
}

function isQQ () {
    // @source 2021.04.21, add ua.includes('miniprogram'), qq's doc ( https://q.qq.com/wiki/develop/miniprogram/component/open-ability/web-view.html )
    return ua.includes('qq') && ua.includes('miniprogram');
}

// return miniapp type of the environment
function isMiniProgram () {
    let appType = false;
    let typeNameObj = globalObj.MINIAPP_TYPE

    try {
        if (isWechatMiniapp()) {
            appType = typeNameObj.WECHATMINIAPP;
        } else if (isOldQuickapp()) {
            appType = typeNameObj.OLDQUICKAPP;
        } else if (isNewQuickapp()) {
            appType = typeNameObj.NEWQUICKAPP;
        } else if (isAlipay()) {
            appType = typeNameObj.ALIPAYAPP;
        } else if (isBaidu()) {
            appType = typeNameObj.BAIDUAPP;
        } else if (isToutiao()) {
            appType = typeNameObj.TOUTIAOAPP;
        } else if (isQQ()) {
            appType = typeNameObj.QQAPP;
        }

        console.log('判斷所處環境,isMiniProgram 返回值: ', appType);
        window['__bfi'].push([
            '_tracklog', 
            '174537', 
            `api_name=isMiniProgram&miniappType=${appType}&pageId=\${page_id}`
        ]);

        return appType;
    } catch (e) {
        window['__bfi'].push([
            '_tracklog', 
            '174537', 
            `api_name=isMiniProgram&err_msg=${e.message}&err_stack=${e.stack}`
        ]);
        return false; // 'catch error'
    }
}

export {
    isAndroid,       // 判斷H5頁面是否處於安卓系統
    isWechatMiniapp, // 判斷H5頁面是否處於微信小程序環境
    isWechat,        // 判斷H5頁面是否處於微信環境
    isOldQuickapp,   // 判斷H5頁面是否處於【老版快應用】小程序環境
    isNewQuickapp,   // 判斷H5頁面是否處於【新版快應用】小程序環境
    isAlipay,        // 判斷H5頁面是否處於支付寶小程序環境
    isBaidu,         // 判斷H5頁面是否處於百度小程序環境
    isToutiao,       // 判斷H5頁面是否處於頭條小程序環境
    isQQ,            // 判斷H5頁面是否處於QQ小程序環境
    isMiniProgram    // 返回H5頁面所處環境的應用名
}
複製代碼

使用時的注意事項

使用前,最好查閱相應小程序的文檔,由於各個小程序對API的支持程度不一樣。js文件的引用不能放在裏,bridge.js 裏面對當前頁面的head進行操做了。由於 bridge.js 引入JSSDK的方式是 爲 head標籤添加 script標籤,若在 head標籤中引入bridge.js,就會報錯。

若打開h5,顯示「頁面訪問受限」之類的提示信息,可嘗試下方的操做:(這種狀況,通常是打開測試環境的h5 url 時出現)勾選IDE中的「忽略webview域名合法性檢查」 和 「忽略request域名合法性檢查」。

【快應用相關】 目前Vivo,Oppo,華爲三家廠商已支持新版快應用,VivoOPPO已上線,小米不支持。對於新版快應用,若H5頁面須要調用新版快應用JS-SDK中提供的API,須要提早將該H5連接的域名配置到可信任的網址裏(應寫成正則表達式的形式進行配置)。

【頭條相關】 頭條小程序的redirectTo、navigateTo 等頁面跳轉的 api 只支持 url 爲 / 開始的絕對路徑

【支付寶相關】 目前的1.0.73版 bridge.js 判斷是否處於支付寶小程序的方法,會將h5處於支付寶小程序、h5處於支付寶內置瀏覽器都判斷爲處於支付寶小程序內。所以,在調my.XXXX以前,須要先調判斷環境工具函數 判斷一下,確保確實是處於支付寶小程序內,而非支付寶內置瀏覽器內。

3.5 小程序獲取最新版本號

在小程序中,咱們利用 app 的 onShow 鉤子函數來完成最新的 URL 獲取,同時還要保證只有獲取了版本號以後才能加載其餘的頁面,所以這裏要用到同步接口調用。請參考下面代碼:

//這裏加入同步請求到服務器獲取最新路徑
onShow: function (options) {
    this.getFEVersion()
},
getFEVersion: function () {
    //下面是利用Promise進行同步調用的寫法
    return new Promise(function (resolve, reject) {
      wx.request({
        //下面是本機調試的一個地址,上線時請改爲本身服務端的地址
        url: 'http://192.168.0.168:8090/getFEVersion',
        data: {},
        method: 'POST',
        header: {
          'content-type': 'application/json',
        },
        success: function (res) {
          if (res.data.success) {
            const app = getApp();
            //res.data.version 是從服務端返回的最新fe的版本號,即上面的數字101
            app.globalData.feUrl = 'https://www.yourdomain.com/' + res.data.version + '/#/index'
          }
          resolve();
        },
        fail: function (error) {
          console.log(error);
          reject();
        }

      })
    });
  },
webview動態處理
/** * @file 根據入參的小程序類型,動態加載相應的 JavaScript文件 * 指定<script>元素的src屬性,指定事件處理程序(onload事件 onerror事件) */

const globalObj = {
    testDataArr: [],
    doJSReadyFuncExecuted: false,
    errorInfo: '',
    miniappSDK: null,
    miniappType: '',
    actionQueue: [],
    MINIAPP_TYPE: {
        WECHATMINIAPP:  'WECHATMINIAPP',// miniprogram
        WECHATAPP:      'WECHATAPP',    // miniprogram + offiaccount
        OLDQUICKAPP:    'OLDQUICKAPP',  // old
        NEWQUICKAPP:    'NEWQUICKAPP',  // new
        ALIPAYAPP:      'ALIPAYAPP',
        BAIDUAPP:       'BAIDUAPP',
        TOUTIAOAPP:     'TOUTIAOAPP',
        QQAPP:          'QQAPP'         // No longer maintained
    },
    JSSDK_URL_OBJ: {
        WECHATMINIAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        WECHATAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        OLDQUICKAPP:'https://xxxxxxxxx/amsweb/quickApp/mixBridge.js',
        NEWQUICKAPP: 'https://quickapp/jssdk.webview.min.js',
        ALIPAYAPP: 'https://appx/web-view.min.js',
        BAIDUAPP: 'https://b.bdstatic.com/searchbox/icms/searchbox/js/swan-2.0.21.js',
        TOUTIAOAPP: 'https://s3.pstatp.com/toutiao/tmajssdk/jssdk-1.0.1.js',
        QQAPP: 'https://qqq.gtimg.cn/miniprogram/webview_jssdk/qqjssdk-1.0.0.js'
    },
    bversion: '1.0.0'
}

let n = 0;
function loadListener (type) {
    // 先執行一次,再進入setTimeout
    // 多加幾個埋點,記錄不一樣類型的信息
    console.log(`====== 重試次數:${n} ======`);
    if(n === 0) {
        processAddRes(type);
    } else {
        setTimeout(function () {
            processAddRes(type);
        }, 200)
    }
}

function processAddRes(type) {
    let curMiniappType = globalObj.miniappType;
    let curLoadJsUrl = globalObj.JSSDK_URL_OBJ[curMiniappType];

    if(!addJSSDKToGlobalObj()){
        n++;

        loadListener();

        if(n % 10 === 0) {
            const msg = `重試達到【${n}】次`
            console.log(msg);
            console.log(globalObj.errorInfo || '======');
        }
        return;
    }

    let actionQueue = globalObj.actionQueue;
    if (actionQueue && actionQueue.length) {
        let aItem = null;
        while (aItem = actionQueue.shift()) {
            try {
                globalObj.miniappSDK[aItem.apiName].apply(globalObj.miniappSDK, aItem.args)
            } catch (e) {
                //
            }
        }
    }
}

// 將JSSDK提供的方法保存到global
function addJSSDKToGlobalObj () {
    let curMiniappType = globalObj.miniappType;

    try{
        let _miniappSDK = null;
        switch(curMiniappType) {
            case 'WECHATMINIAPP':
            case 'WECHATAPP':
            case 'OLDQUICKAPP':
                _miniappSDK = typeof wx !== 'undefined' && wx.miniProgram;
                break;
            case 'NEWQUICKAPP':
                _miniappSDK = qa;
                break;
            case 'ALIPAYAPP':
                _miniappSDK = my;
                break;
            case 'BAIDUAPP':
                _miniappSDK = typeof swan !== 'undefined' && swan.webView;
                break;
            case 'TOUTIAOAPP':
                _miniappSDK = typeof tt !== 'undefined' && tt.miniProgram;
                break;
            case 'QQAPP':
                _miniappSDK = typeof qq !== 'undefined' && qq.miniProgram;
                break;
        }

        if(_miniappSDK) {
            globalObj.miniappSDK = _miniappSDK
        }

        if (!globalObj.miniappSDK || !globalObj.miniappSDK.navigateTo) {
            console.log(globalObj)
            let g_errmsg = (!globalObj.miniappSDK ? 'miniappSDK_is_undefined' : 'API_is_undefined');
            let g_errstack = 'none'
            globalObj.errorInfo = 'g_errmsg=' + g_errmsg + '&g_errstack=' + g_errstack;

            return false;
        }
    } catch (e) {
        // 記錄下是什麼緣由return的false: 在return false 的地方,將緣由掛到全局變量上,loadListener觸發埋點時,記錄下來
        globalObj.errorInfo = 'g_errmsg=' + e.message + '_have_catch_error' + '&g_errstack=' + e.stack;

        return false;
    }
    globalObj.errorInfo = 'g_errmsg=outof_try-catch_return_true';

    return true;
}

function parseQuery(url) {
    let query = {};
    let idx = url.indexOf("?");
    let str = url.substr(idx + 1);
    if (str == "" || idx == -1) {
        return {};
    }
    let pairs = str.split('&');
    for (let i = 0; i < pairs.length; i++) {
        let pair = pairs[i].split('=');
        // 當根據 = 號分割後有多條數據時,從數組第1位起以後的要所有保留。
        // 好比 src=/issue/create?type=1752,要處理成爲:src: '/issue/create?type=1752',而不是 src: '/issue/create?type'
        if (pair.length > 2) {
            pair[1] = pair.slice(1).join('=');
        }
        query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
    }
    return query;
};

function loadScript() {
    // 埋點信息,增長加載的jssdk-url,後續可能能夠從url中獲取到微信的版本號
    // 設備品牌、設備型號、微信版本號、操做系統及版本、客戶端平臺、客戶端基礎庫版本 Object wx.getSystemInfoSync()
    let curMiniappType = globalObj.miniappType;
    let curLoadJsUrl = globalObj.JSSDK_URL_OBJ[curMiniappType];

    let jSBridgeReady = function(type) {
        console.log('jSBridgeReady, event type: ', type);

        // 保證後續邏輯只會執行一次
        if (globalObj.doJSReadyFuncExecuted) {
            return;
        }
        globalObj.doJSReadyFuncExecuted = true;
        console.log('script is onload, doJSReadyFuncExecuted')

        loadListener(type);
    }


    if (curMiniappType === "WECHATMINIAPP" || curMiniappType === "WECHATAPP" || curMiniappType === "OLDQUICKAPP") {
        // 監聽WeixinJSBridgeReady 和 onload 前,發個埋點,看下當前是否已經有wx 和 wx.miniProgram(由於目前nfes 只引入了微信jssdk)
        document.addEventListener('WeixinJSBridgeReady', function() {
            console.log('WeixinJSBridgeReady ======');
            jSBridgeReady('WeixinJSBridgeReady')
        }, false)
    }

    if (curMiniappType === "NEWQUICKAPP") {
        document.addEventListener('QaJSBridgeReady', function() {
            console.log('QaJSBridgeReady ======');
            jSBridgeReady('QaJSBridgeReady')
        }, false)
    }

    let script = document.createElement("script");
    script.src = curLoadJsUrl;
    script.async = false; // 註釋掉,由於添加async的話,執行順序沒法保證

    let scriptArr = document.getElementsByTagName('script');
    console.log(scriptArr);

    for(let i = 0; i < scriptArr.length; i++) {
        let item = scriptArr[i];
        if(item.src.includes('/ares2/market/mixappBridge/') && item.src.includes('/default/bridge')) {
            // 取參數,動態設置async
            let queryObj = parseQuery(item.src); // 兜底的值爲 {}
            console.log('queryObj: ', queryObj);
            if(typeof queryObj.bridgeAsync !== 'undefined') {
                script.async = queryObj.bridgeAsync === '1' ? true : false;
            }
        }
    }
    console.log('最終,script.async: ', script.async);

    script.onload = function(e) {
        console.log('script is onload ======')
        jSBridgeReady('onload')
    }

    script.onerror = function(e) {
        console.log('script is onerror')
    }

    window.onerror = function(message, source, lineNo, columnNo, error) {
        // to do track
    }
    document.getElementsByTagName('head')[0].appendChild(script)
}

export {
    loadScript
}
複製代碼

工做中小程序webview業務細節總結

5.1 區分環境

微信提供了一個環境變量,加載h5之後第一個頁面能夠及時拿到,但後續的頁面都須要在微信的sdk加載完成之後才能拿到,所以建議你們在wx.ready或者是weixinjsbridgeready事件裏面去判斷,區別就在於前者須要加載jweixin.js纔有,但這裏有坑,坑在於h5的開發者可能並不知道你這個檢測過程須要時間,是一個異步的過程,他們可能頁面一加載就須要調用一些api,這時候就可能會出錯,所以你必定要提供一個api調用的隊列和等待機制。具體作法見上面代碼。

5.2 支付

第二個常見問題是支付,由於小程序webview裏面不支持直接調起微信支付,因此基本上須要支付的時候,都須要來到小程序裏面,支付完再回去。上面作好了之後,在h5這塊調用就一句話就能夠了。針對產品有大量內嵌H5頁面的狀況下,最好根據業務分兩種支付頁面,一是有的業務h5有本身完善的交易體系,下單動做在h5裏面就能夠完成,他們只須要小程序付款,所以咱們有一個精簡的支付頁,進來直接就拉起微信支付,還有一種狀況是業務須要小程序提供完整的下單支付流程,那麼久能夠直接進入咱們小程序的收銀臺來,圖上就是sdk裏面的基本邏輯,咱們經過payOnly這個參數來決定進到哪一個頁面。

咱們再看下小程序裏面精簡支付怎麼實現的,就是onload以後直接調用api拉起微信支付,支付成功之後根據h5傳回來的參數,若是是個小程序頁面,那直接跳轉過去,不然就刷新上一個webview頁面,而後返回回去。

5.3 左上角返回

那怎麼解決這種流失呢,咱們加了一個左上角返回的功能。首先進入的是一個空白的中轉頁,而後進入h5頁面,這樣左上角就會出現返回按鈕了,當用戶按左上角的返回按鈕時候,頁面會被重載到小程序首頁去,這個看似簡單又微小的動做,對業務其實有很大的影響,咱們看兩個數字,通過咱們的數據統計發現,左上角返回按鈕點擊率高達70%以上,由於這種落地頁通常是被用戶分享出來的,之前純h5的時候只能經過左上角返回,因此在小程序裏用戶也習慣如此,第二個數字,重載到首頁之後,後續頁面訪問率有10%以上,這兩個數字對業務提高其實蠻大的。其實現原理很簡單,都是經過第二次觸發onShow時進行處理。

Q: 可能出現的登陸登出同步問題

A: 跳到我的頁登陸完成,此時是新開的webview同步兩端登陸態,點返回,到上一個webview,此時這個webview嵌套的首頁,沒有觸發react-imvc onshow事件。這個頁面是老的,退出登陸也是同樣,因此在首頁會去跳h5的登陸而不是小程序登陸,致使登陸態不一樣步。 解決思路:須要返回首頁刷一下h5頁面。

誤區:直接在我的登陸以後,relaunch到首頁,會致使沒有直接調用註銷webview把token置換,沒法退出 解決方案:判段從我的頁返回的時候,設置webview的url加個參數,從新刷一下。

相關文章
相關標籤/搜索