您好,登錄后才能下訂單哦!
小程序webview的現狀
h6頁面在小程序中的交互(跳轉)場景
主要痛點
在完成相關操作后, 頁面狀態需要更新 ,目前常見的更新方式有如下兩種:
第一種方案,功能上沒有問題,但會導致頁面刷新,如果頁面操作復雜,需要多次刷新
第二種方案,正向操作時體驗比方案一好,但導致了另外一個問題:操作 跳轉層級過深 ,尤其返回的時候簡直讓人崩潰。
小程序中,h6頁面打開新頁面方式
我們先來看下小程序中常見的h6跳h6的方式:
我們采用的是方式3,理由如下:
由于這種方案可能會達到小程序的10層限制。所以在一些重要頁面建議加入“ 回到首頁 ”的操作,通過這個操作來縮短小程序歷史棧
回到首頁方案簡述
(如果不感興趣這部分可以直接略過)
wx.miniProgram.reLaunch({ url: '/pages/webview/bridge?url=項目首頁地址' })
先聲明,我們webview的路徑是/pages/webview/webview
/pages/webview/bridge是個中轉頁,有如下特點: 該頁面并 不是最終打開h6頁面的webview頁 ,而是一個 中轉頁。
主要用作返回處理
這個中轉頁:主要保證reLaunch到某h6頁面后,用戶仍然可以點擊返回到小程序首頁。
該方案通常用于:小程序中內嵌了多個業務線的h6頁面這種場景。
一個內容發布場景
我們從首頁進入發布頁,完成發布后,跳轉至商品詳情頁
那么對于一個新用戶來講,整個操作過程是這樣的:
這個場景就是同一個頁面,里面不同的內容項需要跳轉不同的頁面去操作,然后再回到原來頁面更新狀態的問題。
假如商品詳情頁沒有“回到首頁”的入口,那么這個用戶要想回到首頁。。。需要按8次“返回” = =!
經過這個體驗后,我想一般的用戶是沒有勇氣再發布內容的。
當然也有另一種這種折中方案
就是商品提到的,在連接中加入某個標志位,比如在url中加入__isonshowrefresh=1,webview在打開連接時候,會去讀取這個參數,如果有,則每次在onShow時候,重新加載url,通過刷新頁面進行頁面狀態更新。
這個體驗也不爽,就是在復雜的頁面會多次刷新。
聲明
我下面要講的這個方案并不是停留在設想階段,它已經在線上跑了
想看效果的朋友,可以在微信小程序中搜:
“轉轉二手交易網”-“0元免費領”-(底部)“送閑置賺星星”-進入到發布頁后
分類(跳轉h6,選中內容后返回,將參數傳給之前的h6)
取件地址(跳轉native原生地址選擇,選中后返回,將參數傳給之前的h6)
OK,我們進入今天的主題
小程序中h6頁面onShow和跨頁面通信的實現
首先想到的就是onShow方法的實現,之前有人提議用visibilitychange來實現onShow方法。
但調研過后,這種方式在ios中表現符合預期,但是在安卓手機里,是不能按預期觸發的。所以該方案被我否了。
于是就有了下面的方案
原理介紹
這個方案需要h6和小程序的webview都做處理。
核心思想: 利用webview的hash特性
為什么要執行window.history.go(-1)
這一步是整個方案的精髓:
方案延伸(跨頁面數據傳遞)
小程序里另個一常見的場景就是調用第三業務(或者己方業務),在做完某些操作后需要把選中的數據帶回之前的頁面。
如前面提到的例子:發布頁,需要選擇發布類型,然后返回,發布頁發布類型局部更新
當然有些同學會說:我可以用setInterval,監控localStorage。在新頁面選中內容后,設置localStorage,然后在返回不就可以了。
我這里說的是 通用方案 。如果頁面都是由己方業務線維護的當然可以隨便折騰。
但是一旦涉及到第三方業務線,尤其不同域名頁面的業務調用,這種通信方式就尷尬了。
那我的方案怎么處理呢,我總結了一張圖
我們來解讀一下這張圖:
整個過程就是這樣
代碼示意:
小程序
小程序webview要先做幾方面考慮:
小程序端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; // 獲取其他頁面經過操作后,需要傳遞給h6的參數 let data = routeParams.getBackFromData() || {}; // webview頁面狀態更新 if(this.pageShowCount > 1 && this.mQuery.__isonshowpro && this.mQuery.__isonshowpro === '1' || data.refresh){ // 獲取需要傳遞給h6頁面的參數 let refreshParam = data.refreshParam; ... // 如果連接中帶有需要處理onShow邏輯的參數(通過url的hash和h6交互,而不是刷頁面) 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 在找到指定h6鏈接時觸發 content: '' // 條件內容 - type=immediately 時為空 - type=url 時候為h6鏈接地址 } } */ 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); } ... } } ... }
上面東西看著挺多,總結下來就是幾點:
h6端
h6端在做修改時也要考慮幾點:
最好能把這些交互邏輯封裝起來
讓業務方比較簡單方便的調用
這里我新定義了2個方法
onShow(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 觸發條件
例子:類型選擇頁
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,我們來看看h6端的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和更新都可以正常觸發 // 但是:h6調用部分小程序能力會失敗(如: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',在找到指定h6鏈接時觸發,'path'在打開指定小程序路徑時觸發 type: 'immediately', // 條件內容,immediately是為空,url是為h6鏈接地址,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方法的實現
ok,整個方案就介紹完了
結語
最早的方案并不完全是這樣的,但原理是一樣的。在我實現的過程中發現原始方案有很多問題
于是我又做了大量的改造和細節優化,于是形成了上面的最終方案。
這個方案屬于侵入式改造方案,需要各業務方改造自己的代碼。雖然有一定改造成本,但用戶體驗的收益非常明顯。
ps:我們的QA在測試時都說“這用起來就爽多了”
注意:
采用這個方案需要注意幾點:
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。