0. 背景介绍:
移动端混合开发,APP中90%的内容均为内嵌H5,由于种种原因,我在客户端内无法使用单页面路由跳转,只能新开窗口跳转页面,于是被迫形成了“多页面”的情形。(即使是连贯的页面)
1. 需求场景
例如当处于一个列表中,此时点击某一项跳转至详情进行操作,并改变了这一项的状态,那么列表也需要同步改变该项的状态。(此时列表与详情是两个webview)
最终实现效果:(使用两个标签页模拟真机情况)
2. 问题思考
既然是多页面,Vue中的各种通信方式就用不了了,首先想到的是小程序的onShow()生命周期,我们的APP也提供了类似的协议方法,意味着H5页面中可以监听到页面重回,但是该方式依赖原生,且正常情况下也无法做到数据通信,页面重回时直接重新请求新数据会导致整个页面刷新,方案pass。
3. 最终解决方案
localStorage是我前端开发中最常用到的数据持久储存方案没有之一,在同源情况下,完全可以作为多页面之间的数据通信桥梁,我决定采用发布订阅模式,编写一个eventBus,来解耦多页面之间的通信,使用window原生的事件监听充当事件总栈,监听localStorage的变动并广播触发对应操作。下面就一起看看我是如何实现的吧。
4. 实现代码
创建 storageBus.js
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| let effects = [];
function depend(obj) { effects.push(obj); } function notify(key, params) { const fnList = effects.filter(x => x.name === key); fnList.forEach(list => list.fn(JSON.parse(params).data)); }
export default { $emit(name, data) { let item = localStorage.getItem(name) || "{}"; try { item = JSON.parse(item); item.data = data } catch (e) { item = {}; } item.timeStamp = new Date().getTime(); localStorage.setItem(name, JSON.stringify(item)); }, $on(name, fn) { depend({ name, fn }); } };
window.addEventListener( "storage", e => { notify(e.key, e.newValue); }, false );
|
在一个工具集文件 utils.js
中引入 storageBus
1 2 3 4 5 6
| import storageBus from "./storageBus"; export default { install(myVue) { myVue.prototype.$bus = storageBus; } }
|
在Vue的入口文件(通常为main)中import这个工具集utils.js
,配置 Vue.use(utils);
这样就可以在全局使用 this.$bus
调用我们上面编写的storageBus了;
5. 使用
List.vue 列表中注册事件:
1 2 3 4 5
| mounted() { this.$bus.$on('effect', (data) => { }) },
|
Detail.vue 列表跳转详情页某个条件下发送信息:
1
| this.$bus.$emit('effect', { id: xxx })
|
没错,使用方式和Vue自带的eventBus一致,事件广播,解耦的神。
6. 事件解绑
Vue的eventBus中提供了off
方法用于解绑事件,我们也需要实现一个解绑方法,否则页面在多次进入后会重复注册事件,但是重复注册事件也是我们考虑的情况,那么删除特定的注册事件则需要两个判断条件:1. 事件名 2. 事件函数是否一致,这样导致的结果是off
方法需要把on
注册的匿名函数传递进去,这太麻烦了,为了优雅地解决这个问题,我们可以让on
方法在调用过后,主动将off
方法吐出,以此来注销对应事件。
storageBus.js
新增代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| let effects = [];
function depend(obj) { ... } function notify(key, params) { ... }
export default { $emit(name, data) { ... }, $on(name, fn) { depend({ name, fn }); return () => { this.$off(name, fn); }; }, $off(name, fn) { const fnList = effects.filter(x => x.name === name); effects = fnList.filter(x => x.fn !== fn); if (effects.findIndex(x => x.name === name) === -1) { localStorage.removeItem(name) } } };
window.addEventListener( ... );
|
List.vue 列表中注册事件,并在页面销毁时解除:
1 2 3 4 5 6 7 8
| mounted() { this.storageBusOff = this.$bus.$on('effect', (data) => { }) }, beforeDestroy() { this.storageBusOff(); }
|
7. 真机兼容性
经过测试,Ios中storage
的监听并不生效,这非常令人遗憾,不过在安卓系统中则正常,但是我在偶然中发现,Ios和Android的webview在页面跳转上并不相同,Ios的webview可以正常进行网页前进后退,安卓则是返回直接关闭了页面(不知道是不是安卓没做处理),总之最后我采用判断环境来使用不同的页面跳转与消息通信:
工具函数:
1 2 3 4 5 6 7 8
|
export const isIOS = () => { var u = navigator.userAgent; var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); return isiOS; };
|
修改上面的工具集文件 utils.js
:
1
| myVue.prototype.$bus = utils.isIOS() ? new Vue() : storageBus;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import router from "../router/index";
myVue.prototype.$myRouter = (params, title = null) => { if (!utils.isIOS()) { const path = params.path ? params.path : params.name ? `/${params.name}` : ""; let query = "?"; for (const key in params.query) { if (Object.hasOwnProperty.call(params.query, key)) { query += `${key}=${params.query[key]}&`; } } query = query.substring(0, query.length - 1); xxxxx.appClient({ type: "openPage", data: { url: baseUrl + "saas/dangjian/" + `#${path}` + query } }); } else { router.push(params); } };
|
Vue文件中:
1 2 3 4 5
| beforeDestroy() { this.$bus.$off('effect') this.storageBusOff instanceof Function && this.storageBusOff(); }
|
至此终于解决了使用Vue单页面开发混合app的页面遇到的深坑,在判断函数isIOS
中判断的是整个苹果生态,苹果系统则走vue-router路由并且也不是跨页面通信方式,而我的开发电脑是mac,所以整个开发过程还是比较流畅的,网页调试开发的效果基本不用到原生再重新确认一遍,只管发布后提测。