选择Cordova开发Hybrid App。除此之外还有其他方式,如:React Native, Vue.js等。

一.项目集成

  • 官网提供:NPM(Nodejs的包管理工具,node package manager)方式集成
    • $ npm install -g cordova #若全局安装失败,则进入项目目录执行$ npm install cordova
    • $ cordova create
    • $ cd MyApp
    • $ cordova platform add #cordova platform查看支持平台,这里选ios
  • cocoapods:cocoapods(Ruby开发的iOS库管理工具)
    • $ cd
    • $ pod init # 生成Podfile文件
    • $ vi Podfile # 添加 pod ‘Cordova’, ‘~> 3.6’

二.Cordova工作原理 github

1.交互流程
  • js方法:Cordova.exec(successCallback, failCallback, service, action, actionArgs)
  1. cordova.js内部方法:iOSExec()
  2. webView方法:shouldStartLoadWithRequest::
  3. cordova方法:FetchCommonsFromJS
  4. cordova plugin
  5. do sth
  6. cordova: sendPlugin
  7. oc: StringByEvalutingJavascriptFromString
2.源码

cordova.js

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
57
58
59
60
function iOSExec() {
// 定义变量接受传入的参数,其中
// successCallback, failCallback:成功失败的回调
// service:iOS native的plugin名
// action:plugin中的方法名
// actionArgs: 方法参数
    var successCallback, failCallback, service, action, actionArgs;
// callbackId: 作为key,映射回调方法
    var callbackId = null;
// js中arguments为方法隐式方法,数据结构类数组
    if (typeof arguments[0] !== 'string') {
        // FORMAT ONE
        successCallback = arguments[0];
        failCallback = arguments[1];
        service = arguments[2];
        action = arguments[3];
        actionArgs = arguments[4];
        // Since we need to maintain backwards compatibility, we have to pass
        // an invalid callbackId even if no callback was provided since plugins
        // will be expecting it. The Cordova.exec() implementation allocates
        // an invalid callbackId and passes it even if no callbacks were given.
        callbackId = 'INVALID';
    } else {
        throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' +
            'cordova.exec(null, null, \\'Service\\', \\'action\\', [ arg1, arg2 ]);'
        );
    }
    // If actionArgs is not provided, default to an empty array
    actionArgs = actionArgs || [];
    // Register the callbacks and add the callbackId to the positional
    // arguments if given.
    if (successCallback || failCallback) {
        callbackId = service + cordova.callbackId++;
        cordova.callbacks[callbackId] =
            {success:successCallback, fail:failCallback};
    }
    actionArgs = massageArgsJsToNative(actionArgs);
    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));
    // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
    // then the queue will be flushed when it returns; no need for a poke.
    // Also, if there is already a command in the queue, then we've already
    // poked the native side, so there is no reason to do so again.
    if (!isInContextOfEvalJs && commandQueue.length == 1) {
// 与oc通信方法
        pokeNative();
    }
}

old version中使用XMLHttpRequest发送请求任务,最新version使用iframe.contentWindow.localtion发送请求,并与native约定URL为gap://ready。

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
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);
    }
    // Use a timer to protect against iframe being unloaded during the poke (CB-7735).
    // This makes the bridge ~ 7% slower, but works around the poke getting lost
    // when the iframe is removed from the DOM.
    // An onunload listener could be used in the case where the iframe has just been
    // created, but since unload events fire only once, it doesn't work in the normal
    // case of iframe reuse (where unload will have already fired due to the attempted
    // navigation of the page).
    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).
}

cordova_ios

  • 项目初始化
    CDVViewController中初始化CDVCommandQueueCDVConfigParser
  • CDVCommandQueuefetchCommandsFromJs拦截js请求,并解析js参数,CDVInvokedUrlCommand类定义了参数model
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)fetchCommandsFromJs
{
__weak CDVCommandQueue* weakSelf = self;
NSString* js = @"cordova.require('cordova/exec').nativeFetchMessages()";
// 封装stringByEvaluatingJavaScriptFromString::方法,native调用js方法
[_viewController.webViewEngine evaluateJavaScript:js
completionHandler:^(id obj, NSError* error) {
if ((error == nil) && [obj isKindOfClass:[NSString class]]) {
NSString* queuedCommandsJSON = (NSString*)obj;
CDV_EXEC_LOG(@"Exec: Flushed JS->native queue (hadCommands=%d).", [queuedCommandsJSON length] > 0);
[weakSelf enqueueCommandBatch:queuedCommandsJSON];
// this has to be called here now, because fetchCommandsFromJs is now async (previously: synchronous)
[self executePending];
}
}];
}
  • CDVConfigParser读取解析config.xml中配置信息,将plugin信息保存到pluginsDict属性中,在CDVViewController中注册插件
  • 自定义插件,传入callbackID
  • 实现协议CDVCommandDelegate中sendPluginResult::方法,将plugin result返回js
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
- (void)sendPluginResult:(CDVPluginResult*)result callbackId:(NSString*)callbackId
{
CDV_EXEC_LOG(@"Exec(%@): Sending result. Status=%@", callbackId, result.status);
// This occurs when there is are no win/fail callbacks for the call.
if ([@"INVALID" isEqualToString:callbackId]) {
return;
}
// This occurs when the callback id is malformed.
if (![self isValidCallbackId:callbackId]) {
NSLog(@"Invalid callback id received by sendPluginResult");
return;
}
int status = [result.status intValue];
BOOL keepCallback = [result.keepCallback boolValue];
NSString* argumentsAsJSON = [result argumentsAsJSON];
BOOL debug = NO;
#ifdef DEBUG
debug = YES;
#endif
NSString* js = [NSString stringWithFormat:@"cordova.require('cordova/exec').nativeCallback('%@',%d,%@,%d, %d)", callbackId, status, argumentsAsJSON, keepCallback, debug];
// 封装了webView方法stringByEvaluatingJavaScriptFromString::
[self evalJsHelper:js];
}