Objective-C與JavaScript交互的那些事

t144in93 8年前發布 | 24K 次閱讀 Objective-C開發 JavaScript Objective-C

來自: http://ios.jobbole.com/83945/

最近公司的運營瞎搞了個活動,其活動要服務端提供數據支持, web前端 在微信公眾賬號內作為主要的運營陣地,而 iOS 、 Android 要提供相應的入口及頁面進行配合。一個活動,動用了各個端的程序猿。而在這里面技術方面主要就是涉及到 web端 和服務端的交互, web前端 和 iOS 、 Android 的交互。本人作為一個 iOS 開發者,今天就聊聊 web 、 iOS 、 Android 三端的交互,其實在說明白一點就是方法的互相調用而已。這里主要講解 iOS 。 Android 會稍微提一下,僅作參考。

此篇文章的邏輯圖

圖0-0 此篇文章的邏輯圖

概述

iOS原生應用和web頁面的交互大致上有這幾種方法 iOS7之后的JavaScriptCore 、 攔截協議 、 第三方框架WebViewJavaScriptBridge 、 iOS8之后的WKWebView 在這里主要講解 JavaScriptCore 和 攔截協議 這兩種辦法。 WebViewJavaScriptBridge 是基于 攔截協議 進行的封裝。學習成本相對 JavaScriptCore 較高,使用也不如 JavaScriptCore 方便本文不做敘述。 WKWebView 是iOS8之后推出的,還沒有成為主流使用,所以本篇文章也不做詳細敘述。

Objective-C執行JavaScript代碼

相關方法

Objective-C

// UIWebView的方法
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

// JavaScriptCore中JSContext的方法
- (JSValue *)evaluateScript:(NSString *)script;
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL
// UIWebView的方法
- (nullableNSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
 
// JavaScriptCore中JSContext的方法
- (JSValue *)evaluateScript:(NSString *)script;
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL

相關應用

用這些方法去執行大段的 JavaScript 代碼是沒什么必要的,但是有些小場景用起來還是比較順手和實用的,列舉兩個例子作為參考:

Objective-C

// 獲取當前頁面的title
NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];

// 獲取當前頁面的url
NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];
// 獲取當前頁面的title
NSString *title = [webviewstringByEvaluatingJavaScriptFromString:@"document.title"];
 
// 獲取當前頁面的url
NSString *url = [webviewstringByEvaluatingJavaScriptFromString:@"document.location.href"];

JavaScriptCore

iOS7 之后蘋果推出了 JavaScriptCore 這個框架,從而讓web頁面和本地原生應用交互起來非常方便,而且使用此框架可以做到 Android 那邊和 iOS 相對統一, web前端 寫一套代碼就可以適配客戶端的兩個平臺,從而減少了web前端的工作量。

web前端

在三端交互中, web前端 要強勢一些,一切傳值、方法命名都按 web前端 開發人員來定義,讓另外兩端去做適配。在這里以調用攝像頭和分享為例來詳細講解,測試網頁代碼取名為 test.html ,其代碼內容如下:

test.html代碼內容

Objective-C

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
</head>
<body>
    <div style="margin-top: 100px">
        <h1>Objective-C和JavaScript交互的那些事</h1>
        <input type="button" value="CallCamera" onclick="Toyun.callCamera()">
    </div>       

    <div>
        <input type="button" value="Share" onclick="callShare()">
    </div>

<script>
    var callShare = function() {
        var shareInfo = JSON.stringify({"title": "標題", "desc": "內容", "shareUrl": "http://www.jianshu.com/p/f896d73c670a",
        "shareIco":"http://upload-images.jianshu.io/upload_images/1192353-fd26211d54aea8a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"});
        Toyun.share(shareInfo);
    }

    var picCallback = function(photos) {
        alert(photos);
    }

    var shareCallback = function(){
        alert('success');
    }
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<headlang="en">
    <metacharset="UTF-8">
</head>
<body>
    <divstyle="margin-top: 100px">
        <h1>Objective-C和JavaScript交互的那些事</h1>
        <inputtype="button" value="CallCamera" onclick="Toyun.callCamera()">
    </div>      
 
    <div>
        <inputtype="button" value="Share" onclick="callShare()">
    </div>
 
<script>
    var callShare = function() {
        var shareInfo = JSON.stringify({"title": "標題", "desc": "內容", "shareUrl": "http://www.jianshu.com/p/f896d73c670a",
        "shareIco":"http://upload-images.jianshu.io/upload_images/1192353-fd26211d54aea8a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"});
        Toyun.share(shareInfo);
    }
 
    var picCallback = function(photos) {
        alert(photos);
    }
 
    var shareCallback = function(){
        alert('success');
    }
</script>
</body>
</html>
test.html代碼解釋

