随着苹果SDK的不断升级,越来越多的新特性增加了进来,本文主要讲述从iOS6至今,Native与JavaScript的交互方法
一、UIWebview && iframe && JavaScript <=iOS6
iOS6原生没有提供js直接调用Objective-C的方式,只能通过UIWebView的UIWebViewDelegate协议
(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
方法来做拦截,并在这个方法中,根据url来调用Objective-C方法
1.javascript调用Objective-C
动态添加个iframe改变其地址 最后删除,这种方法不会使当前页面跳转 效果更佳
javascript代码:
function callOC2(func,param){
var iframe = document.createElement("iframe");
var url= "myapp:" + "&func=" + func;
for(var i in param)
{
url = url + "&" + i + "=" + param[i];
}
iframe.src = url;
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.parentNode.removeChild(iFrame);
iframe = null;
}
使用方法
<input type="button" value="传个字典2" onclick="callOC2('testFunc',{'param1':76,'param2':155,'param3':76})" />
Objective-C代码:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *requestString = [[[request URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding ];
if ([requestString hasPrefix:@"myapp:"]) {
NSLog(@"requestString:%@",requestString);
//如果是自己定义的协议, 再截取协议中的方法和参数, 判断无误后在这里手动调用oc方法
NSMutableDictionary *param = [self queryStringToDictionary:requestString];
NSLog(@"get param:%@",[param description]);
NSString *func = [param objectForKey:@"func"];
if([func isEqualToString:@"callFunc"])
{
[self testFunc:[param objectForKey:@"first"] withParam2:[param objectForKey:@"second"] andParam3:[param objectForKey:@"third"] ];
}
/*
* 方法的返回值是BOOL值。
* 返回YES:表示让浏览器执行默认操作,比如某个a链接跳转
* 返回NO:表示不执行浏览器的默认操作,这里因为通过url协议来判断js执行native的操作,肯定不是浏览器默认操作,故返回NO
*
*/
return NO;
}
return YES;
}
- (NSMutableDictionary*)queryStringToDictionary:(NSString*)string {
NSMutableArray *elements = (NSMutableArray*)[string componentsSeparatedByString:@"&"];
NSMutableDictionary *retval = [NSMutableDictionary dictionaryWithCapacity:[elements count]];
for(NSString *e in elements) {
NSArray *pair = [e componentsSeparatedByString:@"="];
[retval setObject:[pair objectAtIndex:1]?:@"" forKey:[pair objectAtIndex:0]?@:"nokey"];
}
return retval;
}
2.Objective-C调用javascript
//插入js 并且执行传值
- (IBAction)insertJSTouched:(id)sender {
NSString *insertString = [NSString stringWithFormat:
@"var script = document.createElement('script');"
"script.type = 'text/javascript';"
"script.text = \"function jsFunc() { "
"var a=document.getElementsByTagName('body')[0];"
"alert('%@');"
"}\";"
"document.getElementsByTagName('head')[0].appendChild(script);", self.someString];
NSLog(@"insert string %@",insertString);
[self.myWeb stringByEvaluatingJavaScriptFromString:insertString];
[self.myWeb stringByEvaluatingJavaScriptFromString:@"jsFunc();"];
}
//提交form表单
- (IBAction)submitTouched:(id)sender {
[self.myWeb stringByEvaluatingJavaScriptFromString:@"document.forms[0].submit(); "];
}
//修改标签属性
- (IBAction)fontTouched:(id)sender {
NSString *tempString2 = [NSString stringWithFormat:@"document.getElementsByTagName('p')[0].style.fontSize='%@';",@"19px"];
[self.myWeb stringByEvaluatingJavaScriptFromString:tempString2];
}
(PS)如果你想去掉webview弹出的alert 中的来自XXX网页
- (void)webViewDidFinishLoad: (UIWebView *) webView
{
//重定义web的alert方法,捕获webview弹出的原生alert 可以修改标题和内容等等
[webView stringByEvaluatingJavaScriptFromString:@"window.alert = function(message) { window.location = \"myapp:&func=alert&message=\" + message; }"];
}
if([func isEqualToString:@"alert"])
{
[self showMessage:@"来自网页的提示" message:[param objectForKey:@"message"]];
}
二、JavaScriptCore && UIWebview >=iOS7
iOS7中加入了JavaScriptCore.framework框架。把 WebKit 的 JavaScript 引擎用 Objective-C 封装。该框架让Objective-C和JavaScript代码直接的交互变得更加的简单方便。
合适时机注入交互对象
什么时候UIWebView会创建JSContext环境?
分两种方式
第一在渲染网页时遇到<script标签时,就会创建JSContext环境去运行JavaScript代码。
第二就是使用方法[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]去获取JSContext环境时,这时无论是否遇到<script标签,都会去创造出来一个JSContext环境,而且和遇到<script标签再创造环境是同一个。
什么时候注入JSContext问题
我通常都会在 - (void)webViewDidFinishLoad:(UIWebView *)webView中去注入交互对象,但是这时候网页还没加载完,JavaScript那边已经调用交互方法,这样就会调不到原生应用的方法而出现问题。
改成在- (void)viewDidLoad中去注入交互对象,这样倒是解决了上面的问题,但是同时又引起了一个新的问题就是在一个网页内部点击链接跳转到另一个网页的时候,第二个页面需要交互,这时JSContext环境已经变化,但是- (void)viewDidLoad仅仅加载一次,跳转的时候,没有再次注入交互对象,这样就会导致第二个页面没法进行交互。当然你可以在- (void)viewDidLoad和- (void)webViewDidFinishLoad:(UIWebView *)webView都注入一次,但是一定会有更优雅的办法去解决此问题。
如果上边的方案能满足需求,建议实在迫不得已再用这个方法, 就是在每次创建JSContext环境的时候,我们都去注入此交互对象这样就解决了上面的问题。具体解决办法参考了此开源库UIWebView-TS_JavaScriptContext(有时会被APPStore检查出使用私有API 上架会被拒绝 不建议使用)。
多个iFrame中的JSContext问题
NSArray *frames = [webView valueForKeyPath:@"documentView.webView.mainFrame.childFrames"];
[frames enumerateObjectsUsingBlock:^(id frame, NSUInteger idx, BOOL *stop) {
JSContext *context = [frame valueForKeyPath:@"javaScriptContext"];
context[@"Window"][@"prototype"][@"alert"] = ^(NSString *message) {
NSLog(@"%@", message);
};
}];
1. JavaScriptCore调用Objective-C
html中的JS代码,直接调用oc注入的javascript方法 mutiParams()
<input type="button" value="多参数调用" onclick="mutiParams('参数1','参数2','参数3');" />
直接将方法mutiParams()注入到javascript中,iOS中的代码 UIWebview的delegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
// 以 html title 设置 导航栏 title
self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
// Undocumented access to UIWebView's JSContext
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 打印异常
self.context.exceptionHandler =
^(JSContext *context, JSValue *exceptionValue)
{
context.exception = exceptionValue;
NSLog(@"%@", exceptionValue);
};
// 以 block 形式关联 JavaScript function
self.context[@"log"] =
^(NSString *str)
{
NSLog(@"%@", str);
};
//多参数
self.context[@"mutiParams"] =
^(NSString *a,NSString *b,NSString *c)
{
NSLog(@"%@ %@ %@",a,b,c);
};
}
JSExport 协议关联 native对象,进而调用对象协议中约定的方法
Objective-C
@protocol TestJSExport <JSExport> - (void)pushViewController:(NSString *)view title:(NSString *)title; - (void)test:(NSString *)a; @end @interface JSCallOCViewController : UIViewController<UIWebViewDelegate,TestJSExport> @property (weak, nonatomic) IBOutlet UIWebView *webView; @property (strong, nonatomic) JSContext *context; @end
将名为"app"的对象注入到javascript window对象中,javascript可以使用"app对象"调用TestJSExport协议中的所有方法
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
// 以 html title 设置 导航栏 title
self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
// Undocumented access to UIWebView's JSContext
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 打印异常
self.context.exceptionHandler =
^(JSContext *context, JSValue *exceptionValue)
{
context.exception = exceptionValue;
NSLog(@"%@", exceptionValue);
};
// 以 JSExport 协议关联 native 对象
self.context[@"app"] = self;
}
- (void)pushViewController:(NSString *)view title:(NSString *)title
{
Class second = NSClassFromString(view);
id secondVC = [[second alloc]init];
((UIViewController*)secondVC).title = title;
[self.navigationController pushViewController:secondVC animated:YES];
}
JavaScript
<a id="push" href="#" onclick="app.pushViewControllerTitle('SecondViewController','secondPushedFromJS');">
app.pushViewControllerTitle()(也可以调用 window.app.pushViewControllerTitle()),当多参数oc方法与javascript关联时,oc方法转为javascript方法,参数转化为括号(参数1,参数2,参数N...)
2.Objective-C 调用 JavaScriptCore
Objective-C
调用js的showResult方法,这里是一个参数 result,多个就依次写到数组中
[self.context[@"showResult"] callWithArguments:@[result]];
JavaScript
function showResult(resultNumber)
{
document.getElementById("result").innerText = resultNumber;
}
callWithArguments 线程问题
JSValue的callWithArguments就是oc调用js函数所执行的方法,正常调用callWithArguments的时候偶尔会崩溃。显示一堆webview 线程出错堆栈。
很容易联想到是callWithArguments的执行线程问题,试了下主线程与子线程与webview thread,最终webview最安全,几乎不会报错。个人理解JavaScriptCore的“主线程”其实就是他所在的webThread。所以放在webThread中是正确的方法。
///假设这个函数js调用oc的方法。或者JSExport方法。
- (void)jsCallOCMethod{
//获取webView线程
NSThread *webviewThread = [NSThread currentThread];
//网络请求等等涉及到变更线程的操作
[self sendAsynchronousRequest:^(id item) {
NSLog(@"一层网络请求");
[self sendAsynchronousRequest:^(id item) {
NSLog(@"二层网络请求");
//正常情况下是直接在这里调用,但是会偶尔闪退
//JSValue *callBackJS = self.context[@"callBackJS"];
//[callBackJS callWithArguments:nil];
[self performSelector:@selector(callBackJS) onThread:webviewThread withObject:nil waitUntilDone:NO];
}];
}];
}
//异步网络请求
- (void)sendAsynchronousRequest:(void (^)(id item))completion{
[[[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://api.skyfox.org/project/afndemo/newsList.do"]] completionHandler:^(NSData *taskData, NSURLResponse *taskResponse, NSError *taskError) {
completion(taskData);
}] resume];
}
//oc回调js,callBackJS方法存在于网页javascript中。
- (void)callBackJS{
JSValue *callBackJS = self.context[@"callBackJS"];
[callBackJS callWithArguments:@[@"name",@"id"]];
}
还有一个解决办法就是在执行callWithArguments之前执行下 [webView stringByEvaluatingJavaScriptFromString:@""];
三、WKWebView && JavaScript >=iOS8
iOS 8引入了一个新的框架——WebKit,之后变得好起来了。在WebKit框架中,有WKWebView可以替换UIKit的UIWebView和AppKit的WebView,而且提供了在两个平台可以一致使用的接口。WebKit框架使得开发者可以在原生App中使用Nitro来提高网页的性能和表现,Nitro就是Safari的JavaScript引擎 WKWebView 不支持JavaScriptCore的方式但提供message handler的方式为JavaScript 与Native通信.
1.Objective-C 调用JavaScript
//执行html 已经存在的js方法
- (IBAction)exeFuncTouched:(id)sender {
[self.myWebView evaluateJavaScript:@"showAlert('hahahha')" completionHandler:^(id item, NSError * _Nullable error) {
}];
}
2. JavaScript 调用 Objective-C
本人的demo只注入了一个"Native"对象,然后使用"function"进行方法区分,当然你也可以注册多个messageHandler。
messageHandle调用方法如下两种方式:
window.webkit.messageHandlers.<messageHandlerName>.postMessage(<messageBody>) //点语法不能动态传messageHandlerName只能写死,方括号可以动态传值 window.webkit.messageHandlers[messageHandlerName].postMessage(<messageBody>)
简单的封装一下,本方法兼容安卓与iOS,‘Native’为事先在Objective-C注册注入的js对象
//本方法兼容安卓与iOS
function callMobile(handlerInterface,handlerMethod,parameters){
//handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
var dic = {'handlerInterface':handlerInterface,'function':handlerMethod,'parameters': parameters};
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)){
window.webkit.messageHandlers[handlerInterface].postMessage(dic);
}else{
//安卓传输不了js json对象,只能传输string
window[handlerInterface][handlerMethod](JSON.stringify(dic));
}
}
function callMobileNative(handlerInterface,handlerMethod,parameters){
//如果只注入一个js交互对象
callMobile("Native",handlerMethod,parameters);
}
handlerInterface:由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来,一个app可以输入一个或者多个,本人习惯注入一个。
handlerMethod:为具体调用哪个方法,当然也可以使用handlerInterface来区分调用哪个方法。
parameters:为调用方法所需参数
JavaScript调用
<input type="button" value="打个招呼" onclick="callMobile('Native','dosomeThing',{'message':'你好么'})" />
<input type="button" value="打个招呼" onclick="callMobileNative('dosomeThing',{'message':'你好么'})" /
Objective-C实现
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; config.userContentController = [[WKUserContentController alloc] init]; // 注入JS对象Native, // 声明WKScriptMessageHandler 协议 [config.userContentController addScriptMessageHandler:self name:@"Native"]; //本人喜欢只定义一个MessageHandler协议 当然可以定义其他MessageHandler协议 [config.userContentController addScriptMessageHandler:self name:@"Pay"]; [config.userContentController addScriptMessageHandler:self name:@"messageHandler1"]; [config.userContentController addScriptMessageHandler:self name:@"messageHandler2"]; [config.userContentController addScriptMessageHandler:self name:@"messageHandler3"]; self.myWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config]; self.myWebView.UIDelegate = self; [self.view addSubview:self.myWebView]
js调用原生后会触发didReceiveScriptMessage方法,WKScriptMessage包含js调用方法的所有信息。message.name为messageHander的名称,message.body为调用方法的参数。
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
NSDictionary *bodyParam = (NSDictionary*)message.body;
NSString *func = [bodyParam objectForKey:@"function"];
NSLog(@"MessageHandler Name:%@", message.name);
NSLog(@"MessageHandler Body:%@", message.body);
NSLog(@"MessageHandler Function:%@",func);
//本人喜欢只定义一个MessageHandler协议 当然可以定义其他MessageHandler协议
if ([message.name isEqualToString:@"Native"])
{
NSDictionary *parameters = [bodyParam objectForKey:@"parameters"];
if([func isEqualToString:@"alert"])
{
[self showMessage:@"来自网页的提示" message:[parameters description]];
}
} else if ([message.name isEqualToString:@"Pay"]) {
//如果是自己定义的协议, 再截取协议中的方法和参数, 判断无误后在这里进行逻辑处理
} else if ([message.name isEqualToString:@"messageHandler1"]) {
//........
}
}
四、与安卓的兼容
UIWebview location监听url,解析参数的交互方式,与安卓的监听url变化交互方式相同,调用方法一致。
UIWebview与JavascriptCore的交互方式,与安卓的addJavascriptInterface方式都是往window对象中注入对象,调用方法一致。
WKWebView与安卓的addJavascriptInterface方式,存在很大不同,以下是兼容代码。
//本方法兼容安卓与iOS
function callMobile(handlerInterface,handlerMethod,parameters){
//handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
var dic = {'handlerInterface':handlerInterface,'function':handlerMethod,'parameters': parameters};
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)){
window.webkit.messageHandlers[handlerInterface].postMessage(dic);
}else{
//安卓传输不了js json对象
window[handlerInterface][handlerMethod](JSON.stringify(dic));
}
}
iOS与android Demo地址: https://github.com/shaojiankui/iOS-WebView-JavaScript
