iOS下JS与OC互相调用(八)--Cordova详解+实战
小编:管理员 813阅读 2022.09.13
由于项目中Cordova相关功能一直是同事在负责,所以也没有仔细的去探究Cordova到底是怎么使用的,又是如何实现JS 与 OC 的交互。所以我基本上是从零开始研究和学习Cordova的使用,从上篇在官网实现命令行创建工程,到工程运行起来,实际项目中怎么使用Cordova,可能还有一些人并不懂,其实我当时执行完那些命令后也不懂。 后来搜索了一下关于Cordova 讲解的文章,没有找到一篇清晰将出如何使用Cordova,大多都是讲如何将Cordova.xcodeproj拖进工程等等。我不喜欢工程里多余的东西太多,其实并不需要将Cordova 整个工程拖进去,只需要一部分就够了,下面我会一一道来。
1.新建工程,添加Cordova 关键类我这里用Xcode 8 新建了一个工程,叫JS_OC_Cordova,然后将Cordova关键类添加进工程。 有哪些关键类呢? 这里添加config.xml、Private和Public两个文件夹里的所有文件。工程目录结构如下:
然后运行工程,? ? ? ,你会发现报了一堆的错误:
为什么有会这么多报错呢?
原因是Cordova 部分类中,并没有#import
其实这里有两种解决方案:1、在报错的类里添加上#import
再次编译、运行,依然报错。 What the fuck ? ? ? !!!
不用急,这里报错是因为Cordova的类引用错误,在命令行创建的工程里Cordova 是以子工程的形式加入到目标工程中,两个工程的命名空间不同,所以import 是用 类似这样的方式#import
当然,如果想偷懒,也可以从后面我给的示例工程里拷贝,我修改过的Cordova库。
2.设置网页控制器,添加网页首先将ViewController的父类改为CDVViewController。如下图所示:
这里分两种情况,加载本地HTML 和远程HTML 地址。 ** 加载本地HTML ** 加载本地HTML,为了方便起见,首先新建一个叫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; }复制
此时运行效果图:
** 加载远程HTML ** 项目里一般都是这种情况,接口返回H5地址,然后用网页加载H5地址。 只需要设置下self.startPage就好了。
这里有几个需要注意的地方:
- self.startPage的赋值,必须在[super viewDidLoad]之前,否则self.startPage 会被默认赋值为index.html。
- 需要在config.xml中修改一下配置,否则加载远程H5时,会自动打开浏览器加载。 需要添加的配置是:
复制
- 远程H5中也要引用cordova.js文件。
- 在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中添加自己创建的插件。 如下图所示:
关于插件中方法的实现有几个注意点: 1、如果你发现类似如下的警告:
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]; }); }]; }复制
2、如何获取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]; }复制
3、如何将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 是这样写:
复制然后,在HTML中创建几个按钮,以及实现按钮的点击事件,示例代码如下:
复制
点击事件对应的关键的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内部调用的其实是UIWebView的stringByEvaluatingJavaScriptFromString方法。
6.如果你在使用Xcode 8时,觉得控制台里大量的打印很碍眼,可以这样设置来去掉。首先:
然后,添加一个环境变量:
好了,到这里关于Cordova 的讲解就结束了。
示例工程的github地址:JS_OC_Cordova
Have Fun!
相关推荐
- Cordova 什么是Cordova? Cordova是用于使用HTML,CSS和JS构建移动应用的平台。我们可以认为Cordova是一个容器,用于将我们的网络应用程序与本机移动功能连接。默认情况下,Web应用程序不能使用本机移动功能。这就是Cordova进来的地方。它为网络应用和移动设备之间的连…
- 3DMAX提示和技巧 本主题标识使用 Civil View 的一些重要提示和技巧。常规使用屏幕分辨率至少为 1280x1024 的 Civil View。低于此分辨率时,一些面板将占用过多屏幕空间。 将视口设置为线框显示以达到最佳性能。 要尽可能简化用户界面,请在单个视口中工作并关闭 3ds Max 命令面…