最近公司的運營瞎搞了個活動,其活動要服務端提供數據支持,web前端在微信公衆帳號內做爲主要的運營陣地,而iOS、Android要提供相應的入口及頁面進行配合。一個活動,動用了各個端的程序猿。而在這裏面技術方面主要就是涉及到web端和服務端的交互,web前端和iOS、Android的交互。本人做爲一個iOS開發者,今天就聊聊web、iOS、Android三端的交互,其實在說明白一點就是方法的互相調用而已。這裏主要講解iOS。Android會稍微提一下,僅做參考。html
此篇文章的邏輯圖前端
圖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 |
|
相關應用
用這些方法去執行大段的JavaScript代碼是沒什麼必要的,可是有些小場景用起來仍是比較順手和實用的,列舉兩個例子做爲參考:
1 2 3 4 5 |
|
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 |
|
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 |
|
ViewController中的代碼解釋
自定義JSObjcDelegate協議,並且此協議必須遵照JSExport這個協議,自定義協議中的方法就是暴露給web頁面的方法。在webView加載完畢的時候獲取JavaScript運行的上下文環境,而後再注入橋樑對象名爲Toyun,承載的對象爲self即爲此控制器,控制器遵照此自定義協議實現協議中對應的方法。在JavaStript調用完本地應用的方法作完相對應的事情以後,又回調了JavaStript中對應的方法,從而實現了web頁面和本地應用之間的通信。
JavaScriptCore使用注意
JavaStript調用本地方法是在子線程中執行的,這裏要根據實際狀況考慮線程之間的切換,而在回調JavaScript方法的時候最好是在剛開始調用此方法的線程中去執行那段JavaStript方法的代碼,我在實際運用中開始沒注意,就被坑慘了啊。什麼,說的太繞,看下面的代碼解釋:
1 2 3 4 5 6 7 8 9 |
|
運行效果
運行效果如圖3-1所示
圖3-1 運行效果
攔截協議
攔截協議這個適合一些比較簡單的一些狀況,不須要引入什麼框架,只須要web前端配合一下就好。可是在具體調用哪個方法上,以及在傳值的時候可能會有些不方便,並且調用完後沒法在回調JavaScript的方法。
web前端
test.html中的代碼(因識別問題,用方括號替換了代碼中的尖括號)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
test.html中的代碼解釋
這段代碼相比上面的那段測試代碼是很簡單的,一樣有一個按鈕,名字爲CallCamera點擊以後調用本身的callCamera方法,window.location.href這裏是改變主窗口的指向從而立刻發出一個連接爲Toyun://callCamera請求,而想要傳給原生應用的參數也可已包含到此請求中,而在iOS方法中咱們要攔截這個請求,根據請求內容去判斷JavaStript想要作的事情,從而實現web頁面和本地應用之間的交互。
iOS
iOS對應的代碼
1 2 3 4 5 6 7 8 9 10 |
|
iOS對應的代碼的解釋
在webView的代理方法中去攔截自定義的協議Toyun://若是是此協議則據此判斷JavaStript想要作的事情,調用原生應用的方法,這些都是提早約定好的,同時阻止此連接的跳轉。
總結
隨着手機硬件的配置愈來愈強大和HTML5的興起,一個App徹底能夠由web頁面來寫。如今已經有部分應用這麼幹了,我是碰見過的,如古詩文網。儘管比較少可是web頁面和本地應用的交互不管是iOS仍是Android都是會有遇到的。iOS我仍是比較推薦JavaScriptCore,這樣三端能夠相對統一塊兒來,寫的時候都比較簡單。隨着時間的推移iOS8推出的WKWebView會逐漸成爲主流,這個的功能更強大。攔截協議也只能說用到比較簡單的一些狀況吧,複雜的狀況處理相互之間參數的傳遞仍是比較麻煩的,並且這個不能回調JavaScript的方法,確實喜歡攔截協議的同窗能夠研究WebViewJavaScriptBridge這個第三方庫。對於Android本人也就是略知皮毛而已,就不班門弄斧了,對於一些Android開發者來講,能夠看地第一段的test.html這個頁面的寫法徹底是能夠適配Android的。
做者 TIME_for 關注
2016.11.19 16:31* 字數 2498 閱讀 4996評論 54喜歡 97
iOS8
以後,蘋果推出了WebKit
這個框架,用來替換原有的UIWebView
,新的控件優勢多多,不一一敘述。因爲一直在適配iOS7
,就沒有去替換,如今仍掉了iOS7
,覺得很簡單的就替換過來了,然而在替換的過程當中,卻遇到了不少坑。還有一點就是原來寫過一篇文章 Objective-C與JavaScript交互的那些事覺得年代久遠的UIWebView
已經做古,可這篇文章如今依然有必定的閱讀量。因此在決定在續一篇此文,以引導你們轉向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
所有被修改。
在iOS9
,WKWebView
提供了一個很是便捷的屬性去更改User-Agent
,就是customUserAgent
屬性。這樣使用起來不只方便,也不會全局更改User-Agent
,惋惜的是iOS9
纔有,若是適配iOS8
,仍是要使用上面的方法。
WKWebView的相關的代理方法
WKWebView
的相關的代理方法分別在WKNavigationDelegate
和WKUIDelegate
以及WKScriptMessageHandler
這個與JavaScript
交互相關的代理方法。
WKNavigationDelegate
: 此代理方法中除了原有的UIWebView
的四個代理方法,還增長了其餘的一些方法,具體可參考我下面給出的Demo
。WKUIDelegate
: 此代理方法在使用中最好實現,不然遇到網頁alert
的時候,若是此代理方法沒有實現,則不會出現彈框提示。WKScriptMessageHandler
: 此代理方法就是和JavaScript
交互相關,具體介紹參考下面的專門講解。WKWebView下面添加自定義View
由於咱們有個需求是在網頁下面在添加一個View
,用來展現此連接內容的相關評論。在使用UIWebView
的時候,作法很是簡單粗暴,在UIWebView
的ScrollView
後面添加一個自定義View
,而後根據View
的高度,在改變一下scrollView
的contentSize
屬性。覺得WKWebView
也能夠這樣簡單粗暴的去搞一下,結果卻並非這樣。
首先改變WKWebView
的scrollView
的contentSize
屬性,系統會在下一次幀率刷新的時候,再給你改變回原有的,這樣這條路就行不通了。我立刻想到了另外一個辦法,改變scrollView
的contentInset
這個系統倒不會在變化回原來的,自覺得完事大吉。後來過了兩天,發現有些頁面的部分區域的點擊事件沒法響應,百思不得其解,最後想到多是設置的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
交互,在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
吧。