可能有些同學對 web前端 的一些知識不太熟悉,稍微對這段代碼做下解釋,先說 Toyun 是 iOS 和 Android 這兩邊在本地要注入的一個對象【參考下面iOS的代碼更容易明白】,充當原生應用和web頁面之間的一個橋梁。頁面上定義了兩個按鈕名字分別為 CallCamera 和 Share 。點擊 CallCamera 會通過 Toyun 這個橋梁調用本地應用的方法 - (void)callCamera ,沒有傳參;而點擊 Share 會先調用本文件中的 JavaScript 方法 callShare 這里將要分享的內容格式轉成 JSON字符串 格式(這樣做是為了適配 Android , iOS 可以直接接受 JSON對象 )然后再通過 Toyun 這個橋梁去調用原生應用的 - (void)share:(NSString *)shareInfo 方法這個是有傳參的,參數為 shareInfo 。而下面的兩個方法為原生方法調用后的回調方法,其中 picCallback 為獲取圖片成功的回調方法,并且傳回拿到的圖片 photos ; shareCallback 為分享成功的回調方法。

iOS

iOS 這邊根據前端定義的方法名來寫代碼,但是有些時候 web前端 會讓我們定義,但是我們定義好之后他又要修改,這時候就會很煩啊。所以碰到三端交互的時候最好就是讓 web前端 去定義方法名, iOS 和 Android 根據 web前端 定義好的去寫代碼。 JavaScriptCore 中 web頁面 調用原生應用的方法可以用 Delegate 或 Block 兩種方法,此文以按 Delegate 講解。

JavaScriptCore中類及協議:
  • JSContext:給 JavaScript 提供運行的上下文環境
  • JSValue: JavaScript 和 Objective-C 數據和方法的橋梁
  • JSManagedValue:管理數據和方法的類
  • JSVirtualMachine:處理線程相關,使用較少
  • JSExport:這是一個協議,如果采用協議的方法交互,自己定義的協議必須遵守此協議
ViewController中的代碼

Objective-C

#import "ViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>

@protocol JSObjcDelegate <JSExport>

- (void)callCamera;
- (void)share:(NSString *)shareString;

@end

@interface ViewController () <UIWebViewDelegate, JSObjcDelegate>

@property (nonatomic, strong) JSContext *jsContext;
@property (weak, nonatomic) IBOutlet UIWebView *webView;

@end

@implementation ViewController

#pragma mark - Life Circle

- (void)viewDidLoad {
    [super viewDidLoad];

    NSURL *url = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"html"];
    [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:url]];

}

#pragma mark - UIWebViewDelegate

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    self.jsContext[@"Toyun"] = self;
    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
        context.exception = exceptionValue;
        NSLog(@"異常信息:%@", exceptionValue);
    };
}

#pragma mark - JSObjcDelegate

- (void)callCamera {
    NSLog(@"callCamera");
    // 獲取到照片之后在回調js的方法picCallback把圖片傳出去
    JSValue *picCallback = self.jsContext[@"picCallback"];
    [picCallback callWithArguments:@[@"photos"]];
}

- (void)share:(NSString *)shareString {
    NSLog(@"share:%@", shareString);
    // 分享成功回調js的方法shareCallback
    JSValue *shareCallback = self.jsContext[@"shareCallback"];
    [shareCallback callWithArguments:nil];
}

@end
#import "ViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>
 
@protocol JSObjcDelegate <JSExport>
 
- (void)callCamera;
- (void)share:(NSString *)shareString;
 
@end
 
@interface ViewController () <UIWebViewDelegate, JSObjcDelegate>
 
@property (nonatomic, strong) JSContext *jsContext;
@property (weak, nonatomic) IBOutlet UIWebView *webView;
 
@end
 
@implementation ViewController
 
#pragma mark - Life Circle
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    NSURL *url = [[NSBundle mainBundle]URLForResource:@"test"withExtension:@"html"];
    [self.webViewloadRequest:[[NSURLRequest alloc]initWithURL:url]];
 
}
 
#pragma mark - UIWebViewDelegate
 
- (void)webViewDidFinishLoad:(UIWebView *)webView {
    self.jsContext = [webViewvalueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    self.jsContext[@"Toyun"] = self;
    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
        context.exception = exceptionValue;
        NSLog(@"異常信息:%@", exceptionValue);
    };
}
 
#pragma mark - JSObjcDelegate
 
- (void)callCamera {
    NSLog(@"callCamera");
    // 獲取到照片之后在回調js的方法picCallback把圖片傳出去
    JSValue *picCallback = self.jsContext[@"picCallback"];
    [picCallbackcallWithArguments:@[@"photos"]];
}
 
- (void)share:(NSString *)shareString {
    NSLog(@"share:%@", shareString);
    // 分享成功回調js的方法shareCallback
    JSValue *shareCallback = self.jsContext[@"shareCallback"];
    [shareCallbackcallWithArguments:nil];
}
 
@end
ViewController中的代碼解釋

