js ios 交互

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

此篇文章的邏輯圖前端

1192353-fd26211d54aea8a9.png

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

概述git

iOS原生應用和web頁面的交互大體上有這幾種方法iOS7以後的JavaScriptCore、攔截協議、第三方框架WebViewJavaScriptBridge、iOS8以後的WKWebView在這裏主要講解JavaScriptCore和攔截協議這兩種辦法。WebViewJavaScriptBridge是基於攔截協議進行的封裝。學習成本相對JavaScriptCore較高,使用也不如JavaScriptCore方便本文不作敘述。WKWebView是iOS8以後推出的,尚未成爲主流使用,因此本篇文章也不作詳細敘述。github

Objective-C執行JavaScript代碼web

相關方法緩存

 

1微信

2app

3框架

4

5

6

// UIWebView的方法

- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

 

// JavaScriptCore中JSContext的方法

- (JSValue *)evaluateScript:(NSString *)script;

- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL

相關應用

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

 

1

2

3

4

5

// 獲取當前頁面的title

NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];

 

// 獲取當前頁面的url

NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];

JavaScriptCore

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

web前端

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

test.html代碼內容(因識別問題,用方括號替換了代碼中的尖括號)

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

[!DOCTYPE html]

[html]

[head]

    [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]

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中的代碼

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

#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

ViewController中的代碼解釋

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

JavaScriptCore使用注意

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

 

1

2

3

4

5

6

7

8

9

//  假設此方法是在子線程中執行的,線程名sub-thread

- (void)callCamera {     

    // 這句假設要在主線程中執行,線程名main-thread

    NSLog(@"callCamera");

       

    // 下面這兩句代碼最好仍是要在子線程sub-thread中執行啊

    JSValue *picCallback = self.jsContext[@"picCallback"];

    [picCallback callWithArguments:@[@"photos"]];

}

運行效果

運行效果如圖3-1所示

1192353-c7968c7ff587cf91.jpg

圖3-1 運行效果

攔截協議

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

web前端

test.html中的代碼(因識別問題,用方括號替換了代碼中的尖括號)

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

[!DOCTYPE html]

[html]

[head]

    [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]

test.html中的代碼解釋

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

iOS

iOS對應的代碼

1

2

3

4

5

6

7

8

9

10

- (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;

}

iOS對應的代碼的解釋

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

總結

隨着手機硬件的配置愈來愈強大和HTML5的興起,一個App徹底能夠由web頁面來寫。如今已經有部分應用這麼幹了,我是碰見過的,如古詩文網。儘管比較少可是web頁面和本地應用的交互不管是iOS仍是Android都是會有遇到的。iOS我仍是比較推薦JavaScriptCore,這樣三端能夠相對統一塊兒來,寫的時候都比較簡單。隨着時間的推移iOS8推出的WKWebView會逐漸成爲主流,這個的功能更強大。攔截協議也只能說用到比較簡單的一些狀況吧,複雜的狀況處理相互之間參數的傳遞仍是比較麻煩的,並且這個不能回調JavaScript的方法,確實喜歡攔截協議的同窗能夠研究WebViewJavaScriptBridge這個第三方庫。對於Android本人也就是略知皮毛而已,就不班門弄斧了,對於一些Android開發者來講,能夠看地第一段的test.html這個頁面的寫法徹底是能夠適配Android的。

 

 

 

 

WKWebView使用及注意點(keng)

144 做者 TIME_for 關注

2016.11.19 16:31* 字數 2498 閱讀 4996評論 54喜歡 97

iOS8以後,蘋果推出了WebKit這個框架,用來替換原有的UIWebView,新的控件優勢多多,不一一敘述。因爲一直在適配iOS7,就沒有去替換,如今仍掉了iOS7,覺得很簡單的就替換過來了,然而在替換的過程當中,卻遇到了不少坑。還有一點就是原來寫過一篇文章 Objective-C與JavaScript交互的那些事覺得年代久遠的UIWebView已經做古,可這篇文章如今依然有必定的閱讀量。因此在決定在續一篇此文,以引導你們轉向WKWebView,並指出本身踩過的坑,讓你們少走彎路。

此篇文章的邏輯圖

此篇文章的邏輯圖

WKWebView使用

WKWebView簡單介紹

使用及注意點

WKWebView只能用代碼建立,並且自身就支持了右滑返回手勢allowsBackForwardNavigationGestures和加載進度estimatedProgress等一些UIWebView不具有卻很是好用的屬性。在建立的時候,指定初始化方法中要求傳入一個WKWebViewConfiguration對象,通常咱們使用默認配置就好,可是有些地方是要根據本身的狀況去作更改。好比,配置中的allowsInlineMediaPlayback這個屬性,默認爲NO,若是不作更改,網頁中內嵌的視頻就沒法正常播放。

