前言
在 iOS 开发中,JS 与 Native 的交互分为两种,第一种是 Native 调 JS,即通过在 Native 代码中执行 JS 达到在 webkit 控件中展现相应 JS 代码的效果;另一种就是 JS 调用 Native ,通过 web 前段 JS 的执行来调用 Native 本地的方法,用以实现例如开启照相机、数据持久化等等只能通过 Native 代码实现的效果。
目前进行 JS 和 Native 交互主要有两种方式,下面进行一一介绍:
一、WebView 方法/代理方法
通常来说,iOS 中实现加载 web 页面主要有两种控件,UIWebView 和 WKWebview,两种控件对应具体的实现方法不同,我们在这里分开进行介绍:
UIWebView控件
- Native 调用 JS:
在 Native 中执行 JS 语句非常简单, JS 作为脚本语言它的执行需要解释器的存在,即浏览器,所以 UIWebView 作为浏览器控件,提供了 native 调用 JS 的对象方法:
//script 是要执行的 JS 语句
//返回值为 JS 执行结果,如果 JS 执行失败则返回 nil,如果 JS 执行没有返回值,则返回值为空字符串
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
这里编写了一个 demo 仅供参考:
- (void)webViewDidFinishLoad:(UIWebView*)webView
{
NSString* str = [self.webView stringByEvaluatingJavaScriptFromString:@"pageDidLoad()"];
NSLog(@"%@", str);
}
当 WebView 加载完毕的时候调用 JS 中的 pageDidLoad
方法,并在控制台打印 JS 的执行结果。
- JS 调用 Native:
使用 WebView 方法/代理方法完成 JS 调用 Native 要稍微复杂一点,需要 Native前端和 web 前端的良好配合,主要原理是通过 UIWebVIew 的代理方法截取 web 前端的跳转请求,通过识别与 web 前端约定好的自定义协议头来判断本次请求是否为 JS 调用 Native 的请求,来调用对应的 Native 方法。
其中涉及到的 UIWebView 代理方法为:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
下面通过例子来进行演示:
JavaScript 代码:
function btnOnClickBaidu() {
var url = "http://www.baidu.com";
alert("马上跳转的页面是:" + url);
window.location.href = url;
}
function btnOnClickNative() {
var url = "DZBridge://printSomeWords";
alert("马上跳转的页面是:" + url);
window.location.href = url;
}
function btnOnClickNativeWithConfig() {
var url = "DZBridge://printSomeWords?{\"string\":\"Hello World\"}";
alert("马上跳转的页面是:" + url);
window.location.href = url;
}
function pageDidLoad() {
alert("页面加载完毕!");
return 11;
}
OC代码:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
//dzbridge 为约定好的协议头,如果是,则页面不进行跳转
if ([request.URL.scheme isEqualToString:@"dzbridge"]) {
//截取字符串来判断是否存在参数
NSArray<NSString*>* arr = [request.URL.absoluteString componentsSeparatedByString:@"?"];
if (arr.count > 1) {
NSString* str = [arr[1] stringByRemovingPercentEncoding];
NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];
NSLog(@"%@", dict[@"string"]);
}
else {
NSLog(@"没有参数的打印");
}
return NO;
}
//不是自定义协议头,跳转页面
return YES;
}
WKWebView控件
iOS8 以后,苹果推出了新框架 WKWebKit, 其中提供了可以替换 UIWebView 的组件 WKWebView。原来 UIWebView 的各种问题得到了改善,速度更快了,占用内存少了(模拟器加载百度与开源中国网站时,WKWebView 占用23M,而UIWebView 占用85M),目前来看,WKWebView 是 App 内部加载网页更佳的选择!
WKWebView 相对 UIWebView 做了较大幅度的重构,将 UIWebViewDelegate 与 UIWebView 重构成了14类与3个协议,因此,在 WKWebView 中进行 JS 与 Native 的交互与 UIWebView 相比也有较大的不同。
- Native 调用 JS:
在 WKWebView 中 Native 调用 JS 的方式与 UIWebview 中比较相似,也是通过自己本身的一个对象方法:
// javaScriptString 为待执行的 JS 语句
// completionHandler 为执行 JS 完毕后的回调,block 的第一个参数为执行结果,第二个参数为错误
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
看下面一个小例子:
#pragma mark----- WKNavigationDelegate -----
- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
{
[self.webView evaluateJavaScript:@"pageDidLoad()" completionHandler:^(id _Nullable value, NSError* _Nullable error) {
NSLog(@"%@", value);
}];
}
- JS 调用 Native:
WKWebView 中 JS 调用 Native 与 UIWebView 有着比较大的不同,首先需要介绍几个类(/协议/属性):
-
WKWebViewConfiguration
:是 WKWebView 初始化时的配置类,里面存放着初始化 WK 的一系列属性; -
WKUserContentController
:为 JS 提供了一个发送消息的通道并且可以向页面注入 JS 的类; -
WKScriptMessageHandler
:一个协议,协议中只有一个方法,这个方法是页面执行特定 JS 的一个回调,这个特定的 JS 格式为:window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
;
WKWebViewConfiguration
作为 WK 的配置类,其中有一个属性为
@property (nonatomic, strong) WKUserContentController *userContentController;
是WKUserContentController
的一个实例,WKUserContentController
有一个对象方法为:
/*! @abstract Adds a script message handler.
@param scriptMessageHandler The message handler to add.
@param name The name of the message handler.
@discussion Adding a scriptMessageHandler adds a function
window.webkit.messageHandlers.<name>.postMessage(<messageBody>) for all
frames.
*/
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
从苹果给出的注释来看,通过该方法能够添加一个脚本消息的处理器,即(id <WKScriptMessageHandler>)scriptMessageHandler
,另外还能发现,添加脚本处理器后,需要在 JS 中添加window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
才能起作用。
demo:
// 创建并配置 WKWebView 的相关参数
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// self 指代的对象需要遵守 WKScriptMessageHandler 协议
[userContent addScriptMessageHandler:self name:@"test"];
config.userContentController = userContent;
在页面上的 JS 执行window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
时,被添加的ScriptMessageHandler
就会执行实现的WKScriptMessageHandler
协议的方法,例如:
#pragma mark----- WKScriptMessageHandler -----
/**
* JS 调用 OC 时 webview 会调用此方法
*
* @param userContentController webview 中配置的 userContentController 信息
* @param message js 执行传递的消息
*/
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message
{
NSLog(@"%@", message);
}
在代理方法中实现相应的 Native 代码,即完成了 JS 调用 Native 的过程。
二、JavaScriptCore
OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 库,把 WebKit 的 JavaScript 引擎用 Objective-C 封装,提供了简单,快速以及安全的方式接入 JavaScript。
JavaScriptCore中类及协议
- JSContext:JavaScript 运行的上下文环境
- JSValue:JavaScript 和 Objective-C 数据和方法的桥梁
- JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议
- JSManagedValue:管理数据和方法的类
- JSVirtualMachine:处理线程相关,使用较少
JavaScript 调用 Native
使用 JavaScriptCore 进行 JS 和 Native 的交互,无论想要实现什么样的效果都需要获得一个有效的 JSContext 实例,即一个有效的 JS 运行的上下文(这一步骤以下不再重复提及)。
- 获得当前的 JSContext:
可以在页面加载完毕后,采用 KVC 的方式从webView 中获得,如下:
JSContext* jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
- 将想要被暴露给 JS 的方法抽象成为一个协议(protocol),该协议需要遵守
JSExport
协议:
@protocol JSObjcDelegate <JSExport>
- (void)callCamera;
- (NSString*)share:(NSString*)shareString;
@end
- 将要暴露给 JS 的对象的类需要遵守自定义的协议,如上:
JSObjcDelegate
; - 将 OC 对象桥接到 JS 环境中,并设置异常处理
// 将本对象与 JS 中的 DZBridge 对象桥接在一起,在 JS 中 DZBridge 代表本对象
[self.jsContext setObject:self forKeyedSubscript:@"DZBridge"];
self.jsContext.exceptionHandler = ^(JSContext* context, JSValue* exceptionValue) {
context.exception = exceptionValue;
NSLog(@"异常信息:%@", exceptionValue);
};
- 在 JS 中通过 DZBridge 调用本对象暴露出的方法:
var callShare = function() {
var shareInfo = JSON.stringify({"title": "标题", "desc": "内容", "shareUrl": "//www.greatytc.com"});
var str = DZBridge.share(shareInfo);
alert(str);
}
Native 调用 JavaScript
- 第一种方式同 UIWebView 中类似,都是直接执行 JS 字符串,通过 JSContext 执行 JS 代码:
[self.jsContext evaluateScript:@"alert(\"执行 JS\")"];
- 另一种方式适用于执行 web 页面上已有的方法,通过 JSValue 来调用 JS 中的方法,JSValue 是 JavaScript 中值得一个引用,他可能包装着一个 JavaScript 的方法,通过
callWithArguments:
方法进行调用,例如:
JSValue* picCallback = self.jsContext[@"picCallback"];
[picCallback callWithArguments:@[ @"photos" ]];