咱們先來看下小程序中常見的h5跳h5的方式:ios
咱們採用的是方式3,理由以下:web
因爲這種方案可能會達到小程序的10層限制。因此在一些重要頁面建議加入「回到首頁」的操做,經過這個操做來縮短小程序歷史棧小程序
(若是不感興趣這部分能夠直接略過)微信小程序
wx.miniProgram.reLaunch({
url: '/pages/webview/bridge?url=項目首頁地址'
})
複製代碼
先聲明,咱們webview的路徑是/pages/webview/webview數組
/pages/webview/bridge是個中轉頁,有以下特色:瀏覽器
這個中轉頁:主要保證reLaunch到某h5頁面後,用戶仍然能夠點擊返回到小程序首頁。緩存
該方案一般用於:小程序中內嵌了多個業務線的h5頁面這種場景。bash
咱們從首頁進入發佈頁,完成發佈後,跳轉至商品詳情頁微信
那麼對於一個新用戶來說,整個操做過程是這樣的:併發
這個場景就是同一個頁面,裏面不一樣的內容項須要跳轉不一樣的頁面去操做,而後再回到原來頁面更新狀態的問題。
假如商品詳情頁沒有「回到首頁」的入口,那麼這個用戶要想回到首頁。。。須要按8次「返回」 = =!
通過這個體驗後,我想通常的用戶是沒有勇氣再發布內容的。
固然也有另外一種這種折中方案
就是商品提到的,在鏈接中加入某個標誌位,好比在url中加入__isonshowrefresh=1,webview在打開鏈接時候,會去讀取這個參數,若是有,則每次在onShow時候,從新加載url,經過刷新頁面進行頁面狀態更新。
這個體驗也不爽,就是在複雜的頁面會屢次刷新。
我下面要講的這個方案並非停留在設想階段,它已經在線上跑了
想看效果的朋友,能夠在微信小程序中搜:
「轉轉二手交易網」-「0元免費領」-(底部)「送閒置賺星星」-進入到發佈頁後
分類(跳轉h5,選中內容後返回,將參數傳給以前的h5)
取件地址(跳轉native原生地址選擇,選中後返回,將參數傳給以前的h5)
OK,咱們進入今天的主題
首先想到的就是onShow方法的實現,以前有人提議用visibilitychange來實現onShow方法。
但調研事後,這種方式在ios中表現符合預期,可是在安卓手機裏,是不能按預期觸發的。因此該方案被我否了。
因而就有了下面的方案
這個方案須要h5和小程序的webview都作處理。
核心思想:利用webview的hash特性
爲何要執行window.history.go(-1)
這一步是整個方案的精髓:
小程序裏另個一常見的場景就是調用第三業務(或者己方業務),在作完某些操做後須要把選中的數據帶回以前的頁面。
如前面提到的例子:發佈頁,須要選擇發佈類型,而後返回,發佈頁發佈類型局部更新
固然有些同窗會說:我能夠用setInterval,監控localStorage。在新頁面選中內容後,設置localStorage,而後在返回不就能夠了。
我這裏說的是通用方案。若是頁面都是由己方業務線維護的固然能夠隨便折騰。
可是一旦涉及到第三方業務線,尤爲不一樣域名頁面的業務調用,這種通訊方式就尷尬了。
那個人方案怎麼處理呢,我總結了一張圖
咱們來解讀一下這張圖:
整個過程就是這樣
小程序webview要先作幾方面考慮:
<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);
}
...
}
}
...
}
複製代碼
上面東西看着挺多,總結下來就是幾點:
h5端在作修改時也要考慮幾點:
最好能把這些交互邏輯封裝起來
讓業務方比較簡單方便的調用
這裏我新定義了2個方法
例子:發佈頁面,須要選擇分類,返回時須要更新分類信息
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 {
...
}
}
...
複製代碼
例子:類型選擇頁
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;
複製代碼
這個看着也挺多,總結下來是兩點:
ok,整個方案就介紹完了
最先的方案並不徹底是這樣的,但原理是同樣的。在我實現的過程當中發現原始方案有不少問題
因而我又作了大量的改造和細節優化,因而造成了上面的最終方案。
這個方案屬於侵入式改造方案,須要各業務方改造本身的代碼。雖然有必定改形成本,但用戶體驗的收益很是明顯。
ps:咱們的QA在測試時都說「這用起來就爽多了」
採用這個方案須要注意幾點:
好了,今天就介紹這麼多,你們一塊兒學習