論 iOS 開發中 JS 與 Native 的交互方式

前言

在 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 有著比較大的不同,首先需要介紹幾個類(/協議/屬性):
  1. WKWebViewConfiguration:是 WKWebView 初始化時的配置類,里面存放著初始化 WK 的一系列屬性;
  2. WKUserContentController:為 JS 提供了一個發送消息的通道并且可以向頁面注入 JS 的類;
  3. 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" ]];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容