因爲項目中Cordova相關功能一直是同事在負責,因此也沒有仔細的去探究Cordova究竟是怎麼使用的,又是如何實現JS 與 OC 的交互。因此我基本上是從零開始研究和學習Cordova的使用,從上篇在官網實現命令行建立工程,到工程運行起來,實際項目中怎麼使用Cordova,可能還有一些人並不懂,其實我當時執行完那些命令後也不懂。 後來搜索了一下關於Cordova 講解的文章,沒有找到一篇清晰將出如何使用Cordova,大多都是講如何將Cordova.xcodeproj拖進工程等等。我不喜歡工程裏多餘的東西太多,其實並不須要將Cordova 整個工程拖進去,只須要一部分就夠了,下面我會一一道來。javascript
我這裏用Xcode 8 新建了一個工程,叫 JS_OC_Cordova
,而後將Cordova關鍵類添加進工程。 有哪些關鍵類呢? 這裏添加config.xml
、Private
和 Public
兩個文件夾裏的全部文件。工程目錄結構以下:html
而後運行工程,😏 😏 😏 ,你會發現報了一堆的錯誤:java
爲何有會這麼多報錯呢?git
緣由是Cordova 部分類中,並無#import <Foundation/Foundation.h>
,可是它們卻使用了這個庫裏的NSArray、NSString 等類型。 爲何用在終端裏用命令行建立的工程就正常呢? 那是由於用命令行建立的工程裏已經包含了pch 文件,而且已經import 了 Foundation框架。截圖爲證:github
其實這裏有兩種解決方案:web
一、在報錯的類裏添加上 #import <Foundation/Foundation.h>
;json
二、添加一個pch 文件,在pch文件里加上 #import <Foundation/Foundation.h>
。數組
我選擇第二種方案:xcode
再次編譯、運行,依然報錯。 What the fuck 😱 😱 😱 !!!瀏覽器
不用急,這裏報錯是由於Cordova的類引用錯誤,在命令行建立的工程裏Cordova 是以子工程的形式加入到目標工程中,兩個工程的命名空間不一樣,因此import 是用 相似這樣的方式#import <Cordova/CDV.h>
,可是咱們如今是直接在目標工程裏添加Cordova,因此要把#import <Cordova/CDV.h>
改成 #import "CDV.h"
。其餘的文件引用報錯同理。
固然,若是想偷懶,也能夠從後面我給的示例工程裏拷貝,我修改過的Cordova庫。
首先將 ViewController
的父類改成 CDVViewController
。以下圖所示:
www
的文件夾,而後在文件夾裏放入要加載的HTML和
cordova.js
。 這裏把
www
添加進工程時,須要注意勾選的是create foler references,建立的是藍色文件夾。
上面爲何說是方便起見呢? 先說答案,由於CDVViewController
有兩個屬性 wwwFolderName
和 startPage
, wwwFolderName
的默認值爲www
,startPage
的默認值爲 index.html
。
在 CDVViewController
的 viewDidLoad
方法中,調用了與網頁相關的三個方法: - loadSetting
、- createGapView
、- appUrl
。 先看- loadSetting
,這裏會對 wwwFolderName
和 startPage
設置默認值,代碼以下:
- (void)loadSettings
{
CDVConfigParser* delegate = [[CDVConfigParser alloc] init];
[self parseSettingsWithParser:delegate];
// Get the plugin dictionary, whitelist and settings from the delegate.
self.pluginsMap = delegate.pluginsDict;
self.startupPluginNames = delegate.startupPluginNames;
self.settings = delegate.settings;
// And the start folder/page.
if(self.wwwFolderName == nil){
self.wwwFolderName = @"www";
}
if(delegate.startPage && self.startPage == nil){
self.startPage = delegate.startPage;
}
if (self.startPage == nil) {
self.startPage = @"index.html";
}
// Initialize the plugin objects dict.
self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20];
}
複製代碼
要看- createGapView
,是由於這個方法內部先調用了一次 - appUrl
,因此關鍵仍是- appUrl
。源碼以下:
- (NSURL*)appUrl
{
NSURL* appURL = nil;
if ([self.startPage rangeOfString:@"://"].location != NSNotFound) {
appURL = [NSURL URLWithString:self.startPage];
} else if ([self.wwwFolderName rangeOfString:@"://"].location != NSNotFound) {
appURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", self.wwwFolderName, self.startPage]];
} else if([self.wwwFolderName hasSuffix:@".bundle"]){
// www folder is actually a bundle
NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName];
appURL = [bundle URLForResource:self.startPage withExtension:nil];
} else if([self.wwwFolderName hasSuffix:@".framework"]){
// www folder is actually a framework
NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName];
appURL = [bundle URLForResource:self.startPage withExtension:nil];
} else {
// CB-3005 strip parameters from start page to check if page exists in resources
NSURL* startURL = [NSURL URLWithString:self.startPage];
NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];
if (startFilePath == nil) {
appURL = nil;
} else {
appURL = [NSURL fileURLWithPath:startFilePath];
// CB-3005 Add on the query params or fragment.
NSString* startPageNoParentDirs = self.startPage;
NSRange r = [startPageNoParentDirs rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"?#"] options:0];
if (r.location != NSNotFound) {
NSString* queryAndOrFragment = [self.startPage substringFromIndex:r.location];
appURL = [NSURL URLWithString:queryAndOrFragment relativeToURL:appURL];
}
}
}
return appURL;
}
複製代碼
此時運行效果圖:
項目裏通常都是這種狀況,接口返回H5地址,而後用網頁加載H5地址。 只須要設置下 self.startPage
就行了。
這裏有幾個須要注意的地方:
self.startPage
的賦值,必須在[super viewDidLoad]以前,不然self.startPage 會被默認賦值爲index.html。config.xml
中修改一下配置,不然加載遠程H5時,會自動打開瀏覽器加載。 須要添加的配置是:<allow-navigation href="https://*/*" />
<allow-navigation href="http://*/*" />
複製代碼
cordova.js
文件。info.plist
中添加 App Transport Security Setting
的設置。運行效果圖:
在插件中實現JS要調用的原生方法,插件要繼承自CDVPlugin
,示例代碼以下:
#import "CDV.h"
@interface HaleyPlugin : CDVPlugin
- (void)scan:(CDVInvokedUrlCommand *)command;
- (void)location:(CDVInvokedUrlCommand *)command;
- (void)pay:(CDVInvokedUrlCommand *)command;
- (void)share:(CDVInvokedUrlCommand *)command;
- (void)changeColor:(CDVInvokedUrlCommand *)command;
- (void)shake:(CDVInvokedUrlCommand *)command;
- (void)playSound:(CDVInvokedUrlCommand *)command;
@end
複製代碼
配置插件,是在config.xml的widget
中添加本身建立的插件。 以下圖所示:
關於插件中方法的實現有幾個注意點:
一、若是你發現相似以下的警告:
THREAD WARNING: ['scan'] took '290.006104' ms. Plugin should use a background thread.
複製代碼
那麼直須要將實現改成以下方式便可:
[self.commandDelegate runInBackground:^{
// 這裏是實現
}];
複製代碼
示例代碼:
- (void)scan:(CDVInvokedUrlCommand *)command
{
[self.commandDelegate runInBackground:^{
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"原生彈窗" message:nil delegate:nil cancelButtonTitle:@"知道了" otherButtonTitles:nil, nil];
[alertView show];
});
}];
}
複製代碼
二、如何獲取JS 傳過來的參數呢?
CDVInvokedUrlCommand
參數,其實有四個屬性,分別是arguments
、callbackId
、className
、methodName
。其中arguments
,就是參數數組。
看一個獲取參數的示例代碼:
- (void)share:(CDVInvokedUrlCommand *)command
{
NSUInteger code = 1;
NSString *tip = @"分享成功";
NSArray *arguments = command.arguments;
if (arguments.count < 3) {;
code = 2;
tip = @"參數錯誤";
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@')",tip];
[self.commandDelegate evalJs:jsStr];
return;
}
NSLog(@"從H5獲取的分享參數:%@",arguments);
NSString *title = arguments[0];
NSString *content = arguments[1];
NSString *url = arguments[2];
// 這裏是分享的相關代碼......
// 將分享結果返回給js
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
[self.commandDelegate evalJs:jsStr];
}
複製代碼
三、如何將Native的結果回調給JS ?
這裏有兩種方式:第一種是直接執行JS,調用UIWebView 的執行js 方法。示例代碼以下:
// 將分享結果返回給js
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
[self.commandDelegate evalJs:jsStr];
複製代碼
第二種是,使用Cordova 封裝好的對象CDVPluginResult
和API。 使用這種方式時,在JS 調用原生功能時,必須設置執行成功的回調和執行失敗的回調。即設置cordova.exec(successCallback, failCallback, service, action, actionArgs)
的第一個參數和第二個參數。像這樣:
function locationClick() {
cordova.exec(setLocation,locationError,"HaleyPlugin","location",[]);
}
複製代碼
而後,Native 調用JS 的示例代碼:
- (void)location:(CDVInvokedUrlCommand *)command
{
// 獲取定位信息......
// 下一行代碼之後能夠刪除
// NSString *locationStr = @"廣東省深圳市南山區學府路XXXX號";
NSString *locationStr = @"錯誤信息";
// NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",locationStr];
// [self.commandDelegate evalJs:jsStr];
[self.commandDelegate runInBackground:^{
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:locationStr];
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}];
}
複製代碼
終於到重點了,JS想要調用原生代碼,如何操做呢?我用本地HTML 來演示。 首先,HTML中須要加載 cordova.js
,須要注意該js 文件的路徑,由於個人cordova.js
與HTML放在同一個文件夾,因此src 是這樣寫:
<script type="text/javascript" src="cordova.js"></script>
複製代碼
而後,在HTML中建立幾個按鈕,以及實現按鈕的點擊事件,示例代碼以下:
<input type="button" value="掃一掃" onclick="scanClick()" />
<input type="button" value="獲取定位" onclick="locationClick()" />
<input type="button" value="修改背景色" onclick="colorClick()" />
<input type="button" value="分享" onclick="shareClick()" />
<input type="button" value="支付" onclick="payClick()" />
<input type="button" value="搖一搖" onclick="shake()" />
<input type="button" value="播放聲音" onclick="playSound()" />
複製代碼
點擊事件對應的關鍵的JS代碼示例:
function scanClick() {
cordova.exec(null,null,"HaleyPlugin","scan",[]);
}
function shareClick() {
cordova.exec(null,null,"HaleyPlugin","share",['測試分享的標題','測試分享的內容','http://m.rblcmall.com/share/openShare.htm?share_uuid=shdfxdfdsfsdfs&share_url=http://m.rblcmall.com/store_index_32787.htm&imagePath=http://c.hiphotos.baidu.com/image/pic/item/f3d3572c11dfa9ec78e256df60d0f703908fc12e.jpg']);
}
function locationClick() {
cordova.exec(setLocation,locationError,"HaleyPlugin","location",[]);
}
function locationError(error) {
asyncAlert(error);
document.getElementById("returnValue").value = error;
}
function setLocation(location) {
asyncAlert(location);
document.getElementById("returnValue").value = location;
}
複製代碼
JS 要調用原生,執行的是:
// successCallback : 成功的回調方法
// failCallback : 失敗的回調方法
// server : 所要請求的服務名字,就是插件類的名字
// action : 所要請求的服務具體操做,其實就是Native 的方法名,字符串。
// actionArgs : 請求操做所帶的參數,這是個數組。
cordova.exec(successCallback, failCallback, service, action, actionArgs);
複製代碼
cordova,是cordova.js
裏定義的一個 var
結構體,裏面有一些方法以及其餘變量,關於exec ,能夠看 iOSExec這個js 方法。 大體思想就是,在JS中定義一個數組和一個字典(鍵值對)。 數組中存放的就是:
callbackId與服務、操做、參數的對應關係轉成json 存到上面全局數組中。
var command = [callbackId, service, action, actionArgs];
// Stringify and queue the command. We stringify to command now to
// effectively clone the command arguments in case they are mutated before
// the command is executed.
commandQueue.push(JSON.stringify(command));
複製代碼
而字典裏存的是回調,固然回調也是與callbackId對應的,這裏的callbackId與上面的callbackId是同一個:
callbackId = service + cordova.callbackId++;
cordova.callbacks[callbackId] =
{success:successCallback, fail:failCallback};
複製代碼
依然是作一個假的URL 請求,而後在UIWebView的代理方法中攔截請求。
JS 方法 iOSExec
中會調用 另外一個JS方法 pokeNative
,而這個pokeNative
,看到他的代碼實現就會發現與UIWebView 開啓一個URL 的操做是同樣的:
function pokeNative() {
// CB-5488 - Don't attempt to create iframe before document.body is available. if (!document.body) { setTimeout(pokeNative); return; } // Check if they've removed it from the DOM, and put it back if so.
if (execIframe && execIframe.contentWindow) {
execIframe.contentWindow.location = 'gap://ready';
} else {
execIframe = document.createElement('iframe');
execIframe.style.display = 'none';
execIframe.src = 'gap://ready';
document.body.appendChild(execIframe);
}
failSafeTimerId = setTimeout(function() {
if (commandQueue.length) {
// CB-10106 - flush the queue on bridge change
if (!handleBridgeChange()) {
pokeNative();
}
}
}, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire). } 複製代碼
看到這裏,咱們只須要去搜索一下攔截URL 的代理方法,而後驗證咱們的想法接口。 我搜索webView:shouldStartLoadWIthRequest:navigationType
方法,而後打上斷點,看以下的堆棧調用:
關鍵代碼是這裏,判斷url 的scheme 是否等於 gap
。
if ([[url scheme] isEqualToString:@"gap"]) {
[vc.commandQueue fetchCommandsFromJs];
// The delegate is called asynchronously in this case, so we don't have to use // flushCommandQueueWithDelayedJs (setTimeout(0)) as we do with hash changes. [vc.commandQueue executePending]; return NO; } 複製代碼
fetchCommandsFromJs
是調用js 中的nativeFetchMessages()
,獲取commandQueue
裏的json 字符串; executePending
中將json 字符串轉換爲CDVInvokedUrlCommand
對象,以及利用runtime
,將js 裏的服務和 方法,轉換對象,而後調用objc_msgSend 直接調用執行,這樣就進入了插件的對應的方法中了。
這一套思想與WebViewJavascriptBridge
的思想很類似。
這個很是簡單,若是是在控制器中,那麼只須要像以下這樣既可:
- (void)testClick
{
// 方式一:
NSString *jsStr = @"asyncAlert('哈哈啊哈')";
[self.commandDelegate evalJs:jsStr];
}
複製代碼
這裏的evalJs
內部調用的實際上是 UIWebView
的 stringByEvaluatingJavaScriptFromString
方法。
首先:
而後,添加一個環境變量:
好了,到這裏關於Cordova 的講解就結束了。
示例工程的github地址:JS_OC_Cordova
Have Fun!