编程技术记录

世界你好!

问题

对于Native WebView而言,Andoird/iOS都提供相应的方法/代理监听页面URL的变化。例如,Android可以重写doUpdateVisitedHistory方法监听URL变化,iOS可以实现shouldStartLoadWithRequestdecidePolicyForNavigationAction代理监听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

定时器模式的特点是实现简单,适用范围广;缺点也显而易见,监测结果不是十分准确,性能耗用高。而其性能耗用问题在移动端将会放大,故此我们不采用此模式。

观察者模式

上面提到的doUpdateVisitedHistoryshouldStartLoadWithRequestdecidePolicyForNavigationAction的实现可以算作观察者模式。

前面提到,单页应用页面切换在移动端webview的行为表现,并不会每次都通知Native,我们要做的就是通过某种方法来完善这个页面切换的事件通知。

对于观察者模式,iOS中有KVO(key-value-observing)机制,遍历UIWebView和WKWebView的属性发现:

  • WKWebView的URL属性能够响应KVO机制,当前端页面(包括单页应用的页面)URL有变化时,KVO机制可以监测到。
  • UIWebView的request属性并不能响应KVO机制,不能用它检测URL变化。也就是说,单纯使用UIWebView的能力接口不能完全监测URL变化

至此,在Native侧建立观察者的思路并不适用所有情况,我们需要再返回前端侧研究下单页应用的URL变化。

单页应用,通过hashwindow.history可以做到改变URL,使得不刷新页面的情况下重新渲染。我们只要监测hashwindow.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
前端监测 hashwindow.history,通知Native 前端监测 hashwindow.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变化
});

参考:https://stackoverflow.com/questions/4570093/how-to-get-notified-about-changes-of-the-history-via-history-pushstate

© Beli. All Rights Reserved.