概念
页面
对于名词「页面」,原生APP 和 Web 的定义不一样。假设 从「首页」跳转到「个人」,再跳转到「关于」,
在原生APP中,每个页面都是独立存在的,它们存在于一个 栈中,都在内存中有对应的页面对象,打开新页面,就是向栈中push一个页面对象,返回上一页,就是对栈执行pop。
在Web中打开新页面,新页面会替换旧页面(对于单页应用来说,就是新页面组件替换旧页面组件),虽然也有栈,但栈中只存储页面的路径,或称路由,并不存储 页面本身。
WebView 控件
在APP中,有一个UI控件叫WebView,它和普通的按钮、输入框、单选钮等 都是标准的UI控件,只不过它有一个特殊之处,能显示Web内容,以Web的角度看,它很像iframe标签。
当把WebView控件的长宽设置成和整个页面大小相同时,那整个页面就变成了浏览器的模样。
窗口&导航
基于原生APP 和 Web 对「页面」的不同定义,页面导航也会相应地有两种思路:一种是多浏览器导航,一种是单浏览器导航。
多浏览器模式 符合原生APP开发思路,即 每个都是独立的页面对象,都位于同一个页面栈,Web页面只是内嵌在原生页面的WebView控件中,属于标准的原生导航。
单浏览器模式 符合标准Web开发思路,自然是使用标准的浏览器导航。
这里「浏览器」的叫法不太准确,WebView虽很像浏览器,但和完整的浏览器相比少了很多功能,而且浏览器容易让人想到Safari和Chrome,而“承载Web页面的原生页面” 太啰嗦,这里引入「窗口」的概念,来指代“承载Web页面的原生页面”。文中的”多浏览器模式“ 有时也表达为 ”多窗口模式“。
多浏览器模式 在标准Web开发的角度看,不太正常,但它有它适用的地方,对于穿插型页面,比如前一个是Web页面,后一个是原生页面,再后一个又是Web页面,这时肯定是多浏览器模式,如果是连续的Web页面,但却是 不同域名 的Web页面,也适合多浏览器模式。
iOS系统中,WebView跳转不同域名的Web页面,会报错,偶尔会闪退。请务必使用另一浏览器打开不同域名的页面。
返回前一页
原生APP有3种返回方式:左上角按钮,轻扫屏幕边缘,安卓手机底部的物理返回键。
浏览器窗口对返回事件的处理是:首先检查有没有「H5自定义返回」,如果有,就执行它;如果没有,就检查WebView的 history能否返回 ,如果能,就执行history.back();如果不能,就执行原生页面的返回,即 关闭窗口。
backPreviousPage() 有一个可选参数 checkHistory,如果传true,就是先检查history能否返回,如果能,就执行history.back()。 如果不传这个参数,就是原生页面的返回上一页,即关闭窗口。
history.replace() 不会产生history导航记录,在处理返回时,如果不能history.back(),就关闭窗口。
H5自定义返回
window.setGoBack可以定制返回逻辑。
Bridge.window.setGoBack('myGoBack', () => { /* 自定义逻辑 */ });
自定义返回可以实现 挽留提示,页面返回时的埋点,还可以禁用返回(myGoBack是空函数)。
浏览器窗口默认有原生导航栏,导航栏自带标题和返回按钮,然而实际开发中,经常要自定义导航栏和返回按钮,自定义按钮的点击函数自然是自己定义,然而 轻扫屏幕边缘 和 底部物理返回 只能关联到window.setGoBack指定的函数,这样就形成了一种常见写法:
// 页面返回函数
const onPageBack = () => {
setShowConfirmBack(true); // 显示「确认返回」弹框
};
// 拦截返回 (窗口层面)
Bridge.window.setGoBack('myGoBack', onPageBack);
// UI组件 (组件层面)
<NavBar title="自定义标题" onBack={onPageBack} />
如果自定义导航栏和返回按钮 仅仅为了个性化样式,那大概率不需要使用window.setGoBack,因为默认的返回处理就是最符合大多数场景的处理,只有特殊情况才会用到window.setGoBack。
window.setGoBack是作用在窗口上的,和页面组件无关,对于单窗口模式(SPA),如果切换页面时,想要对返回事件做不同处理,建议在页面组件的清理函数中恢复默认返回行为,即调用resetGoBack('myGoBack'),再在新页面组件中重新设置自定义返回,即再次调用setGoBack。
useEffect(() => {
// 第1个参数:函数名称/标签,在单窗口模式(SPA),要唯一,比如用组件名称做前缀。
Bridge.window.setGoBack('_contentPageBack', () => {
// TODO 自定义返回
});
return () => {
// 恢复默认行为
Bridge.window.resetGoBack('_contentPageBack'); // 传入函数名称/标签
};
}
窗口焦点
- 当窗口可见或重新可见时(覆盖窗口消失,或回前台),会调用H5注册的window.onfocus函数。
- 当窗口将要不可见时(被新窗口覆盖,或退后台,或将被关闭),会调用H5注册的window.onblur函数。
如果以埋点来举例,大致如下:
Bridge.window.setOnFocus('_contentPage_focus', () => {
Bridge.trackEvent({ name: 'x_page_enter' }) // 埋点 进入页面
})
Bridge.window.setOnBlur('_contentPage_blur', () => {
Bridge.trackEvent({ name: 'x_page_leave' }) // 埋点 离开页面
})
在使用React时,如果要在componentDidMount
或useEffect
中注册,可以参考下面的方式:
// 窗口层面
useEffect(() => {
// 注册
Bridge.window.setOnFocus('_contentPage_focus', (flag) => {
if (flag !== 'loadFinish') { // !!如果初次加载,不埋点,从第二次开始埋点
Bridge.trackEvent({ name: 'x_page_enter' })
}
});
Bridge.window.setOnBlur('_contentPage_blur', () => {
Bridge.trackEvent({ name: 'x_page_leave' })
})
// 清理、重置
return () => {
Bridge.window.resetOnFocus('_contentPage_focus') // 传入函数名称/标签
Bridge.window.resetOnBlur('_contentPage_blur')
}
}, [])
// 组件层面
useEffect(() => {
Bridge.trackEvent({ name: 'x_page_enter' })
return () => { Bridge.trackEvent({ name: 'x_page_leave' }) }
})
以上写法 在窗口层面忽略了初次加载(flag !== 'loadFinish'
)完成时的焦点事件,改为在组件层面弥补(埋点)。之所以忽略首次,是因为componentDidMount
或useEffect
是在组件加载/绘制完成之后 才调用的,位于此函数中的注册动作 就“慢了一拍儿”,这一慢,就可能导致:注册焦点事件处理函数时,焦点事件已经错过了,换句话说,触发窗口焦点事件时,相关处理函数还没注册。而使用上面的写法,忽略(可能失败的)首次注册,用显式的调用来弥补首次, 这样,无论有没有错过首次的焦点事件,都不会错。
窗口焦点只和浏览器窗口相关,和具体Web页面无关,如果不同Web页面需要做不同处理,建议在页面组件的清理函数中重置,即调用resetOnFocus / resetOnBlur, 再在新页面组件中重新设置,即再次调用setOnFocus / setOnBlur。
事件通知
事件通知的主要应用场景是 浏览器窗口与原生页面通信,或浏览器窗口与浏览器窗口通信。
事件通知 是基于原生APP的通知机制 封装构造的,下图是实现原理:
前文提到,浏览器窗口 其实就是「承载Web页面的原生页面」,所以它和普通原生页面采用的事件监听及事件发送方法相同(图中的 小耳朵)。 浏览器窗口有一个bridge(桥接器),可以和 javascript 相互调用,有了它,javascript调用事件监听和事件发送方法,就能通过bridge转换成对原生APP的事件方法调用,反过来,浏览器窗口收到原生APP事件,通过bridge转换成对 javascript 函数的调用,这样就实现了页面间通信。
从实现原理中 能看出:事件的监听及发送,是基于浏览器窗口的,和具体的Web页面无关,也就是说 在单窗口模式中,如果不同Web页面对事件有各自特殊的处理时,就有可能发生覆盖或回调函数失效的情况。换个角度解释:基于浏览器窗口的实现就像是全局变量,任何Web页面都可以读取,也都可以修改,但修改后有可能影响(运行在同一浏览器中的)其他Web页面。 上文的自定义返回和窗口焦点 都有类似问题,需要小心谨慎,安全起见,建议在组件销毁时(componentWilUnmount
或useEffect
返回函数)移除事件(使用 emitter.off 方法)。
// 示例:
useEffect(() => {
Bridge.emitter.on({
name: 'RenewalPaySuccess', // 监听「续租支付成功」事件
listener: '_cardList_RenewPaySuccess', // 名称/标签,单窗口模式(SPA)要唯一,比如用组件名称做前缀。
func: () => { ... }, // 自定义处理
});
return () => {
Bridge.emitter.off({
name: 'RenewalPaySuccess', // 移除「续租支付成功」监听
listener: '_cardList_RenewPaySuccess' // 单窗口模式(SPA),要传入名称/标签
});
};
}, []);
数据存储
sessionStorage: 存储在内存,只到应用被杀死
localStorage: 存储在磁盘, 只到应用被卸载
二者的特点:
- 和域名无关,在线Web页面 和 本地Web页面 可共用,即 浏览器窗口之间 互通。
- Web页面和原生APP页面 可共用,所有页面(包括非页面)之间互通。
由于具有全局互通性,局部使用的数据,建议为key名称添加 前缀 ,以防被意外修改。
数据的互通,可用于页面间的数据传递。所以,至少有3种页面通信方式:1. url查询参数,2. 事件通知,3.数据存储。 一个不错的实践是:对于少量的、关键的数据,比如id,适合用url参数;对于大量的、格式复杂的数据,比如对象或数组,适合用数据存储;对于具有实时性的,需要立刻响应的,适合用事件通知。