iOS下JS与OC互相调用(八)--Cordova详解+实战
扯两句,可以跳过
由于项目中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 <Foundation/Foundation.h>
,但是它们却使用了这个库里的NSArray、NSString 等类型。
为什么用在终端里用命令行创建的工程就正常呢?
那是因为用命令行创建的工程里已经包含了pch 文件,并且已经import 了 Foundation框架。截图为证:
其实这里有两种解决方案:1、在报错的类里添加上 #import <Foundation/Foundation.h>
;2、添加一个pch 文件,在pch文件里加上 #import <Foundation/Foundation.h>
。
我选择第二种方案:
再次编译、运行,依然报错。 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
有两个属性 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
就好了。
这里有几个需要注意的地方:
1.self.startPage
的赋值,必须在[super viewDidLoad]之前,否则self.startPage 会被默认赋值为index.html。
2. 需要在config.xml
中修改一下配置,否则加载远程H5时,会自动打开浏览器加载。
需要添加的配置是:
<allow-navigation href="https://*/*" />
<allow-navigation href="http://*/*" />
- 远程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。
示例代码:
- (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是同一个:
<div class="se-preview-section-delimiter"></div>
callbackId = service + cordova.callbackId++;
cordova.callbacks[callbackId] =
{success:successCallback, fail:failCallback};
** iOSExec 里又是如何调用到原生方法的呢?**
依然是做一个假的URL 请求,然后在UIWebView的代理方法中拦截请求。
JS 方法 `iOSExec `中会调用 另一个JS方法 `pokeNative`,而这个`pokeNative`,看到他的代码实现就会发现与UIWebView 开启一个URL 的操作是一样的:
<div class="se-preview-section-delimiter"></div>
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` 方法,然后打上断点,看如下的堆栈调用:
![](http://upload-images.jianshu.io/upload_images/727768-b290668124dacb99.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
关键代码是这里,判断url 的scheme 是否等于 `gap`。
<div class="se-preview-section-delimiter"></div>
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`的思想很相似。
<div class="se-preview-section-delimiter"></div>
## 5. Native 调用 JS 方法
这个非常简单,如果是在控制器中,那么只需要像如下这样既可:
<div class="se-preview-section-delimiter"></div>
- (void)testClick
{
// 方式一:
NSString *jsStr = @”asyncAlert(‘哈哈啊哈’)”;
[self.commandDelegate evalJs:jsStr];
}
``
evalJs
这里的内部调用的其实是
UIWebView的
stringByEvaluatingJavaScriptFromString` 方法。
6.如果你在使用Xcode 8时,觉得控制台里大量的打印很碍眼,可以这样设置来去掉。
首先:
然后,添加一个环境变量:
好了,到这里关于Cordova 的讲解就结束了。
示例工程的github地址:JS_OC_Cordova
Have Fun!
iOS下JS与OC互相调用(八)--Cordova详解+实战的更多相关文章
- iOS下JS与OC互相调用(六)--WKWebView + WebViewJavascriptBridge
上一篇文章介绍了UIWebView 如何通过WebViewJavascriptBridge 来实现JS 与OC 的互相调用,这一篇来介绍一下WKWebView 又是如何通过WebViewJavascr ...
- iOS下JS与OC互相调用(五)--UIWebView + WebViewJavascriptBridge
WebViewJavascriptBridge是一个有点年代的JS与OC交互的库,使用该库的著名应用还挺多的,目前这个库有7000+star.我去翻看了它的第一版本已经是4年前了,在版本V4.1.4以 ...
- iOS下JS与OC互相调用(四)--JavaScriptCore
前面讲完拦截URL的方式实现JS与OC互相调用,终于到JavaScriptCore了.它是从iOS7开始加入的,用 Objective-C 把 WebKit 的 JavaScript 引擎封装了一下, ...
- iOS下JS与OC互相调用(一)--UIWebView 拦截URL
最近准备把之前用UIWebView实现的JS与原生相互调用功能,用WKWebView来替换.顺便搜索整理了一下JS 与OC 交互的方式,非常之多啊.目前我已知的JS 与 OC 交互的处理方式: * 1 ...
- iOS下JS与OC互相调用(二)--WKWebView 拦截URL
在上篇文章中讲述了使用UIWebView拦截URL的方式来处理JS与OC交互. 由于UIWebView比较耗内存,性能上不太好,而苹果在iOS 8中推出了WKWebView. 同样的用WKWebVie ...
- iOS下JS与OC互相调用
背景情况: app项目中有几个界面是需要经常变动的(不仅是内容还有UI布局等),比如活动宣传界面就是属于这一类.但是由于AppStore提交审核也是需要时间的,就算审核快,也不至于每次都为了这点事频繁 ...
- iOS下JS与OC互相调用(七)--Cordova 基础
Cordova 简介 在介绍Cordova之前,必须先提一下PhoneGap.PhoneGap 是Nitobi软件公司2008年推出的一个框架,旨在弥补web 和iOS 之间的不足,使得web 和 i ...
- iOS下JS与OC互相调用(八)--Cordova简单实战
新建工程,添加Cordova 关键类 新建一个工程TestCordova 然后添加:confug.xml.Private 和 Public 两个文件夹里的所有文件 然后build 发现报错 为什么有会 ...
- iOS下JS与OC互相调用(三)--MessageHandler
使用WKWebView的时候,如果想要实现JS调用OC方法,除了拦截URL之外,还有一种简单的方式.那就是利用WKWebView的新特性MessageHandler来实现JS调用原生方法. Messa ...
随机推荐
- “百度杯”CTF比赛 九月场_YeserCMS
题目在i春秋ctf大本营 题目的提示并没有什么卵用,打开链接发现其实是easycms,百度可以查到许多通用漏洞 这里我利用的是无限报错注入 访问url/celive/live/header.php,直 ...
- Hibernate--对象关系
在hibernate中,关联关系映射分为单向关联和双向关联.共有七种关系 ·@Many To One ·@One To Many(单向) ·@One To Many(多向) ·@One To One( ...
- NOI2006 郁闷的出纳员
题目描述 OIER公司是一家大型专业化软件公司,有着数以万计的员工.作为一名出纳员,我的任务之一便是统计每位员工的工资.这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资 ...
- hdu 5887 搜索+剪枝
Herbs Gathering Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)T ...
- hdu 5458 Stability(树链剖分+并查集)
Stability Time Limit: 3000/2000 MS (Java/Others) Memory Limit: 65535/102400 K (Java/Others)Total ...
- 全排列hash-康拓展开
这是对很多全排列问题适用的方法,而且还能用于一些题目的判重 第一位是3,当第一位的数小于3时,那排列数小于321 如 123. 213 ,小于3的数有1.2 .所以有2*2!个.再看小于第二位2的:小 ...
- bzoj1071[SCOI2007]组队
1071: [SCOI2007]组队 Time Limit: 3 Sec Memory Limit: 128 MBSubmit: 2472 Solved: 792[Submit][Status][ ...
- JMQ
[京东技术]京东的MQ经历了JQ->AMQ->JMQ的发展,其中JQ的基于关系数据库,严格意义上讲称不上消息中间件,JMQ的存储是JFS和HBase,AMQ即ActiveMQ,本文说说JM ...
- JavaScript 题目(作用域)
var length = 10 function fn(){ alert(this.length) } var obj = { length: 5, method: function(fn) { fn ...
- [ SSH框架 ] Hibernate框架学习之二
一.Hibernate持久化类的编写规范 1.什么是持久化类 Hibernate是持久层的ORM影射框架,专注于数据的持久化工作.所谓持久化,就是将内存中的数据永久存储到关系型数据库中.那么知道了什么 ...