一,说明:(此文转载,如果涉及任何版权问题,请联系我)
话说,当时在找js交互这块内容时,朋友给了我这个非常好的代码,我照着这个demo从头敲到尾,对js交互有了很大的理解.今天找到了代码来源,并转载他的博客.
二,讲解:
随着苹果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代码:
|
|
functioncallOC2(func,param){
variframe=document.createElement("iframe");
varurl="myapp:"+"&func="+func;
for(variinparam)
{
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=[[[requestURL] absoluteString]
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
if([requestString
hasPrefix:@"myapp:"]){
NSLog(@"requestString:%@",requestString);
//如果是自己定义的协议,
再截取协议中的方法和参数, 判断无误后在这里手动调用oc方法
NSMutableDictionary*param=[self
queryStringToDictionary:requestString];
NSLog(@"get
param:%@",[paramdescription]);
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
*
*/
returnNO;
}
returnYES;
}
|
|
- (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()
|
|
<inputtype="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
|
|
@protocolTestJSExport<JSExport>
-(void)pushViewController:(NSString*)view
title:(NSString*)title;
-(void)test:(NSString*)a;
@end
@interface
JSCallOCViewController : UIViewController<UIWebViewDelegate,TestJSExport>
@property(weak,nonatomic)IBOutletUIWebView*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
{
Classsecond=NSClassFromString(view);
idsecondVC=[[secondalloc]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=[NSThreadcurrentThread];
//网络请求等等涉及到变更线程的操作
[self
sendAsynchronousRequest:^(iditem){
NSLog(@"一层网络请求");
[self
sendAsynchronousRequest:^(iditem){
NSLog(@"二层网络请求");
//正常情况下是直接在这里调用,但是会偶尔闪退
//JSValue
*callBackJS = self.context[@"callBackJS"];
//[callBackJS
callWithArguments:nil];
[self
performSelector:@selector(callBackJS)
onThread:webviewThread
withObject:nil
waitUntilDone:NO];
}];
}];
}
//异步网络请求
-(void)sendAsynchronousRequest:(void(^)(iditem))completion{
[[[NSURLSessionsharedSession]
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"]];
}
|
三、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
JavaScript,简单的封装一下,‘Native’为事先在Objective-C注册注入的js对象
|
|
functioncallOC(func,param){
varurl="func="+func;
for(variinparam)
{
url=url+"&"+i+"="+param[i];
}
window.webkit.messageHandlers.Native.postMessage(url);
}
|
JavaScript调用
| |
<input type="button" value="打个招呼" onclick="callOC('alert',{'message':'你好么'})" />
|
Objective-C实现
|
|
WKWebViewConfiguration*config=[[WKWebViewConfigurationalloc]
init];
config.userContentController=[[WKUserContentControlleralloc]
init];
// 注入JS对象Native,
// 声明WKScriptMessageHandler 协议
[config.userContentController
addScriptMessageHandler:self
name:@"Native"];
self.myWebView=[[WKWebViewalloc]
initWithFrame:self.view.bounds
configuration:config];
self.myWebView.UIDelegate=self;
[self.view
addSubview:self.myWebView];
|
|
|
#pragma mark - WKScriptMessageHandler
-(void)userContentController:(WKUserContentController*)userContentController
didReceiveScriptMessage:(WKScriptMessage*)message{
if([message.name
isEqualToString:@"Native"]){
NSLog(@"message.body:%@",message.body);
//如果是自己定义的协议,
再截取协议中的方法和参数, 判断无误后在这里手动调用oc方法
NSMutableDictionary*param=[self
queryStringToDictionary:message.body];
NSLog(@"get
param:%@",[paramdescription]);
NSString*func=[param
objectForKey:@"func"];
//调用本地函数
if([func
isEqualToString:@"alert"])
{
[self
showMessage:@"来自网页的提示"
message:[param
objectForKey:@"message"]];
}
}
}
|
注:本文除了第三种方法之外,前两种JavaScript交互方法都和Android开发兼容,仅仅是api略不同。
三,Demo地址: