小程序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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。