iOS下JS与OC互相调用(八)--Cordova详解+实战

扯两句,能够跳过

因为项目中Cordova相关功能一直是同事在负责,因此也没有仔细的去探究Cordova究竟是怎么使用的,又是如何实现JS 与 OC 的交互。因此我基本上是从零开始研究和学习Cordova的使用,从上篇在官网实现命令行建立工程,到工程运行起来,实际项目中怎么使用Cordova,可能还有一些人并不懂,其实我当时执行完那些命令后也不懂。 后来搜索了一下关于Cordova 讲解的文章,没有找到一篇清晰将出如何使用Cordova,大多都是讲如何将Cordova.xcodeproj拖进工程等等。我不喜欢工程里多余的东西太多,其实并不须要将Cordova 整个工程拖进去,只须要一部分就够了,下面我会一一道来。javascript

1.新建工程,添加Cordova 关键类

我这里用Xcode 8 新建了一个工程,叫 JS_OC_Cordova,而后将Cordova关键类添加进工程。 有哪些关键类呢? 这里添加config.xmlPrivatePublic 两个文件夹里的全部文件。工程目录结构以下: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库。

2.设置网页控制器,添加网页

首先将 ViewController 的父类改成 CDVViewController。以下图所示:

这里分两种状况,加载本地HTML 和远程HTML 地址。 ** 加载本地HTML ** 加载本地HTML,为了方便起见,首先新建一个叫 www的文件夹,而后在文件夹里放入要加载的HTML和 cordova.js。 这里把 www添加进工程时,须要注意勾选的是create foler references,建立的是蓝色文件夹。

最终的目录结构以下:

上面为何说是方便起见呢? 先说答案,由于CDVViewController有两个属性 wwwFolderNamestartPagewwwFolderName 的默认值为wwwstartPage 的默认值为 index.html

CDVViewControllerviewDidLoad方法中,调用了与网页相关的三个方法: - loadSetting- createGapView- appUrl。 先看- loadSetting,这里会对 wwwFolderNamestartPage 设置默认值,代码以下:

- (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;
}
复制代码

此时运行效果图:

加载远程HTML

项目里通常都是这种状况,接口返回H5地址,而后用网页加载H5地址。 只须要设置下 self.startPage就行了。

这里有几个须要注意的地方:

  1. self.startPage的赋值,必须在[super viewDidLoad]以前,不然self.startPage 会被默认赋值为index.html。
  2. 须要在config.xml中修改一下配置,不然加载远程H5时,会自动打开浏览器加载。 须要添加的配置是:
<allow-navigation href="https://*/*" />
<allow-navigation href="http://*/*"  />
复制代码
  1. 远程H5中也要引用cordova.js文件。
  2. info.plist 中添加 App Transport Security Setting的设置。

运行效果图:

3.建立插件,配置插件

在插件中实现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 参数,其实有四个属性,分别是argumentscallbackIdclassNamemethodName。其中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];
    }];
}
复制代码

4.JS 调用Native 功能

终于到重点了,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};
复制代码

iOSExec 里又是如何调用到原生方法的呢?

依然是作一个假的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的思想很类似。

5. Native 调用 JS 方法

这个很是简单,若是是在控制器中,那么只须要像以下这样既可:

- (void)testClick
{
    // 方式一:
    NSString *jsStr = @"asyncAlert('哈哈啊哈')";
    [self.commandDelegate evalJs:jsStr];
    
}
复制代码

这里的evalJs内部调用的实际上是 UIWebViewstringByEvaluatingJavaScriptFromString 方法。

6.若是你在使用Xcode 8时,以为控制台里大量的打印很碍眼,能够这样设置来去掉。

首先:

而后,添加一个环境变量:

好了,到这里关于Cordova 的讲解就结束了。

示例工程的github地址:JS_OC_Cordova

Have Fun!

相关文章
相关标签/搜索