前言
在 iOS 開(kāi)發(fā)中,JS 與 Native 的交互分為兩種,第一種是 Native 調(diào) JS,即通過(guò)在 Native 代碼中執(zhí)行 JS 達(dá)到在 webkit 控件中展現(xiàn)相應(yīng) JS 代碼的效果;另一種就是 JS 調(diào)用 Native ,通過(guò) web 前段 JS 的執(zhí)行來(lái)調(diào)用 Native 本地的方法,用以實(shí)現(xiàn)例如開(kāi)啟照相機(jī)、數(shù)據(jù)持久化等等只能通過(guò) Native 代碼實(shí)現(xiàn)的效果。
目前進(jìn)行 JS 和 Native 交互主要有兩種方式,下面進(jìn)行一一介紹:
一、WebView 方法/代理方法
通常來(lái)說(shuō),iOS 中實(shí)現(xiàn)加載 web 頁(yè)面主要有兩種控件,UIWebView 和 WKWebview,兩種控件對(duì)應(yīng)具體的實(shí)現(xiàn)方法不同,我們?cè)谶@里分開(kāi)進(jìn)行介紹:
UIWebView控件
- Native 調(diào)用 JS:
在 Native 中執(zhí)行 JS 語(yǔ)句非常簡(jiǎn)單, JS 作為腳本語(yǔ)言它的執(zhí)行需要解釋器的存在,即瀏覽器,所以 UIWebView 作為瀏覽器控件,提供了 native 調(diào)用 JS 的對(duì)象方法:
//script 是要執(zhí)行的 JS 語(yǔ)句
//返回值為 JS 執(zhí)行結(jié)果,如果 JS 執(zhí)行失敗則返回 nil,如果 JS 執(zhí)行沒(méi)有返回值,則返回值為空字符串
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
這里編寫了一個(gè) demo 僅供參考:
- (void)webViewDidFinishLoad:(UIWebView*)webView
{
NSString* str = [self.webView stringByEvaluatingJavaScriptFromString:@"pageDidLoad()"];
NSLog(@"%@", str);
}
當(dāng) WebView 加載完畢的時(shí)候調(diào)用 JS 中的 pageDidLoad 方法,并在控制臺(tái)打印 JS 的執(zhí)行結(jié)果。
- JS 調(diào)用 Native:
使用 WebView 方法/代理方法完成 JS 調(diào)用 Native 要稍微復(fù)雜一點(diǎn),需要 Native前端和 web 前端的良好配合,主要原理是通過(guò) UIWebVIew 的代理方法截取 web 前端的跳轉(zhuǎn)請(qǐng)求,通過(guò)識(shí)別與 web 前端約定好的自定義協(xié)議頭來(lái)判斷本次請(qǐng)求是否為 JS 調(diào)用 Native 的請(qǐng)求,來(lái)調(diào)用對(duì)應(yīng)的 Native 方法。
其中涉及到的 UIWebView 代理方法為:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
下面通過(guò)例子來(lái)進(jìn)行演示:
JavaScript 代碼:
function btnOnClickBaidu() {
var url = "http://www.baidu.com";
alert("馬上跳轉(zhuǎn)的頁(yè)面是:" + url);
window.location.href = url;
}
function btnOnClickNative() {
var url = "DZBridge://printSomeWords";
alert("馬上跳轉(zhuǎn)的頁(yè)面是:" + url);
window.location.href = url;
}
function btnOnClickNativeWithConfig() {
var url = "DZBridge://printSomeWords?{\"string\":\"Hello World\"}";
alert("馬上跳轉(zhuǎn)的頁(yè)面是:" + url);
window.location.href = url;
}
function pageDidLoad() {
alert("頁(yè)面加載完畢!");
return 11;
}
OC代碼:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
//dzbridge 為約定好的協(xié)議頭,如果是,則頁(yè)面不進(jìn)行跳轉(zhuǎn)
if ([request.URL.scheme isEqualToString:@"dzbridge"]) {
//截取字符串來(lái)判斷是否存在參數(shù)
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(@"沒(méi)有參數(shù)的打印");
}
return NO;
}
//不是自定義協(xié)議頭,跳轉(zhuǎn)頁(yè)面
return YES;
}
WKWebView控件
iOS8 以后,蘋果推出了新框架 WKWebKit, 其中提供了可以替換 UIWebView 的組件 WKWebView。原來(lái) UIWebView 的各種問(wèn)題得到了改善,速度更快了,占用內(nèi)存少了(模擬器加載百度與開(kāi)源中國(guó)網(wǎng)站時(shí),WKWebView 占用23M,而UIWebView 占用85M),目前來(lái)看,WKWebView 是 App 內(nèi)部加載網(wǎng)頁(yè)更佳的選擇!
WKWebView 相對(duì) UIWebView 做了較大幅度的重構(gòu),將 UIWebViewDelegate 與 UIWebView 重構(gòu)成了14類與3個(gè)協(xié)議,因此,在 WKWebView 中進(jìn)行 JS 與 Native 的交互與 UIWebView 相比也有較大的不同。
- Native 調(diào)用 JS:
在 WKWebView 中 Native 調(diào)用 JS 的方式與 UIWebview 中比較相似,也是通過(guò)自己本身的一個(gè)對(duì)象方法:
// javaScriptString 為待執(zhí)行的 JS 語(yǔ)句
// completionHandler 為執(zhí)行 JS 完畢后的回調(diào),block 的第一個(gè)參數(shù)為執(zhí)行結(jié)果,第二個(gè)參數(shù)為錯(cuò)誤
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
看下面一個(gè)小例子:
#pragma mark----- WKNavigationDelegate -----
- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
{
[self.webView evaluateJavaScript:@"pageDidLoad()" completionHandler:^(id _Nullable value, NSError* _Nullable error) {
NSLog(@"%@", value);
}];
}
- JS 調(diào)用 Native:
WKWebView 中 JS 調(diào)用 Native 與 UIWebView 有著比較大的不同,首先需要介紹幾個(gè)類(/協(xié)議/屬性):
-
WKWebViewConfiguration:是 WKWebView 初始化時(shí)的配置類,里面存放著初始化 WK 的一系列屬性; -
WKUserContentController:為 JS 提供了一個(gè)發(fā)送消息的通道并且可以向頁(yè)面注入 JS 的類; -
WKScriptMessageHandler:一個(gè)協(xié)議,協(xié)議中只有一個(gè)方法,這個(gè)方法是頁(yè)面執(zhí)行特定 JS 的一個(gè)回調(diào),這個(gè)特定的 JS 格式為:window.webkit.messageHandlers.<name>.postMessage(<messageBody>);
WKWebViewConfiguration作為 WK 的配置類,其中有一個(gè)屬性為
@property (nonatomic, strong) WKUserContentController *userContentController;
是WKUserContentController的一個(gè)實(shí)例,WKUserContentController有一個(gè)對(duì)象方法為:
/*! @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;
從蘋果給出的注釋來(lái)看,通過(guò)該方法能夠添加一個(gè)腳本消息的處理器,即(id <WKScriptMessageHandler>)scriptMessageHandler,另外還能發(fā)現(xiàn),添加腳本處理器后,需要在 JS 中添加window.webkit.messageHandlers.<name>.postMessage(<messageBody>)才能起作用。
demo:
// 創(chuàng)建并配置 WKWebView 的相關(guān)參數(shù)
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// self 指代的對(duì)象需要遵守 WKScriptMessageHandler 協(xié)議
[userContent addScriptMessageHandler:self name:@"test"];
config.userContentController = userContent;
在頁(yè)面上的 JS 執(zhí)行window.webkit.messageHandlers.<name>.postMessage(<messageBody>)時(shí),被添加的ScriptMessageHandler就會(huì)執(zhí)行實(shí)現(xiàn)的WKScriptMessageHandler協(xié)議的方法,例如:
#pragma mark----- WKScriptMessageHandler -----
/**
* JS 調(diào)用 OC 時(shí) webview 會(huì)調(diào)用此方法
*
* @param userContentController webview 中配置的 userContentController 信息
* @param message js 執(zhí)行傳遞的消息
*/
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message
{
NSLog(@"%@", message);
}
在代理方法中實(shí)現(xiàn)相應(yīng)的 Native 代碼,即完成了 JS 調(diào)用 Native 的過(guò)程。
二、JavaScriptCore
OS X Mavericks 和 iOS 7 引入了 JavaScriptCore 庫(kù),把 WebKit 的 JavaScript 引擎用 Objective-C 封裝,提供了簡(jiǎn)單,快速以及安全的方式接入 JavaScript。
JavaScriptCore中類及協(xié)議
- JSContext:JavaScript 運(yùn)行的上下文環(huán)境
- JSValue:JavaScript 和 Objective-C 數(shù)據(jù)和方法的橋梁
- JSExport:這是一個(gè)協(xié)議,如果采用協(xié)議的方法交互,自己定義的協(xié)議必須遵守此協(xié)議
- JSManagedValue:管理數(shù)據(jù)和方法的類
- JSVirtualMachine:處理線程相關(guān),使用較少
JavaScript 調(diào)用 Native
使用 JavaScriptCore 進(jìn)行 JS 和 Native 的交互,無(wú)論想要實(shí)現(xiàn)什么樣的效果都需要獲得一個(gè)有效的 JSContext 實(shí)例,即一個(gè)有效的 JS 運(yùn)行的上下文(這一步驟以下不再重復(fù)提及)。
- 獲得當(dāng)前的 JSContext:
可以在頁(yè)面加載完畢后,采用 KVC 的方式從webView 中獲得,如下:
JSContext* jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
- 將想要被暴露給 JS 的方法抽象成為一個(gè)協(xié)議(protocol),該協(xié)議需要遵守
JSExport協(xié)議:
@protocol JSObjcDelegate <JSExport>
- (void)callCamera;
- (NSString*)share:(NSString*)shareString;
@end
- 將要暴露給 JS 的對(duì)象的類需要遵守自定義的協(xié)議,如上:
JSObjcDelegate; - 將 OC 對(duì)象橋接到 JS 環(huán)境中,并設(shè)置異常處理
// 將本對(duì)象與 JS 中的 DZBridge 對(duì)象橋接在一起,在 JS 中 DZBridge 代表本對(duì)象
[self.jsContext setObject:self forKeyedSubscript:@"DZBridge"];
self.jsContext.exceptionHandler = ^(JSContext* context, JSValue* exceptionValue) {
context.exception = exceptionValue;
NSLog(@"異常信息:%@", exceptionValue);
};
- 在 JS 中通過(guò) DZBridge 調(diào)用本對(duì)象暴露出的方法:
var callShare = function() {
var shareInfo = JSON.stringify({"title": "標(biāo)題", "desc": "內(nèi)容", "shareUrl": "http://m.itdecent.cn"});
var str = DZBridge.share(shareInfo);
alert(str);
}
Native 調(diào)用 JavaScript
- 第一種方式同 UIWebView 中類似,都是直接執(zhí)行 JS 字符串,通過(guò) JSContext 執(zhí)行 JS 代碼:
[self.jsContext evaluateScript:@"alert(\"執(zhí)行 JS\")"];
- 另一種方式適用于執(zhí)行 web 頁(yè)面上已有的方法,通過(guò) JSValue 來(lái)調(diào)用 JS 中的方法,JSValue 是 JavaScript 中值得一個(gè)引用,他可能包裝著一個(gè) JavaScript 的方法,通過(guò)
callWithArguments:方法進(jìn)行調(diào)用,例如:
JSValue* picCallback = self.jsContext[@"picCallback"];
[picCallback callWithArguments:@[ @"photos" ]];