转自:http://sozhidao.com/articles/374
由于 WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。在 webkit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC 发送给 App Process。出于性能的原因,encode 的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了 —— 摘自 WKWebView 那些坑
针对以上问题我使用过以下两种方案,并且测试过都是可行的
(1)方案一
我们通过 URLProtocol 拦截的 WKWebView 的时候可以不拦截 http、https,而是拦截一个指定的 customScheme。并且将 html 中所有 GET 请求的 url 的 scheme 都替换成 customScheme,POST 请求还是保持 http、https 的 scheme。下面是一个实现流程,很多代码略过了,只是说明方案。
- 下载所有的 html、css、js、图片 至沙盒
 - NSURLProtocol registerScheme
 
[NSURLProtocol wk_registerScheme:@"customScheme"];
- 打开页面 url,假设我们打开的 url 为 
https://www.baidu.com,通过以下代码我们就打开了一个customScheme://www.baidu.com了,这个请求就可以被 NSURLProtocol 拦截 
NSString *url = @"https://www.baidu.com";
[url stringByReplacingOccurrencesOfString:@"https" withString:@"customScheme"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
WKWebView *webview = [[WKWebView alloc] init];
[webview loadRequest:request];
- 在 NSURLProtocol 中判断是否是 
customScheme并且是我们首页index.html的地址,是的话结果该请求,获取本地的 html 数据,手动将内部的 html、css、js、图片 的 url 替换成customScheme 
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    NSString *urlStr = request.URL.absoluteString;
    if([urlStr isEqualToString:@"customScheme://www.baidu.com"]) {
        return YES;
    }
    return NO;
}
- (void)startLoading {
    NSString *urlStr = request.URL.absoluteString;
    if([urlStr isEqualToString:@"customScheme://www.baidu.com"]) {
        return YES;
    }
    
    // 从本地获取缓存的 https://www.baidu.com 文件的内容
    NSString *indexHtml = ...;
    // 将 indexHtml 中 css、js、图片 的 url 的 https 替换成 customScheme
    NSString *customSchemeHtml = [indexHtml convertToCustomSchemeHtml];
    
    NSData *htmlData = [customSchemeHtml dataUsingEncoding:NSUTF8StringEncoding];
    NSURLResponse *response = ...;
    
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [self.client URLProtocol:self didLoadData:htmlData];
    [self.client URLProtocolDidFinishLoading:self];
}
- 那么 
https://www.baidu.com中的静态资源的 scheme 都替换后就可以通过 NSURLProtocol 拦截到了,拦截到后再从本地取缓存返回 WKWebView。同时因为没有拦截 http、https ,所有的 post 请求都不会被 NSURLProtocol 拦截,还是由 WKWebView 自己处理,那么就不会有 Body 丢失的问题 
(2)方案二
这种方案又有两种实现
- 一种是前端手动将 POST 请求改为 JS 调用 Native 的方式,这样的话就将工作量转交给前端开发的朋友的。同时对于移动端和 PC 端都支持的 H5 页面需要前端做适配,避免在 PC 端进行 POST 请求也是用 JS 调用 Native 的方式,请求不到数据。
 - 另一种就是客户端注入一段 HookAjax 的 JS 代码,拦截所有的 XMLHttpRequest 的 POST 请求转移给客户端处理。HookAjax 后也有两种方案处理该 POST 请求
- 将 POST 请求的 body 装在 header 中,NSURLProtocol 拦截到的 POST 请求可以从 header 中获取到实际的 body 数据。但是这样子有个问题,header 的大小是有限制的,会有局限性。但是实现会方便很多。
 - 将 POST 请求通过 JS 和 Native 交互的方式将请求转交给 Native 处理并且在 Native 处理完后将结果返回给 JS,我下面介绍的就是这种方式的实现
 
 
下面是我的 HookAjax 的实现,主要参考了 Ajax-hook 并且做了一定的修改以更好的处理我们的情况。
function hookAjax(proxy) {
    // 保存真正的XMLHttpRequest对象
    window._ahrealxhr = window._ahrealxhr || XMLHttpRequest;
    XMLHttpRequest = function() {
        var xhr = new window._ahrealxhr;
        // 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象
        Object.defineProperty(this, 'xhr', {
            value: xhr
        })
    };
    
    // 获取 XMLHttpRequest 对象的属性
    var prototype = window._ahrealxhr.prototype;
    for (var attr in prototype) {
        var type = "";
        try {
            type = typeof prototype[attr]
        } catch (e) {}
        if (type === "function") {
            XMLHttpRequest.prototype[attr] = hookfunc(attr);
        } else {
            // 给属性提供 getter、setter 方法
            Object.defineProperty(XMLHttpRequest.prototype, attr, {
                get: getFactory(attr),
                set: setFactory(attr),
                enumerable: true
            })
        }
    }
    
    function getFactory(attr) {
        return function() {
            // 判断对象是否包含特定的自身(非继承)属性
            var v = this.hasOwnProperty(attr + "_") ? this[attr + "_"] : this.xhr[attr];
            var attrGetterHook = (proxy[attr] || {})["getter"];
            return attrGetterHook && attrGetterHook(v, this) || v
        }
    }
    
    function setFactory(attr) {
        return function(v) {
            var xhr = this.xhr;
            var that = this;
            var hook = proxy[attr];
            if (typeof hook === "function") {  // 回调属性 onreadystatechange 等
                xhr[attr] = function() {
                    // ========================  修改 1 ==================
                    hook.call(that, xhr) || v.apply(xhr, arguments);
                }
            } else {
                //If the attribute isn't writeable, generate proxy attribute
                var attrSetterHook = (hook || {})["setter"];
                v = attrSetterHook && attrSetterHook(v, that) || v;
                
                // ========================  修改 2 ==================
                xhr[attr] = v;
                this[attr + "_"] = v;
            }
        }
    }
    
    function hookfunc(func) {
        return function() {
            var args = [].slice.call(arguments);
            
            // call() 方法调用一个函数, 其具有一个指定的this值和分别地提供的参数
            // 该方法的作用和 apply() 方法类似,只有一个区别,就是call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组
            if (proxy[func]) {
                // ========================== 修改 3 ===============
                var result = proxy[func].call(this, args, this.xhr);
                if(result) {
                    return result;
                }
            }
            
            return this.xhr[func].apply(this.xhr, args);
        }
    }
    
    return window._ahrealxhr;
}
针对的修改主要有上面注释的 3 个修改
修改 1: 改变 hook 的onreadystatechange、onload这类方法的参数,统一参数修改 2: 强制通过_ + 属性名的方式添加属性到我们修改后的XMLHttpRequest的对象中,因为有一些属性是只读属性,用 Ajax-hook 的方式我们没办法设置这些属性修改 3: 对于getAllResponseHeaders、getResponseHeader返回指定结果
接下来还要添加我们拦截 Ajax Post 请求到 Native 执行的 JS 代码
window.MyAjax = {
    hookedXHR: {},
    hookAjax: hookAjax,
    nativePost: nativePost,
    nativeCallback: nativeCallback
};
        
