前言
在 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": "http://www.lxweimin.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" ]];