iOS原生与JS的交互
目的:
- 交互<1> 原生调用JS
- 交互<2> JS调用原生
方式:
- 拦截协议
- JavaScriptCore库
- WKWebView
- 自定义NSURLProtocol拦截
- WebViewJavascriptBridge
UIWebview 适配iOS7
HTML 网页的源代码
var res = {
"title":" 端午礼包提回去,丈母娘不满意来找我!!!",
"url":"http:\/\/app.xxx.com\/news\/rd\/content_wap_39865.shtml",
"image":"http:\/\/www.xxx.com\/v\/b\/images\/2016\/5\/30\/20165301464599344828_167.jpg",
"summary":" 端午礼包提回去,丈母娘不满意来找我!!!"
};
// js方法
function item_info(){
return JSON.stringify(res);
}
交互<1> 原生调用JS
方式一: 在UIWebView
的代理方法 -(void) webViewDidFinishLoad:(UIWebView *)webView
中
// 使用 OC原生的stringByEvaluatingJavaScriptFromString 方法调用js方法
NSString *info = [webView stringByEvaluatingJavaScriptFromString:@"item_info()"];
NSDictionary *shareData = [NSJSONSerialization JSONObjectWithData:[info dataUsingEncoding:NSUnicodeStringEncoding] options:NSJSONReadingAllowFragments error:nil];
方式二: JSContent
JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
NSLog(@"JSValue: %@, int: %d", jsVal, iVal);
evaluateScript后面跟 JS语句,就可以调用JS了
交互<2> JS调用原生
方式一: 使用 自定义协议头 (原生和H5定义好scheme
)
在 web 中的按钮 或者 点击链接的onclick
方法中 发起一个虚假的请求.
格式为
'协议头://路径?参数1=值1&参数2=值2'
即
yourscheme://callfunction/parameter1/parameter2?parameter3=value
// 1.请求发起方式①
window.location.href='playVideo://tide?url=http://123.125.148.27:96/video/gesila.mp4';
// 2.请求发起方式②
function execute(url)
{
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", url);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
其中 playVideo
为自定义的协议头scheme
然后客户端 原生代码中,使用 webView 的代理方法 即 重定义方法
- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSURL *url = request.URL;
if ([url.scheme isEqualToString:@"playvideo"]) {
NSArray *querys = [url.query componentsSeparatedByString:@"&"];
NSString *videoUrl = nil;
for (NSString *query in querys) {
if ([query rangeOfString:@"url="].location != NSNotFound) {
videoUrl = [query substringFromIndex:4];
break;
}
}
//使用 & 分割 想要传递的数据参数 就可以了
}
这种方式实现简单,但是写法太死板,所有的请求都在一个代理方法中,有太多的判断和重复代码.
而且有可能会影响到H5正常的网络请求.
比如H5连续两次发起此种网络请求的话,结果就是只能截获后面那次请求.
注意点:
-
关于参数也可以将Json数据进行Base64 编码,以保证 url 中不会出现一些非法的字符。
-
js call native 是异步,native call js 是同步
-
在开发中发现,当在 native 层调用
stringByEvaluatingJavaScriptFromString
方法时,可能由于 javascript 是单线程的原因,会阻塞原有 js 代码的执行。这里解决办法是在 js 端用 defer 将 iframe 的插入延后执行 -
UIWebView 的
stringByEvaluatingJavaScriptFromString
方法必须是主线程中执行,而主线程的执行时间过长就会 block UI 的更新。所以我们应该尽量让stringByEvaluatingJavaScriptFromString
方法执行的时间短
方式二: - (void)webViewDidFinishLoad:(UIWebView *)webView
使用JSContent
通过在原生中捕获H5的上下文来做到
JSContext *context = [[JSContext alloc] init];
context[@"log"] = ^() {
NSLog(@"+++++++Begin Log+++++++");
// 获取JS的log函数中传入的参数
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@", jsVal);
}
JSValue *this = [JSContext currentThis];
NSLog(@"this: %@",this);
NSLog(@"-------End Log-------");
};
// JS执行的log方法
[context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
//Output:
// +++++++Begin Log+++++++
// ider
// 7,21
// [object Object]
// this: [object GlobalObject]
// -------End Log-------
注意事项:
Block在JavaScript
和Objective-C
之间的转换 建立起更多的桥梁,让互通更方便。但是要注意的是无论是把Block传给JSContext
对象让其变成JavaScript
方法,还是把它赋给exceptionHandler
属性,在Block内都不要直接使用其外部定义的JSContext
对象或者JSValue
,应该将其当做参数传入到Block中,或者通过JSContext
的类方法+ (JSContext *)currentContext;
来获得。否则会造成循环引用使得内存无法被正确释放。
WKWebview 推荐
如果不需要适配iOS8
之前的版本,强烈建议替换成iOS
的新框架WebKit
注意WKWebView
只能够使用代码创建
交互<1> 原生调用JS
//直接调用js
webView.evaluateJavaScript("hi()", completionHandler: nil)
//调用js带参数
webView.evaluateJavaScript("hello('liuyanwei')", completionHandler: nil)
// 调用js获取返回值
webView.evaluateJavaScript("getName()") { (any,error) -> Void in
NSLog("%@", any as! String)
}
交互<2> JS调用原生
方式一:
经过以下步骤:
① WKScriptMessageHandler
的注册
let config = WKWebViewConfiguration()
//注册js方法 注意 webViewApp 为JS和原生都定义好的name,类似拦截代理的协议头
config.userContentController.addScriptMessageHandler(self, name: "webViewApp")
// 初始化
let webView = WKWebView(frame: self.webWrap.frame, configuration: config)
注意先注册,后初始化WKWebView
② 处理 WKScriptMessageHandler
的回调
遵守WKScriptMessageHandler
的代理
extention ViewController:WKScriptMessageHandler
//实现js调用ios的handle委托
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
//接受JS传过来的消息 从而决定app调用的方法
if let dict = message.body as? [String: Any],
let method = dict["method"] as? String,
let param1 = dict["param1"] as? String,
if method == "hello"{
hello(param1)
}
}
③ 截获js的调用方法,通过WKScriptMessageHandler
的name
H5的源码
var message = {
'method' : 'Hello',
'param1' : 'Word',
};
window.webkit.messageHandlers.webViewApp.postMessage(message);
方式二: 拦截代理
在WKWebView
的代理方法中拦截
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSString *url = navigationAction.request.URL.absoluteString;
NSLog(@"%@",url);
if (navigationAction.navigationType == WKNavigationTypeLinkActivated && [url rangeOfString:@"playVideo://"].location != NSNotFound) {
// 取消虚假的请求
decisionHandler(WKNavigationActionPolicyCancel);
// url的协议头是playVideo ,客户端处理自己的请求
NSLog(@"playVideo");
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
注意这里POST请求这儿还有一个坑。加载POST请求的时候,会丢失HTTPBody。解决办法是在网页上开一个JavaScript方法,在请求POST的时候去调用JavaScript这个方法,从而完成POST请求
Member discussion