UIWebViewDelegate没有监控XMLHttpRequest?

54

UIWebViewDelegate是否无法监控使用XMLHttpRequest发出的请求?如果是这样,有没有一种方法可以监控这些类型的请求呢?

例如,在-(BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSMutableURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType中,UIWebViewDelegate无法捕获此类请求。

var xhr = new XMLHttpRequest();
xhr.open("GET", "http://www.google.com", true);

xhr.onreadystatechange=function() 
{
    if (xhr.readyState==4) 
    {
        alert(xhr.responseText);
    }
}

xhr.send();
5个回答

80

这是一个有趣的问题。

要使其工作,需要两个部分:JavaScript处理程序和UIWebView委托方法。在JavaScript中,我们可以修改原型方法以在创建AJAX请求时触发事件。通过我们的UIWebView代理,我们可以捕获这些事件。

JavaScript处理程序

我们需要在AJAX请求发出时得到通知。我在这里找到了解决方案。

在我们的例子中,为了让代码能够工作,我将下面的JavaScript放在一个名为ajax_handler.js的资源文件中,并与我的应用程序一起打包。

var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.callback = function () {
    window.location='mpAjaxHandler://' + this.url;
};

XMLHttpRequest.prototype.open = function(a,b) {
  if (!a) var a='';
  if (!b) var b='';
  s_ajaxListener.tempOpen.apply(this, arguments);
  s_ajaxListener.method = a;  
  s_ajaxListener.url = b;
  if (a.toLowerCase() == 'get') {
    s_ajaxListener.data = b.split('?');
    s_ajaxListener.data = s_ajaxListener.data[1];
  }
}

XMLHttpRequest.prototype.send = function(a,b) {
  if (!a) var a='';
  if (!b) var b='';
  s_ajaxListener.tempSend.apply(this, arguments);
  if(s_ajaxListener.method.toLowerCase() == 'post')s_ajaxListener.data = a;
  s_ajaxListener.callback();
}

实际上,这将会将浏览器的位置更改为某个虚构的URL方案(在此情况下为 mpAjaxHandle )并提供有关所做请求的信息。不用担心,我们的委托会捕获它,位置不会更改。

UIWebView委托

首先,我们需要读取JavaScript文件。我建议将其存储在静态变量中。我习惯使用+initialize。

static NSString *JSHandler;

+ (void)initialize {
    JSHandler = [[NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ajax_handler" withExtension:@"js"] encoding:NSUTF8StringEncoding error:nil] retain];
}

接下来,我们想在页面加载完成之前注入此JavaScript,以便我们可以接收所有事件。

- (void)webViewDidStartLoad:(UIWebView *)webView {
    [webView stringByEvaluatingJavaScriptFromString:JSHandler];
}

最后,我们要捕获事件。

由于URL Scheme是虚构的,所以我们不想实际跟随它。我们返回NO,一切都很好。

#define CocoaJSHandler          @"mpAjaxHandler"

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[[request URL] scheme] isEqual:CocoaJSHandler]) {
        NSString *requestedURLString = [[[request URL] absoluteString] substringFromIndex:[CocoaJSHandler length] + 3];

        NSLog(@"ajax request: %@", requestedURLString);
        return NO;
    }

    return YES;
}

我创建了一个带有解决方案的示例项目,但没有地方进行托管。如果你能够提供托管,请与我联系,我将相应地编辑这篇文章。


2
请注意,XCode不会自动将扩展名为.js的文件添加到捆绑包中。您需要手动添加该文件。 - Reinhold
1
嗨 Mike - 你能分享这个示例项目给我吗?我会非常感激的。 - Nathan
1
这个问题与以下相关:https://dev59.com/32jWa4cB1Zd3GeqPuMor - Daniel
2
注意:我刚把JS示例直接放入NSString中进行测试,一开始它并没有起作用。我不得不在XMLHttpRequest.prototype.send/open赋值的括号后面添加一个分号。 - Oliver Pearmain
2
请注意,我在整个代码周围添加了一个if(!s_ajaxListener),即使JS被运行/评估两次,它仍然可以正常工作。 - Oliver Pearmain
显示剩余9条评论