更改User-Agent

有時咱們須要在User-Agent添加一些額外的信息,這時就要更改默認的User-Agent在使用UIWebView的時候,可用以下代碼(在使用UIWebView以前執行)全局更改User-Agent

// 獲取默認User-Agent
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];

// 給User-Agent添加額外的信息
NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];

// 設置global User-Agent
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];

以上代碼是全局更改User-Agent,也就是說,App內全部的Web請求的User-Agent都被修改。替換爲WKWebView後更改全局User-Agent能夠繼續使用上面的一段代碼,或者改成用WKWebView獲取默認的User-Agent,代碼以下:

self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero];

// 獲取默認User-Agent
[self.wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
    NSString *oldAgent = result;

    // 給User-Agent添加額外的信息
    NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];

    // 設置global User-Agent
    NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newAgent, @"UserAgent", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
}];

對比發現,這兩種方法並無本質的區別,一點小區別在於一個是用UIWebView獲取的默認User-Agent,一個是用WKWebView獲取的默認User-Agent。上面方法的缺點也是很明顯的,就是App內全部Web請求的User-Agent所有被修改。

iOS9WKWebView提供了一個很是便捷的屬性去更改User-Agent,就是customUserAgent屬性。這樣使用起來不只方便,也不會全局更改User-Agent,惋惜的是iOS9纔有,若是適配iOS8,仍是要使用上面的方法。

WKWebView的相關的代理方法

WKWebView的相關的代理方法分別在WKNavigationDelegateWKUIDelegate以及WKScriptMessageHandler這個與JavaScript交互相關的代理方法。

  • WKNavigationDelegate: 此代理方法中除了原有的UIWebView的四個代理方法,還增長了其餘的一些方法,具體可參考我下面給出的Demo
  • WKUIDelegate: 此代理方法在使用中最好實現,不然遇到網頁alert的時候,若是此代理方法沒有實現,則不會出現彈框提示。
  • WKScriptMessageHandler: 此代理方法就是和JavaScript交互相關,具體介紹參考下面的專門講解。

WKWebView使用過程當中的坑

WKWebView下面添加自定義View

由於咱們有個需求是在網頁下面在添加一個View,用來展現此連接內容的相關評論。在使用UIWebView的時候,作法很是簡單粗暴,在UIWebViewScrollView後面添加一個自定義View,而後根據View的高度,在改變一下scrollViewcontentSize屬性。覺得WKWebView也能夠這樣簡單粗暴的去搞一下,結果卻並非這樣。

首先改變WKWebViewscrollViewcontentSize屬性,系統會在下一次幀率刷新的時候,再給你改變回原有的,這樣這條路就行不通了。我立刻想到了另外一個辦法,改變scrollViewcontentInset這個系統倒不會在變化回原來的,自覺得完事大吉。後來過了兩天,發現有些頁面的部分區域的點擊事件沒法響應,百思不得其解,最後想到多是設置的contentInset對其有了影響,事實上正是如此。查來查去,最後找到了一個解決辦法是,就是當頁面加載完成時,在網頁下面拼一個空白的div,高度就是你添加的View的高度,讓網頁多出一個空白區域,自定義的View就添加在這個空白的區域上面。這樣就完美解決了此問題。具體可參考Demo所寫,核心代碼以下:

self.addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, addViewHeight)];
self.addView.backgroundColor = [UIColor redColor];
[self.webView.scrollView addSubview:self.addView];

NSString *js = [NSString stringWithFormat:@"\
                        var appendDiv = document.getElementById(\"AppAppendDIV\");\
                        if (appendDiv) {\
                        appendDiv.style.height = %@+\"px\";\
                        } else {\
                        var appendDiv = document.createElement(\"div\");\
                        appendDiv.setAttribute(\"id\",\"AppAppendDIV\");\
                        appendDiv.style.width=%@+\"px\";\
                        appendDiv.style.height=%@+\"px\";\
                        document.body.appendChild(appendDiv);\
                        }\
                        ", @(addViewHeight), @(self.webView.scrollView.contentSize.width), @(addViewHeight)];

[self.webView evaluateJavaScript:js completionHandler:nil];

WKWebView加載HTTPS的連接

