攜程小程序內嵌WebView實踐指南
作者簡介
思語,攜程高級前端開發工程師,關注互動營銷領域;
Olivio,攜程高級前端開發工程師,關注React Node 組件化;
Stone,攜程高級研發經理,關注跨端解決方案,云原生落地等領域。
一、背景
這篇文章將向大家分享團隊在小程序 webview 方面的開發心得,以微信小程序為主要環境,介紹在業務開發中處理小程序webview內嵌H5所遇到的問題及解決方案。具體將從小程序平臺與H5差異、小程序內嵌webview通信、小程序webview常見問題展開敘述。
二、平臺差異
下面將淺析并回顧一下小程序和H5在渲染方面的幾點差異。
2.1 小程序方面
以微信小程序為例,相信今天大部分的讀者對微信小程序的系統架構都比較熟悉了,總體來講分為兩部分:
- iew 視圖端通過小程序的框架,將用戶采用 WXML 和 WXSS 描述的UI信息處理成 H5 元素,最終交給 WebView 去渲染;
- 邏輯層運行JS邏輯,并且可以調用具有微信開放能力的 JSAPI。邏輯和視圖分離,通過事件和數據彼此之間建立聯系。
微信小程序使用 WebView 渲染,與原生客戶端的是兩套不同的視圖渲染體系。一個小程序存在多個界面,所以渲染層存在多個 WebView。邏輯層采用 JSCore 線程運行 JavaScript 腳本。這兩個線程間的通信經由小程序 Native 側中轉,邏輯層發送網絡請求也經由 Native 側轉發。
如此設計的初衷是為了管控和安全,微信小程序阻止開發者使用一些瀏覽器提供的,諸如跳轉頁面、操作 DOM、動態執行腳本的開放性接口。將邏輯層與視圖層進行分離,視圖層和邏輯層之間只有數據的通信,可以防止開發者隨意操作界面,更好地保證了用戶數據安全。同時小程序設計一套組件框架—— Exparser ,基于這個框架內置了一套組件,以涵蓋小程序的基礎功能,便于開發者快速搭建出任何界面,同時也提供了自定義組件的能力,開發者可以自行擴展更多的組件,以實現代碼復用。
值得一提的是,內置組件有一部分較復雜組件是用客戶端原生渲染的,同時微信團隊又通過結合 Flutter 和 LV-CPP,把實現代碼收斂在 C++ 和 Dart 上,進一步簡化了基于小程序技術棧實現跨平臺業務開發的框架維護成本,以提供更好的性能。
2.2 小程序WebView內嵌H5
H5頁面投放在小程序WebView,在配置完合法域名后,即可在小程序應用中展示。那么,針對不同廠商小程序,可能法務、廠商合規有所差異,需要H5判斷所在的環境,去調用不同 api 方法,展示不同的業務頁面。
在攜程內部封裝了小程序CWX的SDK,小程序端主要采用原生+Taro框架,H5這塊主要是NFES(React)和Vue,無論是哪一段端都通過一個CWX來連接,內部封裝了各端通用的功能比如登錄、發布、支付、個人中心等功能,這些功能都可以直接通過CWX這個中間件進行調用。
并且,H5在檢測到當前處于小程序webview環境下時,會根據環境異步加載SDK文件、及其廠商的JS-SDK,初始化小程序版本wx.config。這里的關鍵點是我們要做個api調用的隊列,因為sdk加載異步的過程,如果期間頁面內發生了api調用,那肯定得不到正確的響應。因此要做個調用隊列,當sdk初始化完畢之后再處理這些調用。其實CWX原理很純粹,如果你想實現多端適配,那么只需要根據所在的環境去加載不同的sdk就可以了。
> 下面簡要列舉一下工作中常用的幾個小程序環境判斷:
使用時的注意事項:
使用前,最好查閱相應小程序的文檔,因為各個小程序對API的支持程度不同。引用bridge.js的方式視情況而定,因為 bridge.js 引入JSSDK的方式是 為 head標簽添加 script標簽,若在 head標簽中引入bridge.js,就會報錯若打開h5,顯示“頁面訪問受限”之類的提示信息,可嘗試下方的操作:(這種情況,一般是打開測試環境的h5 url 時出現)勾選IDE中的“忽略webview域名合法性檢查” 和 “忽略request域名合法性檢查”。
【快應用相關】
目前Vivo、Oppo、華為三家廠商已支持新版快應用,Vivo、OPPO已上線,華為正在測試中,小米不支持。對于新版快應用,若H5頁面需要調用新版快應用JS-SDK中提供的API,需要提前將該H5鏈接的域名配置到可信任的網址里(應寫成正則表達式的形式進行配置)。
【頭條相關】
頭條小程序的redirectTo、navigateTo 等頁面跳轉的 api 只支持 url 為 / 開始的絕對路徑。
【支付寶相關】
目前的1.0.73版 bridge.js 判斷是否處于支付寶小程序的方法,會將h5處于支付寶小程序、h5處于支付寶內置瀏覽器都判斷為處于支付寶小程序內。因此,在調my.XXXX之前,需要先調環境工具函數判斷一下,確保確實是處于支付寶小程序內,而非支付寶內置瀏覽器內。
三、小程序內嵌WebView通信
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部分,帶有 __isnotallow=1 再進行通過hash方式傳參。改造原有邏輯,讓__isnotallow=1時,hash處理邏輯優先級最高參數定義,在前面加入了兩個下劃線,目的是為了區分url中正常的參數。我們來看看h5端的sdk是怎么實現的。
總結下來是兩點:
- 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 就是構建的一個版本號,每次遞增,保證次次不同即可。
四、WebView常見難題與解決方案
小程序和h5 之間的通信基本上常用兩種方式,一個是postMessage,這個方法大家都知道,只有在三種情況才可以觸發,后退、銷毀和分享。但也有個問題,就是需要注意這個方法是基礎庫1.7.1才開始支持的,1.7.1以下就只能通過第二種方法來傳遞數據,也就是設置和檢測webview組件的url變化,類似pc時代的iframe的通信方式。
sdk這塊怎么做呢,定義一個share方法,首先需要檢測下基礎庫版本,看是否支持postMessage,如果支持直接調用,如果不支持,把分享參數拼接到url當中,然后進行一次重載。也就是說,通過url傳遞數據有個缺點,就是頁面可能需要刷新一次才能設置成功。
目前在webview環境下支持支持的幾種通用業務:
4.1 左上角返回
在訪問小程序webview頁面時,首先進入的是一個空白的中轉頁,然后進入h5頁面,這樣左上角就會出現返回按鈕了,當用戶按左上角的返回按鈕時候,頁面會被重載到小程序首頁去,這個看似簡單又微小的動作,對業務其實有很大的影響。
經過我們的數據統計發現,左上角返回按鈕點擊率高達70%以上,因為這種落地頁一般是被用戶分享出來的,以前純h5的時候只能通過左上角返回,所以在小程序里用戶也習慣如此;第二個數字,重載到首頁以后,后續頁面訪問率有10%以上,這兩個數字對業務提升其實蠻大的。其實現原理很簡單,都是通過第二次觸發onShow時進行處理。
4.2 H5和小程序登錄態同步問題
分兩種情況,接入的H5可能一開始就需要登錄,也可能開始不需要登錄態中途需要登錄,這兩種情況我們約定了h5通過自己的url上一個參數進行控制。
一開始就需要登錄態的情況,具體來講就是在加載webview之前,首先進行授權登錄,然后把登錄信息拼接到url里面,再去來加載webview,在h5里面通過adapter來把登錄信息提取出來并且存到cookie里,這樣h5一進來就是有登錄態的。
一開始不需要登錄態的情況,一進入小程序就直接通過webview加載h5,h5調用login方法的時候,把needLogin這個參數拼接到url中,然后利用api進行重載,就走了第一種情況進行授權登錄了。
Q:可能出現的登錄同步問題
A: 跳到個人頁登錄完成,此時是新開的webview同步兩端登錄態,點返回,到上一個webview,此時這個webview嵌套的首頁,沒有觸發react-imvc onshow事件。這個頁面是老的,退出登錄也是一樣,所以在首頁會去跳h5的登錄而不是小程序登錄,導致登錄不同步。
解決思路:需要返回首頁刷一下h5頁面。
誤區:直接在個人登錄之后,relaunch到首頁,會導致沒有直接調用注銷webview把token置換,無法退出。
解決方案:判斷從個人頁返回的時候,設置webview的url加個參數,重新刷一下。
4.3 WebView分享
在沒接入websocket之前,小程序主要通過bind。首先通過bindmessage事件接收h5傳回來的數據,然后在用戶分享的時候onShareAppMessage判斷有沒有回傳的數據,如果沒有就到webviewurl當中取,否則就是用默認分享數據。
4.4 支付
1)WebView頁面刷新問題
因為小程序webview里面不支持直接調起微信支付,所以基本上需要支付的時候,都需要來到小程序里面,支付完再回去。上面做好了以后,在h5這塊調用一句話就可以了。
針對產品有大量內嵌H5頁面的情況下,最好根據業務分兩種支付頁面,一是有的業務h5有自己完善的交易體系,下單動作在h5里面就可以完成,他們只需要小程序付款,因此我們有一個精簡的支付頁,進來直接就拉起微信支付。
還有一種情況是業務需要小程序提供完整的下單支付流程,通過直接進入小程序的收銀臺來,圖上是sdk里面的基本邏輯,通過payOnly這個參數來決定進到哪個頁面。再看下小程序里面精簡支付怎么實現的,onload之后直接調用api拉起微信支付,支付成功以后根據h5傳回來的參數,如果是個小程序頁面,直接跳轉過去,否則就刷新上一個webview頁面,然后返回回去。
新的問題與挑戰:webview返回上一頁數據刷新問題
有客戶反饋在A頁面點擊任務后跳轉到B頁面,待任務完成后,手機手勢左滑返回或點擊默認導航欄的左上角返回,上一個頁面不會觸發任務的更新。原因是上一個頁面已經初始化并沒有執行重渲染,在APP環境下JSBridge 沒有提供偵聽手勢左滑返回、左上角物理返回的回調事件,且在小程序webview頁面也會遇到上述同樣的情況。
由于微信并沒有提供偵聽手勢左滑返回、左上角物理返回的,且webview頁面也不支持自定義導航欄,這導致下一個頁面觸發的新事件,在返回上個頁面時 無法做到針對性的更新。前期可以簡單粗暴地通過約定參數 doRefreshWhileBack=true 作為options,來通過webview頁面每次onShow刷新頁面,但是刷新整個頁面的成本太大,且用戶體驗不好。
2)引入WebSocket?
帶著這些疑問,我們進行一系列的嘗試與試驗,最終采用了 websocket 的方式,解決并封裝出我們市場業務的輕量的websocket服務,主要用于解決webview跨頁面通信和游戲方面的業務。
在這個過程中,我們總結出了一些經驗,希望能給從事相關研究的同學帶來一些幫助。上述做法是針對不同的應用環境,分別使用或約定不同的api派發給各自的事件系統,從而解決頁面物理回退時頁面不主動刷新的方案。
簡要介紹一下websocket,websocket標準誕生于2011年,RFC 文檔編號是 6455。TML 5 規范定義了 WebSocket 協議,它可以通過 HTTP 的端口(或者 HTTPS 的端口)來完成,從而最大程度上對 HTTP 協議通透的防火墻保持友好。但是,它是真正的雙向、全雙工協議,也就是說,客戶端和服務端都可以主動發起請求,回復響應,而且兩邊的傳輸都互相獨立。和上文的 Comet 不同,WebSocket 的服務端推送是完全可以由服務端獨立、主動發起的,因此它是服務端的"真 Push"。
WebSocket 是一個可謂"科班出身"的二進制協議,也沒有那么大的頭部開銷,這樣就解決了接線員要反復解析HTTP協議,還要查看identity info的信息,因此它的傳輸效率更高。同時,和 HTTP 不一樣的是,它是一個帶有狀態的協議,雙方可以約定好一些狀態,而不用在傳輸的過程中帶來帶去。而且,WebSocket 相比于 HTTP,它沒有同源的限制,服務端的地址可以完全和源頁面地址無關,即不會出現的瀏覽器"跨域問題"。
優勢:?
- 消息實時:真正的雙向、全雙工協議,完全的服務端推送保證了數據的時效性。
- 通信高效:可以由客戶端和服務端主動發送請求,不會像輪詢那樣產生大量無效傳輸報文。
- 協議支持:標準誕生較早,瀏覽器支持度高,且沒有同源策略的限制。
劣勢:
- 開發與維護成本:服務器長期維護長連接需要一定的成本,且受網絡限制比較大,需要處理好重連。
借助websocket的輔助,在小程序webview內嵌H5的業務場景中,可做的事情就更多了。在市場的webview容器加載流程中。
3)WebSocket背景下的WebView通信實踐
小程序webview初始化并在onLoad階段通過 options.useMktsocket 判斷是否需要加載 socket,同時判斷應用環境通過 wx.connectSocket api 連接不同的 socket 服務;
初始化webview socket服務,接受服務器消息-對服務器消息進行甄別,如果H5頁面通過socket傳遞給webview容器的數據data格式符合預期,且H5環境下登錄態中的openId與小程序環境一致,則認為此次通信合法;
webview容器中綁定了 小程序分享miniShare 、小程序訂閱openScribe、 健康檢查health等常用業務API,用于處理廣告、訂閱、任務更新等業務實時回調;H5業務可通過此接口設置觸發小程序原生頁面的一些原生功能,為上層業務提供服務。
H5頁面就可以通過 socket 通信更改并調用小程序的膠囊欄分享、通知webview容器頁面調用小程序廣告、也可以調用喚起小程序頁面中的分享組件面板、觸發左上角物理返回時及時通知H5頁面觸發回調等諸多業務;同時小程序容器頁面原生事件完成后(比如廣告、分享)再次通過socket返回給H5頁面的回調,實現小程序webview跨頁面的實時通信。
在websocket加持下,此時的小程序webview賦予了更多和H5通信的功能。
4.5 自定義分享面板
H5頁面可以通過 websocket 通信更改并調用小程序的分享參數,不再依賴于頁面options參數,可以調用在webview頁面封裝的分享面板,提供更加靈活的分享方式。
4.6 H5調用小程序原生的激勵廣告
H5頁面可以通過 WebSocket 通信調用小程序原生的激勵廣告。
4.7 任務體系中用戶任務組件狀態的更新
用戶在訪問加載了webview-h5的頁面會與websocket的server A服務器連接、小程序原生頁面與server B連接時,這兩個頁面因為在不同的容器下,所以無法通信和告知;但是只要這兩個頁面加載的是同一個市場的websocket服務,服務端可以設置共享一個redis,通過redis的發布訂閱功能,連通集群內部各個機器,那么在頁面前進、回退時都可以綁定對應的回調事件,實現任務組件的靈活更新,給用戶展示最新的任務狀態。
五、總結
在處理小程序webview的業務方面,可以通過封裝一個包含各端環境的SDK,在H5初始化時加載,打通H5和小程序webview之間的通道,實現H5控制分享、登錄態同步、支付信息同步等功能。
在遇到跨頁面數據刷新問題時,借助了websocket這把利器,通過redix的發布訂閱通知鏈接了websocket服務器的頁面,實現小程序webview物理返回上一頁而數據不刷新的問題,同時websocket使得H5與webview的通信更加便捷靈活,拓展了H5調用小程序原生激勵廣告、封裝并調用小程序原生的分享面板等功能。
【參考文獻】
- 《WebSockets 教程》,鏈接:https://www.tutorialspoint.com/websockets/