咱们先来看下小程序中常见的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在测试时都说“这用起来就爽多了”
采用这个方案须要注意几点:
好了,今天就介绍这么多,你们一块儿学习