扩展 IOS 的功能
注意:Weex 所有暴露给 JS 的内置 module 或 component API 都是安全和可控的,它们不会去访问系统的私有 API,也不会去做任何 runtime 上的 hack 更不会去改变应用原有的功能定位。
如果需要扩展自定义的 module 或者 component ,一定注意不要将 OC 的 runtime 暴露给 JS ,不要将一些诸如dlopen()
,dlsym()
,respondsToSelector:
,performSelector:
,method_exchangeImplementations()
的动态和不可控的方法暴露给JS,也不要将系统的私有API暴露给JS。否则将可能面临苹果上架审核问题。
在这篇文档里,我们使用 Objective-C 来扩展 Weex 功能,如果想使用 Swift,可以参考 使用 Swift 扩展。
JSEnv 扩展
接口
NSDictionary *options = @{@"testVlaue":@"hello"};
[_instance renderWithURL:url options:options data:nil];
使用
var value = weex.config.testValue;
console.log(value);
自定义 module
自定义 module,需要让自己的 class 遵循 WXModuleProtocol
这个protocol,通过 WX_EXPORT_METHOD
这个宏暴露出需要透出到 JavaScript
调用的方法,再向 WeexSDK 注册 module,就可以完成一个简单 module 的自定义。
- 新建一个基类为 NSObject 的 class
WXCustomEventModule
,让该类遵循WXModuleProtocol
的协议。 - 添加打印的方法,通过
WX_EXPORT_METHOD
暴露该方法。 - 在初始化完成 Weex SDK 之后注册该 module。
到此,我们已经完成了一个简单的 module 方法的封装,在 JS 里使用方法如下:
weex.requireModule("event").showParams("hello Weex")
Module 进阶
关于 Module 和 Module 方法的执行特性(同步、异步;执行线程),需要了解:
weexInstance
在一个 Weex 页面中,默认 WXSDKInstance 的实例持有多个 module 的实例, 而 Module 的实例是是没有对 WXSDKInstance 做持有的,在自定义的 module 中添加@synthesize weexInstance
,module 实例可以对持有它本身的 WXSDKInstance 实例做一个弱引用,通过 weexInstance 可以拿到调用该 module 的页面的一些信息。targetExecuteThread
Module 方法默认会在 UI 线程(iOS 主线程)中被调用,建议不要在这做太多耗时的任务。如果你的任务不需要在 UI 线程执行或需要在特定线程执行,需要实现WXModuleProtocol
中的- (NSThread *)
的方法,并返回你希望方法执行所在的线程。WXModuleKeepAliveCallback
Module 支持返回值给 JavaScript 中的回调,回调的类型是WXModuleKeepAliveCallback
。回调的参数可以是String
或者Map
。该 block 第一个参数为回调给 JavaScript 的数据,第二参数是一个 BOOL 值,表示该回调执行完成之后是否要被清除。JavaScript 每次调用都会产生一个回调,但是对于单独一次调用,是否要在完成该调用之后清除掉回调函数 id 就由这个选项控制,如非特殊场景,建议传 NO。WX_EXPORT_METHOD_SYNC
使用WX_EXPORT_METHOD
暴露到前端的方法都是异步方法(获得结果需要通过回调函数获得)。如果期望获得同步调用结果,可以使用WX_EXPORT_METHOD_SYNC
声明同步的 Module 方法。
扩展组件
- 新建一个基类为
WXComponent
的类。如果这个类里什么代码也不写,它和默认的的div
组件能力是一致的。 - 覆盖
WXComponent
中的生命周期方法
loadView
一个组件默认对应于一个 view,如果未覆盖loadView
提供自定义 view,会让WXComponent
基类创建WXView
。WXView
继承自UIView
。比如我们要实现一个组件支持地图功能,我们可以返回系统的MKMapView
。
- (UIView *)loadView {
return [MKMapView new];
}
viewDidLoad
对组件 view 需要做一些配置,比如设置 delegate,可以在viewDidLoad
生命周期做。如果当前 view 没有添加 subview 的话,不要设置 view 的 frame,WeexSDK 会根据 style 进行排版后设置。
- (void)viewDidLoad {
((MKMapView*)self.view).delegate = self;
}
- 注册组件
[WXSDKEngine registerComponent:@"map" withClass:[WXMapComponent class]];
- 在前端代码中使用新组件
<template>
<div>
<map style="width:200px;height:200px"></map>
</div>
</template>
自定义事件
- 对于每个组件,WeexSDK 默认提供了一些事件能力,如点击等。假如想给我们的地图组件提供
mapLoaded
事件。
<template>
<div>
<map style="width:200px;height:200px" @mapLoaded="onMapLoaded"></map>
</div>
</template>
<script>
export default {
methods: {
onMapLoaded:function(e) {
console.log("map loaded"+JSON.stringify(e))
}
}
}
</script>
- 覆盖组件生命周期方法,记录事件是否需要处理
我们需要额外添加一个 BOOL
成员 mapLoaded
用来记录该事件是否生效。
- (void)addEvent:(NSString *)eventName {
if ([eventName isEqualToString:@"mapLoaded"]) {
_mapLoaded = YES;
}
}
- (void)removeEvent:(NSString *)eventName {
if ([eventName isEqualToString:@"mapLoaded"]) {
_mapLoaded = NO;
}
}
- 给前端发送事件
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView {
if (_mapLoaded) {
[self fireEvent:@"mapLoaded" params:@{@"customKey":@"customValue"} domChanges:nil];
}
}
自定义属性
给我们的地图组件添加一个新的属性 showTraffic
。在前端代码里可以控制组件是否显示车流情况。
<template>
<div>
<map style="width:200px;height:200px" showTraffic="true"></map>
</div>
</template>
- 覆盖组件初始化方法
initWithRef...
给组件添加一个成员变量记录showTraffic
属性的值,并在 init 方法中初始化。
- (instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance {
if(self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
if (attributes[@"showsTraffic"]) {
_showsTraffic = [WXConvert BOOL: attributes[@"showsTraffic"]];
}
}
return self;
}
- 在生命期事件中记得将属性值同步给地图控件
- (void)viewDidLoad {
((MKMapView*)self.view).showsTraffic = _showsTraffic;
}
- 当属性更新时,同步给地图控件
- (void)updateAttributes:(NSDictionary *)attributes {
if (attributes[@"showsTraffic"]) {
_showsTraffic = [WXConvert BOOL: attributes[@"showsTraffic"]];
((MKMapView*)self.view).showsTraffic = _showsTraffic;
}
}
更多的组件生命期方法
组件是由 Weex 管理的,比如创建、布局、渲染、销毁。Weex 组件的生命周期方法都是可以重写的,你可以在这些生命周期中去做自己的事情。
方法 | 描述 |
---|---|
initWithRef:type:… | 用给定的属性初始化一个component. |
layoutDidFinish | 在component完成布局时候会调用. |
loadView | 创建component管理的view. |
viewWillLoad | 在component的view加载之前会调用. |
viewDidLoad | 在component的view加载完之后调用. |
viewWillUnload | 在component的view被释放之前调用. |
viewDidUnload | 在component的view被释放之后调用. |
updateStyles: | 在component的style更新时候调用. |
updateAttributes: | 在component的attribute更新时候调用. |
addEvent: | 给component添加event的时候调用. |
removeEvent: | 在event移除的时候调用. |
给组件添加方法
在组件代码中使用宏 WX_EXPORT_METHOD
声明组件方法供前端调用。
@implementation WXMyComponent
WX_EXPORT_METHOD(@selector(focus))
- (instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance {
if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
// handle your attributes
// handle your styles
}
return self;
}
- (void)focus {
NSLog(@"you got it");
}
@end
在 JS 中调用 focus
方法。
<template>
<mycomponent ref='mycomponent'></mycomponent>
</template>
<script>
module.exports = {
created: function() {
this.$refs.mycomponent.focus();
}
}
</script>
获取组件的 CSS 样式
- 在 0.19 版本之前,Weex 使用 Yoga 排版引擎,可以通过访问 WXComponent 的 cssNode 属性获取。例如:
self.cssNode->style.flex = 1.0;
float height = self.cssNode->style.dimensions[CSS_HEIGHT];
从 0.19 版本开始,Weex 使用自已的排版引擎,是 C++ 代码。获取 CSS 属性可以从 WXComponent 的 styles 字典(NSDictionary)里获取,也可以访问 WXComponent 的 flexCssNode 属性,其类型为 C++ 类 WeexCore::WXCoreLayoutNode。注意,需要源文件为 .mm 类型。
从 0.20 版本开始,iOS 接入 WeexCore,排版引擎也沉入 WeexCore层,并且 CSS 相关属性不再上传给 WXComponent 对象,WXComponent 对象的 styles 字典里只有非 CSS 样式。我们的目的是上层 UI 组件只需要关心排版引擎生成的最终坐标即可,不需要关心前端标记的 CSS 属性。如果仍然需要获取 CSS 样式,可以在 .mm 文件中直接访问 flexCssNode,或通过 WXComponent+Layout.h 文件中提供的扩展方法。
/**
* @abstract Get css style value for key. The key should be of CSS standard form.
* This method is for convenience use in C/ObjC environment. And if you want to
* retrieve all style values or in C++, you could use flexCssNode directly.
*
* Thread usage:
* This method should be invoked in component thread by WXPerformBlockOnComponentThread.
* Note that all initWithRef methods of WXComponent and its subclasses are performed in
* component thread by default. Therefore you can call this method directly in initWithRef.
*
* Supported keys:
* width, height, min-width, min-height, max-width, max-height,
* margin-(left/right/top/bottom)
* padding-(left/right/top/bottom)
* border-(left/right/top/bottom)-width
* left, right, top, bottom
* flex-grow
*/
- (float)getCssStyleValueForKey:(NSString *)key;
// Other methods which should also be used in component thread.
- (WXCoreFlexDirection)getCssStyleFlexDirection;
- (WXCoreFlexWrap)getCssStyleFlexWrap;
- (WXCoreJustifyContent)getCssStyleJustifyContent;
- (WXCoreAlignItems)getCssStyleAlignItems;
- (WXCoreAlignSelf)getCssStyleAlignSelf;
- (WXCorePositionType)getCssStylePositionType;
- (WXCoreDirection)getCssDirection;
自定义 handler
Handler 的作用是什么?比如 WeexSDK 目前没有提供图片下载的能力,而是在 WXImgLoaderProtocol
定义了一些获取图片的接口。Image 组件正是通过 WXImgLoaderProtocol
获得并展示图片,开发者可以实现该 protocol 中的接口方法,这样 image
标签才能正常展示图片。
开发者也可以定义自己的 protocol
和对应的实现来使用 handler
机制。常见的一个场景就是在每个 App 中需要特定的 Handler 来实现一些公共的能力。
- 新建基类为 NSObject 的类,并实现
WXImgLoaderProtocol
协议,实现WXImgLoaderProtocol
的方法。 下面加载图片的逻辑需要依赖 SDWebImage,你也可以不依赖 SDWebimage 使用自己的方式加载对应 URL 图片。
@implementation WXImgLoaderDefaultImpl
- (id<WXImageOperationProtocol>)downloadImageWithURL:(NSString *)url imageFrame:(CGRect)imageFrame userInfo:(NSDictionary *)userInfo completed:(void(^)(UIImage *image, NSError *error, BOOL finished))completedBlock {
if ([url hasPrefix:@"//"]) {
url = [@"http:" stringByAppendingString:url];
}
return (id<WXImageOperationProtocol>)[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:url] options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (completedBlock) {
completedBlock(image, error, finished);
}
}];
}
@end
- 注册
[WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)];
- 使用
id<WXImgLoaderProtocol> imageLoader = [WXSDKEngine handlerForProtocol:@protocol(WXImgLoaderProtocol)];
[iamgeLoader downloadImageWithURL:imageURl imageFrame:frame userInfo:customParam completed:^(UIImage *image, NSError *error, BOOL finished) {
}];