自定義 JSObjcDelegate 協議,而且此協議必須遵守 JSExport 這個協議,自定義協議中的方法就是暴露給 web頁面 的方法。在 webView 加載完畢的時候獲取 JavaScript 運行的上下文環境,然后再注入橋梁對象名為 Toyun ,承載的對象為 self 即為此控制器,控制器遵守此自定義協議實現協議中對應的方法。在 JavaStript 調用完本地應用的方法做完相對應的事情之后,又回調了 JavaStript 中對應的方法,從而實現了 web頁面 和 本地應用 之間的通訊。

JavaScriptCore使用注意

JavaStript 調用本地方法是在 子線程 中執行的,這里要根據實際情況考慮線程之間的切換,而在回調 JavaScript 方法的時候最好是在剛開始調用此方法的線程中去執行那段 JavaStript 方法的代碼,我在實際運用中開始沒注意,就被坑慘了啊。什么,說的太繞,看下面的代碼解釋:

Objective-C

//  假設此方法是在子線程中執行的,線程名sub-thread
- (void)callCamera {     
    // 這句假設要在主線程中執行,線程名main-thread
    NSLog(@"callCamera");  

    // 下面這兩句代碼最好還是要在子線程sub-thread中執行啊
    JSValue *picCallback = self.jsContext[@"picCallback"];
    [picCallback callWithArguments:@[@"photos"]];
}
//  假設此方法是在子線程中執行的,線程名sub-thread
- (void)callCamera {    
    // 這句假設要在主線程中執行,線程名main-thread
    NSLog(@"callCamera");  
 
    // 下面這兩句代碼最好還是要在子線程sub-thread中執行啊
    JSValue *picCallback = self.jsContext[@"picCallback"];
    [picCallbackcallWithArguments:@[@"photos"]];
}
運行效果

運行效果如圖3-1所示

圖3-1 運行效果

攔截協議

攔截協議這個適合一些比較簡單的一些情況,不需要引入什么框架,只需要 web前端 配合一下就好。但是在具體調用哪一個方法上,以及在傳值的時候可能會有些不方便,而且調用完后無法在回調 JavaScript 的方法。

web前端

test.html中的代碼

Objective-C

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
</head>
<body>
    <div>
        <input type="button" value="CallCamera" onclick="callCamera()">
    </div>

<script>
    function callCamera() {
        window.location.href = 'toyun://callCamera';
    }
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<headlang="en">
    <metacharset="UTF-8">
</head>
<body>
    <div>
        <inputtype="button" value="CallCamera" onclick="callCamera()">
    </div>
 
<script>
    function callCamera() {
        window.location.href = 'toyun://callCamera';
    }
</script>
</body>
</html>
test.html中的代碼解釋

這段代碼相比上面的那段測試代碼是很簡單的,同樣有一個按鈕,名字為 CallCamera 點擊之后調用自己的 callCamera 方法, window.location.href 這里是改變主窗口的指向從而馬上發出一個鏈接為 toyun://callCamera 請求,而想要傳給原生應用的參數也可已包含到此請求中,而在iOS方法中我們要攔截這個請求,根據請求內容去判斷 JavaStript 想要做的事情,從而實現 web頁面 和 本地應用 之間的交互。

iOS

iOS對應的代碼

Objective-C

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSString *url = request.URL.absoluteString;
    if ([url rangeOfString:@"toyun://"].location != NSNotFound) { 
        // url的協議頭是toyun
        NSLog(@"callCamera");
        return NO;
    }
    return YES;
}
- (BOOL)webView:(UIWebView *)webViewshouldStartLoadWithRequest:(NSURLRequest *)requestnavigationType:(UIWebViewNavigationType)navigationType
{
    NSString *url = request.URL.absoluteString;
    if ([urlrangeOfString:@"toyun://"].location != NSNotFound) { 
        // url的協議頭是toyun
        NSLog(@"callCamera");
        return NO;
    }
    return YES;
}
iOS對應的代碼的解釋

在 webView 的代理方法中去攔截自定義的協議 Toyun:// 如果是此協議則據此判斷 JavaStript 想要做的事情,調用原生應用的方法,這些都是提前約定好的,同時阻止此鏈接的跳轉。

總結

隨著手機硬件的配置越來越強大和 HTML5 的興起,一個 App 完全可以由 web頁面 來寫。現在已經有部分應用這么干了,我是遇見過的,如 古詩文網 。盡管比較少但是 web頁面 和 本地應用 的交互不論是 iOS 還是 Android 都是會有遇到的。 iOS 我還是比較推薦 JavaScriptCore ,這樣三端可以相對統一起來,寫的時候都比較簡單。隨著時間的推移 iOS8 推出的 WKWebView 會逐漸成為主流,這個的功能更強大。 攔截協議 也只能說用到比較簡單的一些情況吧,復雜的情況處理相互之間參數的傳遞還是比較麻煩的,而且這個不能回調 JavaScript 的方法,確實喜歡攔截協議的同學可以研究 WebViewJavaScriptBridge 這個第三方庫。對于 Android 本人也就是略知皮毛而已,就不班門弄斧了,對于一些 Android開發者 來說,可以看地第一段的 test.html 這個頁面的寫法完全是可以適配 Android 的。

參考

 本文由用戶 t144in93 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!