一、场景
App混合开发中, IOS 将服务器前端dist包下载到手机应用沙盒目录中,然后通过file:// 协议加载资源,然后前端调用后台api 接口照常走http 接口。
二、问题
当将资源都统一改为file协议加载进来后,去除了混合使用http协议加载资源的load not allowed 问题后,进入登录界面,发现验证码没有出来,刚开始觉得有点不知所然,感觉应该一切都会顺利地走下去,因为安卓平台这边已经走通了,但是问题就是出现了,于是和前端一起排查。
由于IOS 这边的native 端看不到前端的日志信息,所以只能用前端进行alert弹框来显示其信息这种笨拙的方式来调试,于是很费劲,前端那边对于native这边有些概念不清楚,因为在交流过程中需要疏导其理解。
在其调试过程中,我也在想便于调试的方案,后来找到了在ios xcode中能查看console.log日志的方法,其实很简单,就是通过js 和 native 之间的交互,将其转换为nslog 日志输出,这个在混合开发至初期,自己应该就已经尝试过这种交互,只是没有想到作为日志输出来使用:
//rewrite the method of console.log
NSString *jsCode = @"console.log = (function(oriLogFunc){\
return function(str)\
{\
window.webkit.messageHandlers.log.postMessage(str);\
oriLogFunc.call(console,str);\
}\
})(console.log);";
//injected the method when H5 starts to create the DOM tree
[config.userContentController addUserScript:[[WKUserScript alloc] initWithSource:jsCode injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]];
[config.userContentController addScriptMessageHandler:self name:@"log"];
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
// 输出前端的consolo.log
NSLog(@"%@",message.body);
}
看其js中的window.webkit.messageHandlers.log.postMessage 这个方法是不是很熟悉? 基本的js和native交互方式。
这样设置完之后,在xcode中就能看到前端页面中的console.log 日志信息了,方便调试。
最终发现原来前端在IOS 这边没有正确地调用native 提供的接口来获取 http 接口的头部 ip+port 信息,导致接口调用就直接走到错误回调中,所以拿不到后台的任何数据,验证码显示不出来。
刚开始以为这就是问题的原因,然鹅too young , too simple.
三、入坑
在前端改完上述问题之后,在日志信息中已经是正确的http 访问url 接口调用了,但是验证码依旧不出来,当看到这个现象的时候,我又丈二的和尚,摸不着头发了,因为安卓平台又是正常的,基于前面出现了前端调用接口不对造成的url 不对的问题,我的主观意识里面仍然觉得是不是前端某些地方还存在差异逻辑,导致IOS这边无法正常调用接口,但是前端确认没有其他差异了,而且之前直接使用http从服务器加载资源的方式也是同样的代码,一切都是正常的。现在只是将资源放到本地通过file加载资源,接口照常还是之前的http调用就出现了问题,不合理。
几番定位排查之后,仍然不得要领,过程中也开始怀疑是不是跨域的问题,但是日志信息并不明显,自己对此概念的理解也不太明了。
在此过程中还发现了一个现象就是:前端直接使用常规的http请求直接访问后台能正常拿到结果,但是其业务逻辑中需要对请求进行二次保证处理,在头部增加信息或者其他参数,拦截统一对返回结果进行处理,经过这种包装的请求就无法正常访问。
最终,我想还是通过fiddler抓包拦截再看看,于是看到下面的结果:
直接写死的http请求正常访问的:
通过包装后不能正常访问的:
前端发出的都是GET 请求,但是为什么经过包装之后,请求变成OPTIONS 了呢?
刚开始我以为请求就只有 POST GET PUT DELETE 这几种,现在又冒出了这个,所以开始在网上查找相关信息,最后被引导到了跨域问题。
尤其是这个:nginx 跨域踩坑及解决--OPTIONS请求处理 | LongSheng
OPTIONS请求方法的主要用途有两个:
1、获取服务器支持的HTTP请求方法;
2、用来检查服务器的性能。例如:AJAX进行跨域请求时的预检,需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全。
再来看下这个“某些情况下”都是什么情况?
1、跨域请求,非跨域请求不会出现options请求
2、自定义请求头
3、请求头中的content-type是application/x-www-form-urlencoded,multipart/form-data,text/plain之外的格式当满足条件12或者13的时候,简单的ajax请求就会出现options请求,有没有感觉到一点同源策略的意思,个人理解这个就是浏览器底层对于同源策略的一个具体实现。首先得到服务器端的确认,才能继续下一步的操作,这也是为什么options请求也被叫做“预检”请求的原因吧。
这段描述我感觉就和我遇到的问题一模一样,写死的http请求直接访问可以,这应该就是简单请求,包装之后的不行,就是里面描述的触发OPTIOS预检请求。
期间根据网上的各种方案进行了各种尝试,nginx 配置:
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST,PUT,DELETE,OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; add_header 'Access-Control-Max-Age' 3600;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
在后台返回头里面增加各种信息,最终都是没有效果,感觉问题找到了,但是就是无法解决,濒临崩溃,甚至想到了是否要拦截然后由IOS这边直接访问后台在返回结果这前端这种机制。
四、破局
今天早上起来在上厕所的时候还在想着这个问题要怎么处理,当我蹲在茅坑上思索的时候,又在想为什么安卓会正常呢,突然灵光一闪,我似乎看到了几行代码,对了,安卓这边之前在webview的设置中加过允许跨域的处理,是不是那几行代码有效果。于是早上来到办公室就开始尝试,刚开始加了一行代码:
[config.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];
调试后发现还是没有效果,但是我隐约从这个key的名称中来看,就是针对本地文件访问跨域的处理。
于是先到安卓端将那段代码屏蔽来进行假设验证,发现屏蔽后安卓果然也不正常了,这下心里已经有一定把握了,最终再加一个设置代码到ios wkwebview中:
//允许跨域
[config.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];
if (@available(iOS 10.0, *)) {
[config setValue:@YES forKey:@"allowUniversalAccessFromFileURLs"];
}
验证码终于出现了,这几个图片字母终于跃然屏上。心中释然,对茅塞顿开这个成语的理解再次升级,茅房顿悟真的很有效果,在我十年的工作生涯中,我感觉有好多次的问题都是在不经意间想到了解决方案,打水的时候,蹲坑的时候,窗口远眺的时候,念念不忘,必有回响,放松放空,一切皆通。
其实这个问题还是在过程中被带偏了,认为手机端的webview 相当于浏览器,然后解决跨域问题应该都是从前后端的配置去解决,没有想过去改浏览器来处理。