OC与JS交互之UIWebView
一、UIWebView介绍
(1)UIWebView提供了三个方法来加载html资源
1、 loadHTMLString:baseURL: 把html文件的内容以字符串的形式加载到webView里面,然后解析。编码为UTF8
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"]; NSURL *baseURL = [[NSBundle mainBundle] bundleURL]; [self.webView loadHTMLString:[NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil] baseURL:baseURL];
2、loadData:MIMEType:textEncodingName:baseURL: 把html文件的内容以二进制的形式加载到webView里面,然后解析。MIMETYPE指定html文件的格式为"text/html",编码为UTF8
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"]; NSURL *baseURL = [[NSBundle mainBundle] bundleURL]; NSData *data = [NSData dataWithContentsOfFile:filePath]; [self.webView loadData:data MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:baseURL];
3、loadRequest:构造一个请求,webView以请求的形式加载本地html文件,然后解析。这种方式可以直接加载网络html
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"]; NSURL *url = [NSURL fileURLWithPath:filePath]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [self.webView loadRequest:request];
(2)UIWebView代理
//成为UIWebView的代理,遵守UIWebViewDelegate协议,就能监听UIWebView的加载过程 //UIWebView在发送请求之前,都会调用这个方法,如果返回NO,代表停止加载请求,返回YES,代表允许加载请求 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType; //开始发送请求(加载数据)时调用这个方法 - (void)webViewDidStartLoad:(UIWebView *)webView; //请求完毕(加载数据完毕)时调用这个方法 - (void)webViewDidFinishLoad:(UIWebView *)webView; //请求错误时调用这个方法 - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
(3)常用属性和方法
// 刷新 - (void)reload; // 停止加载 - (void)stopLoading; // 后退函数 - (void)goBack; // 前进函数 - (void)goForward; // 是否可以后退 @property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack; // 是否可以向前 @property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward; // 是否正在加载 @property (nonatomic, readonly, getter=isLoading) BOOL loading;
二、基于UIWebView"协议拦截"实现的交互方式
(1)JS调用OC
实现原理
1、JS与iOS约定好jsToOc协议,用作JS在调用iOS时url的scheme;
2、点击JS中的按钮加载含有token数据的url:(jsToOc://loginSucceed?js_tokenString);
3、iOS的UIWebView在加载请求前都会调用-webView:shouldStartLoadWithRequest:navigationType:方法来确认是否加载此请求;
4、iOS在此方法内截取jsToOc协议获取JS传过来的数据,执行OC相应的方法,并不允许加载此请求。
JS代码:
var loginBtn = document.getElementById("loginID"); loginBtn.onclick = function () { var token = "js_tokenString"; loginSuccess(token); } function loginSuccess(token) { var action = "loginSuccess"; jsToOc(action, token); } //JS调用OC入口 function jsToOc(action, params) { var url = "jsToOc://" + action + "?" + params; window.location.href = url; }
OC代码:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ NSString *absolutePath = request.URL.absoluteString; NSString *scheme = request.URL.scheme; NSString *host = request.URL.host; NSString *query = request.URL.query; if ([scheme caseInsensitiveCompare:@"jsToOc"] == NSOrderedSame) { UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"JS调OC01" message:query delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; return NO; } return YES; }
除了显示截取到的数据,iOS还可以将request.URL.host
看作JS想调用的方法名,将request.URL.query
看作该方法的参数集,从而体现出JS调用iOS
的概念。
JS代码:
function btnClick1() { window.location.href = "rrcc://showMobile" } function btnClick2() { window.location.href = "rrcc://showName_?xiaohuang" } function btnClick3() { window.location.href = "rrcc://showSendNumber_msg_?13300001111&go climbing this weekend" }
OC代码:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ NSString *absolutePath = request.URL.absoluteString; NSString *scheme = request.URL.scheme; NSString *host = request.URL.host; NSString *query = request.URL.query; if ([scheme caseInsensitiveCompare:@"rrcc"] == NSOrderedSame){ NSString *schemeStr = @"rrcc://"; NSString *subPath = [absolutePath substringFromIndex:schemeStr.length]; if ([subPath containsString:@"?"]) {//1个或多个参数 if ([subPath containsString:@"&"]) {//多个参数 NSArray *components = [subPath componentsSeparatedByString:@"?"]; NSString *methodName = [components firstObject]; methodName = [methodName stringByReplacingOccurrencesOfString:@"_" withString:@":"]; SEL sel = NSSelectorFromString(methodName); NSString *parameter = [components lastObject]; NSArray *params = [parameter componentsSeparatedByString:@"&"]; if (params.count == 2) { if ([self respondsToSelector:sel]) { [self performSelector:sel withObject:[params firstObject] withObject:[params lastObject]]; } } }else{//1个参数 NSArray *components = [subPath componentsSeparatedByString:@"?"]; NSString *methodName = [components firstObject]; methodName = [methodName stringByReplacingOccurrencesOfString:@"_" withString:@":"]; SEL sel = NSSelectorFromString(methodName); NSString *parameter = [components lastObject]; if ([self respondsToSelector:sel]) { [self performSelector:sel withObject:parameter]; } } }else{//没有参数 NSString *methodName = [subPath stringByReplacingOccurrencesOfString:@"_" withString:@":"]; SEL sel = NSSelectorFromString(methodName); if ([self respondsToSelector:sel]) { [self performSelector:sel]; } } return NO; } return YES; } - (void)showMobile{ NSLog(@"showMobile"); } - (void)showName:(NSString *)name{ NSLog(@"showName:%@", name); } - (void)showSendNumber:(NSString *)phone msg:(NSString *)msg{ NSLog(@"showSendNumbermsg:==phone:%@===msg:%@", phone, msg); }
(2)OC调用JS
OC使用UIWebView的-stringByEvaluatingJavaScriptFromString:方法访问JS;
JS代码:
function alertMobile() { alert('alertMobile 我是手机号是:13300001111') } function alertName(msg) { alert('alertName 你好 ' + msg + ', 我也很高兴见到你') }
OC代码:
- (void)btnAction:(UIButton *)sender{ if (sender.tag == 123) { [self.webView stringByEvaluatingJavaScriptFromString:@"alertMobile()"]; } if (sender.tag == 234) { [self.webView stringByEvaluatingJavaScriptFromString:@"alertName('小红')"]; } }
UIWebView的-stringByEvaluatingJavaScriptFromString:方法可以执行JS代码。但只有在整个webView加载完成之后调用此方法才会有响应。
三、OC与JS交互之JavaScriptCore
JavaScriptCore提供了JavaScript和Objective-C桥接的Obj-C API。JavaScriptCore提供了让我们脱离UIWebView执行JavaScript脚本的能力,以及使用现代的Objective-C语法(例如Blocks和下标)在Objective-C和JavaScript之间无缝的传递值或者对象。
(1)JS调用OC
原理:
1、JS与iOS约定好jsToOc方法,作为JS调用iOS的入口;
2、iOS通过[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]获取JS代码的上下文JSContext;
3、JS调用jsToOc方法,iOS使用Block形式监听(重写)此方法context[@"jsToOc"] = ^() {},收到JS的调用请求和参数;
JS代码:
var loginBtn = document.getElementById("loginID"); loginBtn.onclick = function () { var token = "js_tokenString"; loginSuccess(token); } //登录成功 function loginSuccess(token) { var action = "loginSuccess"; jsToOc(action, token); } //JS调用OC入口 function jsToOc(action, params) { var url = "jsToOc://" + action + "?" + params; window.location.href = url; }
function btnClick1() { //showMobile(); window.location.href = "rrcc://showMobile"; } function btnClick2() { showName('xiaohuang') } function btnClick3() { showSendMsg('13300001111', 'Go Climbing This Weekend !!!') }
OC代码:
#import <JavaScriptCore/JavaScriptCore.h>
//UIWebView在每次加载请求完成后会调用此方法 - (void)webViewDidFinishLoad:(UIWebView *)webView{ //获取JS代码的执行环境/上下文/作用域 JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; //监听JS代码里面的方法(执行效果上可以理解成重写了JS的方法) context[@"jsToOc"] = ^(NSString *action, NSString *param){ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:action message:param delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); }; context[@"btnClick1"] = ^{ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showMobile" message:@"showMobile" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); }; // context[@"showName"] = ^(NSString *name){ // dispatch_async(dispatch_get_main_queue(), ^{ // UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showName" message:name delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; // [alertView show]; // }); // }; void(^showName)(NSString *name) = ^(NSString *name){ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showName" message:name delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); }; [context setObject:showName forKeyedSubscript:@"showName"]; context[@"showSendMsg"] = ^(NSString *phone, NSString *msg){ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:phone message:msg delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); }; }
(2)OC调JS
JS代码:
function ocToJs(action, params) { document.getElementById("returnValue").innerHTML = action + '?' + params; } function alertMobile() { alert('alertMobile 我是手机号是:13300001111') } function alertName(msg) { alert('alertName 你好 ' + msg + ', 我也很高兴见到你') } function alertSendMsg(num,msg) { alert('alertSendMsg这是我的手机号:' + num + ',' + msg + '!!') }
OC代码:
#pragma mark - Action - (void)loginBtn01Action:(UIButton *)btn{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; [context evaluateScript:[NSString stringWithFormat:@"ocToJs('loginSucceed', 'oc_tokenString')"]]; }); } - (void)btnAction:(UIButton *)sender{ if (sender.tag == 123) { //[self.webView stringByEvaluatingJavaScriptFromString:@"alertMobile()"]; JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; [context evaluateScript:[NSString stringWithFormat:@"alertMobile()"]]; } if (sender.tag == 234) { //[self.webView stringByEvaluatingJavaScriptFromString:@"alertName('小红')"]; JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; [context evaluateScript:[NSString stringWithFormat:@"alertName('小红')"]]; } if (sender.tag == 345) { //[self.webView stringByEvaluatingJavaScriptFromString:@"alertSendMsg('18870707070','周末爬山真是件愉快的事情')"]; JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; [context evaluateScript:[NSString stringWithFormat:@"alertSendMsg('18870707070','周末爬山真是件愉快的事情')"]]; } }
除了使用JSContext的-evaluateScript:方法之外,还可以先通过[context[@"ocToJs"]获取到JS的ocToJs方法对应的JSValue,然后使用JSValue的-callWithArguments:方法调用JS的ocToJs方法。
四、OC与JS交互之JSExport协议
下面介绍如何使用JSExport协议在 UIWebView上实现OC与JS交互。JSExport是JavaScriptCore框架里的一个协议。如果一个协议遵守了JSExport,那么该协议的方法会对JS开放,允许JS直接调用。
JS调OC
实现原理:
1、JS与iOS约定好OCJSBridge类名和jsToOc方法,作为JS调用iOS的入口OCJSBridge.jsToOc;
2、iOS创建遵守JSExport协议的OCJSExport协议,在该协议中声明-jsToOc:params:方法(起个别名jsToOc);
3、UIWebViewJSExportController遵守OCJSExport协议,实现该协议中的-jsToOc:params:方法;
4、iOS通过[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]获取JS代码的上下文JSContext;
5、iOS通过context[@"OCJSBridge"] = self;在context注册OCJSBridge对象为self(UIWebViewJSExportController的实例);
6、JS调用OCJSBridge.jsToOc方法,iOSUIWebViewJSExportController的-jsToOc:params:方法会响应JS的调用请求和参数;
JS代码:
var loginBtn = document.getElementById("loginID"); loginBtn.onclick = function () { var token = "js_tokenString"; loginSuccess(token); } //登录成功 function loginSuccess(token) { var action = "loginSuccess"; OCJSBridge.jsToOc(action, token); } function btnClick1() { OCJSBridge.showMobile(); } function btnClick2() { OCJSBridge.showName('xiaohuang'); } function btnClick3() { OCJSBridge.showSendMsg('13300001111', 'Go Climbing This Weekend !!!'); }
OC代码
#import <JavaScriptCore/JavaScriptCore.h>
@protocol OCJSExport <JSExport>
//为OC的-jsToOC:params:方法起个JS认识的别名jsToOc
JSExportAs(jsToOc, - (void)jsToOc:(NSString *)action params:(NSString *)params);
JSExportAs(showSendMsg, - (void)showSendMsg:(NSString *)phone params:(NSString *)msg);
JSExportAs(showName, - (void)showName:(NSString *)name);
//无参数和单参数时可不起别名
- (void)showMobile;
@end
//UIWebView在每次加载请求完成后会调用此方法 - (void)webViewDidFinishLoad:(UIWebView *)webView{ //获取JS代码的执行环境/上下文/作用域 JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; //! 在context注册OCJSBridge对象为self context[@"OCJSBridge"] = self;//!< 有循环引用问题 }
//! 实现OCJSExport协议的方法 - (void)jsToOc:(NSString *)action params:(NSString *)params { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:action message:params delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); } - (void)showMobile{ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showMobile" message:@"showMobile" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); } - (void)showName:(NSString *)name{ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:name delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); } - (void)showSendMsg:(NSString *)phone params:(NSString *)msg{ dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:phone message:msg delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alertView show]; }); }
1、JS的方法命名规则与OC不一样,当OCJSExport协议中的方法有多个参数时,需要使用JSExportAs(<#PropertyName#>, <#Selector#>)为OC方法起个别名。否则,JS就需要很别扭的使用OCJSBridge.jsToOcParams来调用OC的-jsToOc:params:方法(无参数和单参数时可不起别名)。
2、context[@"OCJSBridge"] = self;会有循环引用问题,导致self的-dealloc方法不被执行。因为JS中没有弱引用,所以__weak在这里不起作用。一般来说,可以使用单独的类来处理OCJSExport协议的相关方法,以解决此问题(比如:context[@"OCJSBridge"] = [OCJSBridge new];)。
参考:https://www.jianshu.com/p/f94448235209
https://www.cnblogs.com/markstray/p/5757238.html
https://www.jianshu.com/p/f4ec947f0721
https://www.cnblogs.com/markstray/p/5757255.html
GitHub:https://github.com/hongsheng1024/RunTime