幾個月前看過phonegap在Android和WP上的實現源碼,當時苦於沒mac環境,直到現在才抽出時間學習了一下phonegap是如何讓JS與native串聯起來的。
phonegap在IOS上和在WP很類似,由於IOS App內置的WebBrowser提供了比較好的與JS的互通機制,所以整個代碼讀起來比較輕鬆,架構圖如下所示:
其中有這麼幾個點值得強調一下:
- Phonegap IOS的項目結構及初始化過程
- JS調用Native
- Native向JS返回結果
1)Phonegap IOS的項目結構及初始化過程
Phonegap IOS的項目結構非常簡單,其實就是一個標準的單View的IOS App。只不過這個單view的視圖文件xib是空的,它在MainViewController裏添加了一個UIWebView,通過這個UIWebView來展現www裏的html。
Phonegap的初始化過程包含JS端和Native端,這兩端都是基於事件偵聽的方式結合起來,JS端主要包含以下幾個點:
- onDOMContentLoaded:dom載入完成
- onNativeReady:Native端WebUI載入完成
- onCordovaReady:JS端相關objects都創建完成
- deviceready:整個phonegap初始化完成
以上幾個重要事件的先後順序和hander偵聽是通過channel組件架構,所謂的channel組件實際上就是phonegap自制的保障事件偵聽和觸發的組件,這塊代碼寫得不錯,短短2百多行代碼,就打造了一個JS端事件偵聽的框架,有興趣的同學值得讀一讀。其中 onNativeReady是被native端調的,當native端WebUI初始化好後就會fire JS端onNativeReady事件,下面來看看native的幾個關鍵的初始化節點:
- AppDelegate.didFinishLaunchingWithOptions:App啓動,初始化controller和view
- CDVViewController.viewDidLoad:view加載,初始化WebView
- CDVViewController.webviewDidFinishLoad:WebView加載,觸發JS端onNativeReady
Native端存在着App->view->webview三個層次,以上三個點正好對應着這三個層次的加載。
2)JS調用Native
IOS的UIWebViewDelegate提供了shouldStartLoadWithRequest方法,它能截獲web端url請求,因此phonegap就是通過在web端構造一個不可見的iframe,並置其src爲gap://ready,Native端截獲這個請求後就會得知此時JS端有請求。這塊代碼可見"cordova/exec"模塊:
createGapBridge = function() {
gapBridge = document.createElement("iframe");
gapBridge.setAttribute("style", "display:none;");
gapBridge.setAttribute("height","0px");
gapBridge.setAttribute("width","0px");
gapBridge.setAttribute("frameborder","0");
document.documentElement.appendChild(gapBridge);
}
那麼具體的調用信息是如何傳到native的呢?實際上是每次在js端調用exec時,phonegap會把調用信息放入cordova.commandQueue隊列中,並通知native端。native端得到通知後,會調用js端"cordova/plugin/ios/nativecomm"模塊裏的代碼拿到cordova.commandQueue隊列中所有調用信息,並依次調用plugin來執行請求,源碼如下所示:
JS端:
cordova.commandQueue.push(JSON.stringify(command));
if (cordova.commandQueue.length == 1 && !cordova.commandQueueFlushing) {
if (!gapBridge) {
createGapBridge();
}
gapBridge.src = "gap://ready";
}
Native端:shouldStartLoadWithRequest
if ([[url scheme] isEqualToString:@"gap"]) {
[self flushCommandQueue];
return NO;
}
- (void) flushCommandQueue
{
[self.webView stringByEvaluatingJavaScriptFromString:
@"cordova.commandQueueFlushing = true"];
// Keep executing the command queue until no commands get executed.
// This ensures that commands that are queued while executing other
// commands are executed as well.
int numExecutedCommands = 0;
do {
numExecutedCommands = [self executeQueuedCommands];
} while (numExecutedCommands != 0);
[self.webView stringByEvaluatingJavaScriptFromString:
@"cordova.commandQueueFlushing = false"];
}
- (int) executeQueuedCommands
{
// Grab all the queued commands from the JS side.
NSString* queuedCommandsJSON = [self.webView stringByEvaluatingJavaScriptFromString:
@"cordova.require('cordova/plugin/ios/nativecomm')()"];
// Parse the returned JSON array.
NSArray* queuedCommands =
[queuedCommandsJSON cdvjk_objectFromJSONString];
// Iterate over and execute all of the commands.
for (NSString* commandJson in queuedCommands) {
if(![self.commandDelegate execute:
[CDVInvokedUrlCommand commandFromObject:
[commandJson cdvjk_mutableObjectFromJSONString]]])
{
static NSUInteger maxLogLength = 1024;
NSString* commandString = ([commandJson length] > maxLogLength) ?
[NSString stringWithFormat:@"%@[...]", [commandJson substringToIndex:maxLogLength]] :
commandJson;
DLog(@"FAILED pluginJSON = %@", commandString);
}
}
return [queuedCommands count];
}
這幾段代碼就算沒學過Objective-C也應該能猜出個大概,這也算是一個供應者和消費者模式的應用實例。
3)Native向JS返回結果
上段代碼也透露了Native調用JS的方式:
self.webView stringByEvaluatingJavaScriptFromString
有了這個便利的方法,可以避免像在Android端使用ajax和polling這麼複雜的實現。對於一個標準的phonegap的調用請求,native的plugin完成任務後,會統一調用JS端cordova.callbackSuccess和cordova.callbackError,見CDVPluginResult.m:-(NSString*) toSuccessCallbackString: (NSString*) callbackId
{
NSString* successCB = [NSString stringWithFormat:@"cordova.callbackSuccess('%@',%@);", callbackId, [self toJSONString]];
DLog(@"PluginResult toSuccessCallbackString: %@", successCB);
return successCB;
}
-(NSString*) toErrorCallbackString: (NSString*) callbackId
{
NSString* errorCB = [NSString stringWithFormat:@"cordova.callbackError('%@',%@);", callbackId, [self toJSONString]];
DLog(@"PluginResult toErrorCallbackString: %@", errorCB);
return errorCB;
}
到此,Phonegap在IOS平臺上的實現比較關鍵的幾個點已分析完,後面我會基於之前對android和wp源碼分析一起,來看看這樣的實現方式會有那些限制和性能損耗