YonBuilder DeepEngine引擎实现了HTML+CSS+Javascript开发语言和Objective-C/Java/C/C++等Native开发语言之间的桥接,极大的丰富和增强了标准Javascript的能力。令前端开发者通过JS即可调用移动设备的底层功能,如:电话、短信、定位、多媒体、跨域http请求等,并能将如百度地图、支付宝等第三方厂商的SDK很容易的集成到自己的App中来。
为满足广大开发者自定义扩展Native plugin的需求,Yonbuilder移动开发推出原生插件扩展SDK,本SDK开放桥接机制,方便具有一定iOS基础的开发者自由开发定义Native扩展插件,丰富JS的能力,提升App的用户体验。
本文档面向所有使用该SDK的iOS开发人员、测试人员、合作伙伴以及对此感兴趣的其他用户。阅读该文档要求用户熟悉iOS应用开发,并且对Html、CSS、Javascript有一定了解。DeepEngine引擎强调传输数据的简洁和统一性,因此选择轻量级的JSON作为Javascript和Native语言之间通讯的数据载体,所以要求开发者同时要熟悉Objective-C和Javascript中JSON格式数据的操作。
「自定义插件」页面点击「下载插件开发SDK」按钮,下载最新版本的原生插件开发SDK,找到里面的ModulesDevProject_iOS.zip,这里面包含ModuleDemo、ModulesDevProject。
module.json定义了原生插件的基本信息,我们在开发原生插件之前需要先定义好原生插件名称、原生插件对应的类的名称、开放给JS的方法等。
字段解释:
name:对应值为原生插件的名称,JS中通过该名称来使用原生插件。
class:对应值为原生插件对应的类的名称,原生插件类需要继承于UZModule类。
methods:可选配置项。开放给JS的异步实例方法,多个方法以英文逗号隔开,此方法实现时需带一个参数。(此配置方式已废弃,在原生插件类里面使用 JS_METHOD 宏来定义开放给js的方法)
syncMethods:可选配置项。开放给JS的同步实例方法,多个方法以英文逗号隔开,此方法实现时需带一个参数。(此配置方式已废弃,在原生插件类里面使用 JS_METHOD_SYNC 宏来定义开放给js的同步方法)
launchClassMethod:可选配置项。若配置,引擎将在应用启动的时候调用该方法,注意该方法需是类方法,没有参数。(此配置方式已废弃,在原生插件类里面实现 +(void)onAppLaunch:方法代替)
Build Settings:可选配置项。用于配置编译选项,目前支持配置Other Linker Flags选项,如:
// 当配置-force_load时,后面的路径必须使用 $(PROJECT_DIR)/UZApp/UZModules,云编译时所有原生插件的库文件都会放在UZModules目录下。
{
"name":"moduleDemo",
"class":"UZModuleDemo",
"Build Settings": {
"Other Linker Flags": "-force_load $(PROJECT_DIR)/UZApp/UZModules/libModuleDemo.a"
}
}
如图,在UZApp工程中找到uz目录下的module.json文件,在里面添加原生插件的配置信息:
打开Xcode,在菜单中选择File-New-Project...,在Framework & Library中选择Cocoa Touch Static Library,创建一个名为ModuleDemo的工程。
这里我们将ModuleDemo工程作为UZApp工程的一个依赖工程,这样做的好处是运行UZApp工程时会自动编译ModuleDemo工程,并且可以方便地在ModuleDemo工程中打断点进行调试。
先关闭打开的静态库工程,然后打开UZApp工程,将ModuleDemo.xcodeproj直接拖到UZModules下,如图:
然后再按照下图在UZApp工程中的Linked Frameworks and Libraries处将libModuleDemo添加上:
将静态库工程的Build Active Architecture Only设置为No,如图:
将静态库工程的iOS Deployment Target设置为7.0,如图:
在ModuleDemo静态库工程中引入必要的UZModule.h头文件,其它头文件根据需要引入,这些头文件可以在下载的SDK包里面找到。
在ModuleDemo静态库工程中新建一个UZModuleDemo类,继承于UZModule类,其中UZModule类为原生插件的基类。原生插件开发过程中文件命名时提倡加前缀,以避免和其它原生插件冲突。
原生插件生命周期:
当在前端JS中首次调用原生插件的方法时,引擎会调用原生插件类的 - (id)initWithUZWebView:(UZWebView *)webView 方法进行初始化;
当原生插件所在的页面被销毁时,原生插件类也会被销毁,引擎会主动调用其 - (void)dispose 方法。
这里我们向js端开放一个showAlert方法,用于显示系统提示框。方法使用 JS_METHOD 宏来定义,参数类型为UZModuleMethodContext,可以通过context的param属性来获取js传入的参数,通过context的callbackWithRet:err:delete:方法回传数据给js端。如果要实现同步方法,请参考文档第5节的同步方法。
JS_METHOD(showAlert:(UZModuleMethodContext *)context) {
NSDictionary *param = context.param;
NSString *title = [param stringValueForKey:@"title" defaultValue:nil];
NSString *msg = [param stringValueForKey:@"msg" defaultValue:nil];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSDictionary *ret = @{@"index":@(1)};
[context callbackWithRet:ret err:nil delete:YES];
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSDictionary *ret = @{@"index":@(2)};
[context callbackWithRet:ret err:nil delete:YES];
}]];
[self.viewController presentViewController:alert animated:YES completion:nil];
}
实现原生插件方法时也可以使用以前旧的方式,和上面的方式相比,在方法声明、方法参数类型、以及回传数据方面有所区别,已经不推荐使用旧的方式。
1、首先需要在module.json里面配置methods字段:
{
"name":"moduleDemo",
"class":"UZModuleDemo",
"methods":["showAlert"]
}
2、在原生插件类中实现showAlert方法,参数类型为NSDictionary类型。如果js端调用该方法时传入了function,可以通过cbId字段获取该function对应的id,然后通过原生插件类的sendResultEventWithCallbackId:dataDict:errDict:doDelete方法回传数据给js端。
- (void)showAlert:(NSDictionary *)param {
NSInteger cbId = [param integerValueForKey:@"cbId" defaultValue:0];
NSString *title = [param stringValueForKey:@"title" defaultValue:nil];
NSString *msg = [param stringValueForKey:@"msg" defaultValue:nil];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSDictionary *ret = @{@"index":@(1)};
[self sendResultEventWithCallbackId:cbId dataDict:ret errDict:nil doDelete:YES];
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSDictionary *ret = @{@"index":@(2)};
[self sendResultEventWithCallbackId:cbId dataDict:ret errDict:nil doDelete:YES];
}]];
[self.viewController presentViewController:alert animated:YES completion:nil];
}
前端JS必须使用JSON格式数据作为JS与Native之间交换数据的传参,DeepEngine引擎会对JS传入的参数进行解析并封装,前端JS使用原生插件之前需要require原生插件对象。
找到UZApp工程中widget目录下的index.html,添加调用moduleDemo原生插件的showAlert方法的代码:
function showAlert() {
var demo = api.require('moduleDemo');
demo.showAlert({
msg: 'Hello App!'
},function(ret, err){
var msg = "点击了第" + ret.index + "个按钮";
api.toast({
msg: msg
});
});
}
原生插件包根目录必须以该原生插件的JS对象名命名,这里以moduleDemo为例,原生插件包内可能包含res_moduleDemo、target、framework等文件夹以及module.json。
目录解释:
NSString *path = [[NSBundle mainBundle].bundlePath stringByAppendingPathComponent:@"res_moduleDemo/1.png"];
target目录:存放编译生成的静态库文件、第三方Static类型的framework库(直接添加到工程就能正常使用的是Static类型的,而需要在工程Embedded Binaries处添加才能正常使用的是Dynamic类型的)、以及其它需要以Create groups方式添加到应用工程中使用的文件,如bundle束、图片等。若该目录下存在其它的文件夹,这些文件夹会以Create folder references的方式被加入到应用工程。
framework目录:(可选配置项)存放第三方Dynamic类型的framework库(需要在工程Embedded Binaries处添加才能正常使用的是Dynamic类型的)。
plugins目录:(可选配置项)存放应用扩展(App Extension)。可以将编译好的应用扩展包(后缀为.appex)放到该目录下,支持同时放置多个应用扩展包。注意:编译应用扩展使用的certificate需和云编译使用的certificate是同一个,否则会导致云编译失败。
localization目录:(可选配置项)放置多语言本地化文件,格式如图:
{
"name":"moduleDemo",
"class":"UZModuleDemo"
}
一个原生插件包里面也可以配置多个原生插件,如:
[{
"name":"moduleDemo",
"class":"UZModuleDemo"
},
{
"name":"moduleDemo1",
"class":"UZModuleDemo1"
}]
1、新建一个moduleDemo文件夹。
2、在moduleDemo文件夹里面创建一个module.json的文本文件,拷贝以下内容到module.json中,注意双引号一定要是英文状态下的。
{
"name":"moduleDemo",
"class":"UZModuleDemo"
}
3、在moduleDemo文件夹里面创建target文件夹,将静态库工程编译出来的libModuleDemo.a库拷贝到target目录下。注意.a库必须是真机环境的,并且需要支持armv7和arm64架构,可以在终端用以下命令查看.a库支持的架构:
lipo -info libModuleDemo.a
4、将moduleDemo文件夹压缩成moduleDemo.zip。
「自定义插件」页面,点击「上传自定义插件」按钮,在弹出自定义插件选项卡,输入相关插件信息并上传moduleDemo.zip文件然后「提交」。
保存成功后将在自定义插件列表会显示出该插件,然后点击「添加至本应用」,即可将自定义插件添加到当前应用中。
对于需要添加UIView类视图的接口,需要提供fixedOn参数,让前端JS传入frame的名字,然后将视图添加到该frame上面,同时还应该提供fixed参数,控制视图是否随着frame内容的移动而跟着移动。
UZModule类提供 - (BOOL)addSubview:(UIView )view fixedOn:(NSString)fixedOn fixed:(BOOL)fixed 方法,用于往指定的frame上面添加视图。
JS_METHOD(show:(UZModuleMethodContext *)context) {
NSDictionary *param = context.param;
NSString *fixedOn = [param stringValueForKey:@"fixedOn" defaultValue:nil];
BOOL fixed = [param boolValueForKey:@"fixed" defaultValue:YES];
[self addSubview:yourView fixedOn:fixedOn fixed:fixed];
}
同时UZModule提供controller属性来获取当前所在视图控制器,可通过该控制器对目标控制器进行push或者present操作。
// push
[self.viewController.navigationController pushViewController:controller animated:YES];
// present
[self.viewController presentViewController:controller animated:YES completion:nil];
为消除iOS和Android平台系统间文件路径的差异,YonBuilder移动开发为前端JS提供了fs://、widget://和cache://等虚拟文件路径协议,因此,原生插件在使用JS端传入的路径时需要调用UZModule里面的 - (NSString )getPathWithUZSchemeURL:(NSString)url 方法来转换成正确的绝对路径。
NSString *path = [paramDict stringValueForKey:@"path" defaultValue:nil];
if (path) {
NSString *fullPath = [self getPathWithUZSchemeURL:path];
}
部分原生插件可能要求开发者在config.xml里面配置信息,如第三方平台申请的key之类,配置如下:
<feature name="moduleDemo">
<param name="apiKey" value="123456" />
</feature>
那么在原生插件中通过UZModule中的 getFeatureByName: 方法获取配置信息。如果想在应用启动时就获取,则可以通过UZAppDelegate类的 getFeatureByName: 方法获取。
// 在原生插件类的实例方法中获取
NSDictionary *feature = [self getFeatureByName:@"moduleDemo"];
NSString *apiKey = [feature stringValueForKey:@"apiKey" defaultValue:nil];
// 在应用启动时获取(需要引入UZAppDelegate.h头文件)
NSDictionary *feature = [theApp getFeatureByName:@"moduleDemo"];
NSString *apiKey = [feature stringValueForKey:@"apiKey" defaultValue:nil];
如果原生插件需要在应用启动的时候就执行一些操作,可以在原生插件类中重载实现+ (void)onAppLaunch:(NSDictionary *)launchOptions方法,方法会在应用启动时被调用,如:
+ (void)onAppLaunch:(NSDictionary *)launchOptions {
NSDictionary *feature = [theApp getFeatureByName:@"moduleDemo"];
NSString *apiKey = [feature stringValueForKey:@"apiKey" defaultValue:nil];
}
实现该功能也可以使用以前旧的方式,该方式需要先进行配置,配置的方法不能带参数,因此无法获取应用启动时的参数信息。已经不推荐使用旧的方式。
1、首先需要在module.json里面配置launchClassMethod字段:
{
"name":"moduleDemo",
"class":"UZModuleDemo",
"launchClassMethod":"onAppLaunch"
}
2、在原生插件类中实现配置的方法。
+ (void)onAppLaunch {
}
一些功能需要通过应用程序代理方法才能实现,如获取推送信息、处理第三方应用回调等。
我们在UZAppDelegate中提供了 - (void)addAppHandle:(id
注意一定要在- (void)dispose方法里面调用 - (void)removeAppHandle:(id
如处理应用被第三方应用调起:
- (id)initWithUZWebView:(UZWebView *)webView_ {
if (self = [super initWithUZWebView:webView_]) {
[theApp addAppHandle:self];
}
return self;
}
- (void)dispose {
[theApp removeAppHandle:self];
}
#pragma mark - UIApplicationDelegate
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
//处理应用被三方应用调起
return YES;
}
同步方法是指在js中调用原生插件方法时直接返回结果,而不使用callback的方式返回结果。方法返回的类型包括NSDictionary、NSArray、NSString、NSNumber等。
通过 JS_METHOD_SYNC 宏来定义同步方法,参数类型为UZModuleMethodContext,可以通过context的param属性来获取js传入的参数。返回数据的时候,如果是BOOL、int等基本数据类型,需要用NSNumber对象进行包装。如:
JS_METHOD_SYNC(systemVersion:(UZModuleMethodContext *)context) {
NSString *version = [UIDevice currentDevice].systemVersion;
return version;
}
JS_METHOD_SYNC(applicationIconBadgeNumber:(UZModuleMethodContext *)context) {
NSInteger badgeNumber = [UIApplication sharedApplication].applicationIconBadgeNumber;
return @(badgeNumber);
}
js中调用示例:
var demo = api.require('moduleDemo');
var systemVersion = demo.systemVersion();
var badgeNumber = demo.applicationIconBadgeNumber();
实现同步方法也可以使用以前旧的方式,该方式需要先进行配置,已经不推荐使用旧的方式。
1、首先需要在module.json里面配置syncMethods字段:
{
"name":"moduleDemo",
"class":"UZModuleDemo",
"syncMethods":["systemVersion"]
}
2、在原生插件类中实现配置的方法:
- (NSString *)systemVersion:(NSDictionary *)param {
NSString *version = [UIDevice currentDevice].systemVersion;
return version;
}
使用Swift开发原生插件时,原生插件类需要继承自UZModule类,同时需要在类的前面加上@objc声明,例如:
@objc(UZModuleDemoSwift)
class UZModuleDemoSwift: UZModule {
}
方法声明时也需要在方法前面加上@objc,例如:
@objc func jsmethod_showAlert(_ context:UZModuleMethodContext) {
}
目前静态库工程不支持使用swift,所以直接将swift文件添加到UZApp主工程中测试,上传原生插件包的时候则将swift文件放到target目录下面。
开发者开发的原生插件务必遵守《原生插件审核规范_iOS》。