HTTPS已經愈來愈被重視,前面我也寫過一系列的HTTPS的相關文章HTTPS從原理到應用(四):iOS中HTTPS實際使用當加載一些HTTPS的頁面的時候,若是此網站使用的根證書已經內置到了手機中這些HTTPS的連接能夠正常的經過驗證並正常加載。可是若是使用的證書(通常爲自建證書)的根證書並無內置到手機中,這時是連接是沒法正常加載的,必需要作一個權限認證。開始在UIWebView的時候,是把請求存儲下來而後使用NSURLConnection去從新發起請求,而後走NSURLConnection的權限認證通道,認證經過後,在使用UIWebView去加載這個請求。

WKWebView中,WKNavigationDelegate中提供了一個權限認證的代理方法,這是權限認證更爲便捷。代理方法以下:

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([challenge previousFailureCount] == 0) {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
    } else {
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

這個方法比原來UIWebView的認證簡單的多。可是使用中卻發現了一個很蛋疼的問題,iOS8系統下,自建證書的HTTPS連接,不調用此代理方法。查來查去,原來是一個bug,在iOS9中已經修復,這明顯就是無論iOS8的狀況了,並且此方法也沒有標記在iOS9中使用,這點讓我感到有點失望。這樣我就又想到了換回原來UIWebView的權限認證方式,可是試來試去,發現也不能使用了。因此關於自建證書的HTTPS連接在iOS8下面使用WKWebView加載,我沒有找到很好的辦法去解決此問題。這樣我不得已有些連接換回了HTTP,或者在iOS8下面在換回UIWebView。若是你有解決辦法,也歡迎私信我,感激涕零。

WKWebView加載POST請求

很是感謝@e231e1ff5f8b的指出,原來POST請求這兒還有一個坑。本身項目中並無這塊需求,也就沒有發現。加載POST請求的時候,會丟失HTTPBody。解決辦法是在網頁上開一個JavaScript方法,在請求POST的時候去調用JavaScript這個方法,從而完成POST請求。調用JavaScript方法參考下面交互這一章節。

WKWebView和JavaScript交互

WKWebViewJavaScript交互,在WKUserContentController.h這個頭文件中- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;這個方法的註釋中已經明確給出了交互辦法。使用起來卻是很是的簡單。建立WKWebView的時候添加交互對象,並讓交互對象實現WKScriptMessageHandler中的惟一的一個代理方法。具體的方式參考Demo中的使用。

// 添加交互對象
[config.userContentController addScriptMessageHandler:(id)self.ocjsHelper name:@"timefor"];

/** 此點後來更新,若是不移除交互對象,則致使交互對象內存常駐(2016.12.17) */
// VC銷燬時,移除交互對象
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"timefor"];

// 代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

JavaScript調用Objective-C的時候,使用window.webkit.messageHandlers.timefor.postMessage({code: '0001', functionName: 'getdevideId'}); Objective-C自動對交互參數包裝成了WKScriptMessage對象,其屬性body則爲傳送過來的參數,name爲添加交互對象的時候設置的名字,以此名字能夠過濾掉不屬於本身的交互方法。其中body能夠爲NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull。

Objective-C在回調JavaScript的時候,不能像我原來在 Objective-C與JavaScript交互的那些事這篇文章中寫的那樣,JavaScript傳過來一個匿名函數,Objective-C這邊直接調用一下就完事。WKWebView沒有辦法傳過來一個匿名函數,因此回調方式,要麼執行一段JavaScript代碼,或者就是調用JavaScript那邊的一個全局函數。通常是採用後者,至於Web端雖然說暴露了一個全局函數,一樣能夠把這一點代碼處理的很優雅。Objective-C傳給JavaScript的參數,能夠爲Number, String, and Object。參考以下:

// 數字
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", number];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 字符串
NSString *js = [NSString stringWithFormat:@"globalCallback(\'%@\')", string];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 對象
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", @{@"name" : @"timefor"}];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 帶返回值的JS函數
[self.webView evaluateJavaScript:@"globalCallback()" completionHandler:^(id result, NSError * _Nullable error) {
    // 接受返回的參數,result中
}];

總結

此文主要介紹了WKWebView使用中的注意點,通常也都是經常使用的,還有緩存等一些不是太經常使用的就沒有具體介紹。若是在其餘方面遇到問題,也歡迎你私信我共同探討進步。WKWebView確實比UIWebView有些地方好用很多,可是一些bug至今也沒解決,權限挑戰是在iOS9解決的,POST請求則至今沒有解決,而改變contentInset致使的點擊事件不許確,一樣是沒有解決。這些問題讓開發者使用起來,有諸多不便啊。
此文的Demo地址:WKWebViewDemo 若是此文對你有所幫助,請給個star吧。

相關文章
相關標籤/搜索