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;
这个看着也挺多,总结下来是两点:
onShow方法的实现
绑定一个hashchange事件(这里做了防止重复绑定事件的处理)
将传入的onShow自定义事件缓存在一个数组中,hashchange触发时,根据特有的标志位__isonshow和__wachangehash确定是否触发
serviceDone方法的实现
处理传过来的数据
处理该数据的触发条件:immediately表示最近的一次onShow触发,或者自己指定url
通过wx.miniProgram.postMessage发送数据
ok,整个方案就介绍完了
结语
最早的方案并不完全是这样的,但原理是一样的。在我实现的过程中发现原始方案有很多问题
于是我又做了大量的改造和细节优化,于是形成了上面的最终方案。
这个方案属于侵入式改造方案,需要各业务方改造自己的代码。虽然有一定改造成本,但用户体验的收益非常明显。
ps:我们的QA在测试时都说“这用起来就爽多了”
注意:
采用这个方案需要注意几点:
如果采用这种方式通信,需要在当前页面url的query部分加入__isonshowpro=1,否则是不会通过hash通信的
同时要保证页面确实调用了onShow方法,否则页面也是不会刷新的
如果第三方业务需要传值,需要统一采用serviceDone方法通信