20

您可以使用NSURLProtocol。例如,如果您使用http://localhost/path调用XMLHttpRequest,则可以使用以下内容处理:

@interface YourProtocol: NSURLProtocol

然后是实现:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request 
{
    return [request.URL.host isEqualToString:@"localhost"];
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

- (void) startLoading
{
    // Here you handle self.request 
}

- (void)stopLoading
{
}
你需要按照以下步骤注册协议:
    [NSURLProtocol registerClass:[YourProtocol class]];

这里的问题是,除非您保持一个列表,否则您将失去对UIWebView的所有引用,即使如此,也不是100%可靠的。 - Thizzer
实际上,与其他方法相比(非常hackish),这是一种更健康的解决方案。如果需要,您可以在某个地方引用您的UIWebView实例的类引用... 如果您想要更多的功能(而不仅仅是监视),则此方法也更加强大。 - Radu Simionescu
值得一提的是,在startLoading中,您需要调用[self.client URLProtocolDidFinishLoading:self]来完成请求。(否则它将在javascript端超时。) - sdsykes
更准确地说,您需要调用类似于 [self.client URLProtocol:self didReceiveResponse:[NSURLResponse new] cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 然后在 startLoading 中调用 [self.client URLProtocolDidFinishLoading:self]; 才能完成请求。 - sdsykes

3
通过实现并注册NSURLProtocol的子类,您可以捕获UIWebView发出的所有请求。 在某些情况下可能过于复杂,但如果您无法修改实际运行的javascript,则这是最佳选择。
在我的情况下,我需要捕获所有请求并向每个请求插入特定的HTTP标头。 我通过实现NSURLProtocol、使用registerClass进行注册并在我的子类中对+ (BOOL)canInitWithRequest:(NSURLRequest *)request回答YES(如果请求对应于我感兴趣的URL),来完成此操作。 然后,您必须实现协议的其他方法,可以通过使用NSURLConnection来完成,将协议类设置为委托并将NSURLConnection的委托方法重定向到NSURLProtocolClient。

5
如果您只是想监测请求(而不修改它),那么可以按照以下步骤操作: 1)创建一个NSURLProtocol的子类 2)实现方法 + (BOOL)canInitWithRequest:(NSURLRequest *)request { return NO;} 3)使用[NSURLProtocol registerClass:lShareSwymURLProtocol]注册您的协议。在第2步的实现中,您可以添加任何您想要的代码来检查请求。只需确保如果您希望标准加载机制继续,请返回NO。 - terrinecold

1

看起来确实如此。除非你能找出一种方法使用 stringByEvaluatingJavaScriptFromString: 来注入一些 Javascript 来完成您所需的操作,否则没有办法监视 UIWebView 的操作。


0

正如其他人所提到的,UIWebViewDelegate 仅观察 window.location 和 iframe.src 的更改。

如果您只想使用 iOS 的自定义 URL 方案, 可以利用 <iframe> 来实现。

例如,如果您想调用像这样的 URL Scheme

objc://my_action?arg1=1&arg2=2

在你的 JS 文件中:

/**
* @param msg : path of your query
* @param data : arguments list of your query
*/
var call_url = function(msg, data){
    const scheme = "objc";
    var url = scheme + '://' + msg;
    if(data){
        var pstr = [];
        for(var k in data)
            if(typeof data[k] != 'function')
                pstr.push(encodeURIComponent(k)+"="+encodeURIComponent(data[k]));
        url += '?'+pstr.join('&');
    }
    var i = document.createElement("iframe");
    i.src = url;
    i.style.opacity=0;
    document.body.appendChild(i);
    setTimeout(function(){i.parentNode.removeChild(i)},200);
}

//when you call this custom url scheme
call_url ("my_action", {arg1:1,arg2:2});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接