// 添加请求 Native 的代码
function nativePost(xhrId, params) {
    // TODO: 请求 Native
}
// Native 请求完成后调用该 JS 方法并且传入指定参数
function nativeCallback(xhrId, statusCode, responseText, responseHeaders, error) {
    var xhr = window.MyAjax.hookedXHR[xhrId];
    
    if(xhr.isAborted) { // 如果该请求已经手动取消了
        return;
    }
    
    if(error) {
        xhr.readyState = 1;
        if(xhr.onerror) {
            xhr.onerror();
        }
    } else {
        xhr.status = statusCode;
        xhr.responseText = responseText;
        xhr.readyState = 4;
        
        xhr.myResponseHeaders = responseHeaders;
        
        if(xhr.onreadystatechange) {
            xhr.onreadystatechange();
        }
        if(xhr.onload) {
            xhr.onload();
        }
    }
}
// hook ajax 方法
window.MyAjax.hookAjax({
    // 设置 RequestHeader 将参数保存,在 send 中将其一起发送给 Native
    setRequestHeader: function (arg, xhr) {
        if(!this.myHeaders) {
            this.myHeaders = {};
        }
        this.myHeaders[arg[0]] = arg[1];
    },
    getAllResponseHeaders: function (arg, xhr) {
        var headers = this.myResponseHeaders;
        if(headers) {
            if(typeof(headers) === 'object') {
                var result = '';
                for(var key in headers) {
                    result = result + key + ':' + headers[key] + '\r\n'
                }
                return result;
            }
            return headers;
        }
    },
    getResponseHeader: function (arg, xhr) {
        if(this.myResponseHeaders && this.myResponseHeaders(arg[0])) {
            return this.myResponseHeaders(arg[0]);
        }
    },
    // 保存 open 中的参数,在 send 中判断是否为 POST 方法
    open: function (arg, xhr) {
        this.myOpenArg = arg;
    },
    send: function (arg, xhr) {
        this.isAborted = false;
        if(this.myOpenArg[0] === 'POST') {
            var params = {};
            params.data = arg[0];
            params.method = 'POST';
            params.header = this.myHeaders;
            var url = this.myOpenArg[1];
            var location = window.location;
            if(!url.startsWith(location.protocol)) {
                url = location.origin + url;
            }
            params.url = url;
            // XMLHttpRequest 的标识符,用于 Native 返回时确认时哪个 XMLHttpRequest
            var xhrId = 'xhrId' + (new Date()).getTime();
            window.MyAjax.hookedXHR[xhrId] = this;
            
            window.MyAjax.nativePost(xhrId, params);
            // 通过 return true 可以阻止默认 Ajax 请求,不返回则会继续原来的请求
            return true;
        }
    },
    abort: function (arg, xhr) {
        if(this.myOpenArg[0] === 'POST') {
            if(xhr.onabort) {
                xhr.onabort()
            }
            return true;
        }
    }
    
});
上面的代码和 Native 进行交互的代码就是 nativePost 和 nativeCallback 两个方法,一个需要将 xhrId 和该 POST 请求相关的数据传给 Native,一个需要 Native 执行完请求后将 xhrId 和结果放回给 JS。这样子一个完整的流程就走通了。
这里贴一个例子Ajax-hook iOS Demo,用的是我自己的 JSBridge 作为 JS、Native 的桥接库实现的一个 Demo
将上述两端 JS 代码注入 WebView,就可以实现一个 Post 请求的拦截了。
题外话:通过原生 NSURLSession 请求时,如果用的是 NSURLSession.sharedSession ,那么该请求是会被 NSURLProtocol 拦截到的,打断点后发现 NSURLProtocol 拦截到的 POST 请求的 bady 也是 nil,因为 body 数据这时候已经被转成 stream 了,可以在 request.HTTPBodyStream 中解析它
 Google Chrome 
 Windows 10
 
 
___ 
Added 
 
The photo is broken, sorry((( 
My profile on dating app: http://everydating.net/profile/*/ 
Or write to me in telegram @*_best ( start chat with your photo!!!)
		
				
 Windows 7