问题
对于Native WebView而言,Andoird/iOS都提供相应的方法/代理监听页面URL的变化。例如,Android可以重写doUpdateVisitedHistory
方法监听URL变化,iOS可以实现shouldStartLoadWithRequest
、decidePolicyForNavigationAction
代理监听URL变化
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//此处检测url是否有变化
return super.shouldOverrideUrlLoading(view, url);
}
//UIWebView
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//此处检测url是否有变化
return YES;
}
//WKWebView
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
//此处检测url是否有变化
decisionHandler(WKNavigationActionPolicyAllow);
}
上述技术实现对一些前端应用能正常工作。然而,遇到单页应用(SPA)时,以上方法并不会被触发–这里涉及单页应用的实现原理,尤其是基于 hash
路由的单页应用,其前进/后退
的问题更为突出。
解决思路
对于前端应用而言,如果开发者使用固定的框架,那么在框架的路由层面拦截监听应较为容易做到。但是实际情况是开发者使用的前端框架不确定,甚至开发者连前端框架都不用,所以我们在前端框架层面截获路由器变得不那么容易。
经过调研,要监听前端页面的URL变化,大致有两种模式:
- 定时器模式
- 观察者模式
定时器模式
定期监测URL是否变化,如100毫秒监测一次,与上次监测值对比来判断URL是否变化。定时器,前端可以使用setInterval
来实现
var oldUrl = window.location.href;
setInterval(function(){
if (oldUrl !== window.location.href) {
//通知Native Url变化
oldUrl = window.location.href;
}
},100);
当然,Native也可以开启定时器监测window.location.href
。
定时器模式的特点是实现简单,适用范围广;缺点也显而易见,监测结果不是十分准确,性能耗用高。而其性能耗用问题在移动端将会放大,故此我们不采用此模式。
观察者模式
上面提到的doUpdateVisitedHistory
、shouldStartLoadWithRequest
、decidePolicyForNavigationAction
的实现可以算作观察者模式。
前面提到,单页应用页面切换在移动端webview的行为表现,并不会每次都通知Native,我们要做的就是通过某种方法来完善这个页面切换的事件通知。
对于观察者模式,iOS中有KVO(key-value-observing)机制,遍历UIWebView和WKWebView的属性发现:
- WKWebView的
URL
属性能够响应KVO机制,当前端页面(包括单页应用的页面)URL有变化时,KVO机制可以监测到。 - UIWebView的
request
属性并不能响应KVO机制,不能用它检测URL变化。也就是说,单纯使用UIWebView的能力接口不能完全监测URL变化
至此,在Native侧建立观察者的思路并不适用所有情况,我们需要再返回前端侧研究下单页应用的URL变化。
单页应用,通过hash
或window.history
可以做到改变URL,使得不刷新页面的情况下重新渲染。我们只要监测hash
和window.history
,当它们发生变化时去检测URL的变化:
-
通过
hash
改变URL,会触发hashchange
事件。当监听到hashchange
事件时,去检测URL的变化。 -
window.history
相关的事件为popstate
。当监听到popstate
事件时,去检测URL的变化。History.back()
、History.forward()
、History.go()
会触发popstate
事件 -
另外,
History.pushState()
和History.replaceState()
不会触发popstate
事件,所以我们要hook这两个方法,以检测URL的变化。
方案实现
结合以上,最终我们可以采用的可行方案如下 :
Android WebView | iOS UIWebView | iOS WKWebView |
---|---|---|
前端监测 hash 和window.history ,通知Native |
前端监测 hash 和window.history ,通知Native |
Native监测WKWebView.URL |
注意:上述方案并不是唯一方案,但确实是一个可行方案
WKWebView监测URL部分实现
//添加观察者
[self addObserver:wkwebview forKeyPath:@"URL" options:NSKeyValueObservingOptionNew context:nil];
//接收观察事件
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
//url变化,触发相应动作
}
//使用完毕后移除观察者
[self removeObserver:wkwebview forKeyPath:@"URL"];
前端监测URL部分实现
//监听hash变化
window.addEventListener('hashchange',function(event){
//通过bridge通知Native URL变化
});
//监听history变化,popstate只能监听History.back(),History.forward()、History.go()
window.addEventListener('popstate',function(event){
//通过bridge通知Native URL变化
});
//hook History.pushState() History.replaceState()
var _wr = function(type) {
var orig = history[type];
return function() {
var rv = orig.apply(this, arguments);
var e = new Event('ffpd'+type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
history.pushState = _wr('pushState');
history.replaceState = _wr('replaceState');
window.addEventListener('ffpdpushState',function(event){
//通过bridge通知Native URL变化
});
window.addEventListener('ffpdreplaceState',function(event){
//通过bridge通知Native URL变化
});