Как смоделировать AJAX-вызов с NSURLProtocol?
У меня есть UIWebview, который делает вызовы AJAX для внешних служб. В автономном режиме мне нужно отлавливать эти запросы и возвращать локальный JSON.
Я реализовал NSURLProtocol и мне удается перехватить AJAX-запрос, проблема в том, что jquery всегда возвращает код ошибки 0:
$.ajax({
url: url,
dataType: 'json',
contentType: "application/json",
success: function(jsonData){
alert("success :");
},
error: function (request, status, error) {
alert("failure :" + request.status );
}
});
Я всегда получаю request.status = 0
Чтобы проверить мой протокол, я попытался смоделировать изображение в моем HTML, и это прекрасно работает.
- HTML-запрос к изображению от google.fr => отлично работает
- AJAX вызов JSON на Amazon => не удается
Вот моя полная реализация:
#import "EpubProtocol.h"
@implementation EpubProtocol
#pragma mark - NSURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
BOOL awsRequest = [self request:request contains:@"s3.amazonaws.com"];
BOOL imgRequest = [self request:request contains:@"google.fr"];
BOOL match = awsRequest || imgRequest;
return match;
}
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
return theRequest;
}
- (void)startLoading {
NSURLRequest *request = [self request];
//Mock Amazon call
if([EpubProtocol request:request contains:@"s3.amazonaws.com"]) {
NSString *path = [[NSBundle bundleForClass:self.class] pathForResource:@"epub1" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
[self mockRequest:request mimeType:@"application/json" data:data];
}
//Mock image call
else if([EpubProtocol request:request contains:@"google.fr"]) {
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.itespresso.fr/wp-content/gallery/yahoo/1-yahoo-logo.jpg"]] queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
[self mockRequest:request mimeType:@"image/jpeg" data:data];
}];
}
}
- (void)stopLoading
{
NSLog(@"Did stop loading");
}
#pragma mark - Request utils
+ (BOOL) request:(NSURLRequest*)request contains:(NSString*)domain {
NSString *str = [[request URL] absoluteString];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", domain];
return [pred evaluateWithObject:str];
}
#pragma mark - Mock responses
-(void) mockRequest:(NSURLRequest*)request mimeType:(NSString*)mimeType data:(NSData*)data {
id client = [self client];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[request URL] MIMEType:mimeType expectedContentLength:-1 textEncodingName:nil];
[client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
}
@end
2 ответа
Проблема возникает из-за webkit, который блокирует ответ из-за междоменного запроса источника. Так как мы высмеиваем ответ, мы должны форсировать Access-Control-Allow-Origin.
Затем нам также нужно принудительно указать тип содержимого ответа.
Вот где происходит волшебство:
NSDictionary *headers = @{@"Access-Control-Allow-Origin" : @"*", @"Access-Control-Allow-Headers" : @"Content-Type"};
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"1.1" headerFields:headers];
Окончательная реализация протокола:
#import "EpubProtocol.h"
@implementation EpubProtocol
#pragma mark - NSURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
BOOL isAwsRequest = [self request:request contains:@"s3.amazonaws.com"];
return isAwsRequest;
}
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
{
return theRequest;
}
- (void)startLoading {
NSURLRequest *request = [self request];
//Mock Amazon call
if([EpubProtocol request:request contains:@"s3.amazonaws.com"]) {
NSString *path = [[NSBundle bundleForClass:self.class] pathForResource:@"epub1" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
[self mockRequest:request data:data];
}
}
- (void)stopLoading
{
NSLog(@"Did stop loading");
}
#pragma mark - Request utils
+ (BOOL) request:(NSURLRequest*)request contains:(NSString*)domain {
NSString *str = [[request URL] absoluteString];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF contains[cd] %@", domain];
return [pred evaluateWithObject:str];
}
#pragma mark - Mock responses
-(void) mockRequest:(NSURLRequest*)request data:(NSData*)data {
id client = [self client];
NSDictionary *headers = @{@"Access-Control-Allow-Origin" : @"*", @"Access-Control-Allow-Headers" : @"Content-Type"};
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"1.1" headerFields:headers];
[client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
}
@end
Ничего особенного в JS:
function loadJSONDoc()
{
var url = "https://s3.amazonaws.com/youboox_recette/epub.json";
$.ajax({
url: url,
dataType: 'json',
contentType: "application/json",
success: function(jsonData){
alert('success');
document.getElementById("myDiv").innerHTML='<p>'+$.param(jsonData)+'</p>';
},
error: function (request, status, error) {
alert("failure :" + request.status );
}
});
}
Я должен был сделать аналогичные вещи некоторое время назад.
Сначала мне удалось найти код, который может заставить UIWebViewDelegate перехватывать вызов ajax, поэтому в моей части веб-приложения у меня было:
//code to extend XMLHttpRequest, and have ajax call available in uiwebviewdelegate.
var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.callback = function () {
window.location=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();
}
Затем в iOS я возвращаю NO в UIWebViewDelegate shouldStartLoad (мое состояние было немного некрасивым):
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if ([[[request URL] scheme] rangeOfString:@"https"].length > 0)
{
return NO;
}
return YES;
}
Кроме того, я бы зарегистрировал свой протокол:
[NSURLProtocol registerClass:[MyProtocol class]];
С реализацией StartLoad. Вы также должны иметь подкласс canInitWithRequest
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([request.URL.scheme rangeOfString:@"https"].length > 0)
{
return YES;
}
return NO;
}
Сказать протоколу, что его следует использовать для запроса.
И не забудьте отменить регистрацию, когда у вас есть сеть.
Если вы не хотите беспокоиться о реализации NSURLProtocol, вы можете использовать мой собственный: https://github.com/bcharp/BOURLProtocol;)
Надеюсь, поможет!