前言
距離我寫的上一篇文章 Weex從入門到超神(一) 已經過了挺久了(慚愧而不失禮貌的微笑),起初寫那篇文章的初衷是因為項目中使用到了 Weex ,所以準備對 Weex 做一個心得式的筆記,后來無意間發(fā)現(xiàn)簡書“霜神”已經對 Weex 寫過幾篇剖析比較深刻的文章,還有其他一些原因(懶),所以就沒有繼續(xù)寫下去。
最近由于Facebook的 BSD license,React 被前端社區(qū)的同學們推到了風口浪尖,React&RN、Vue&Weex 又成為了大家碼前碼后討論的話題。Apache 社區(qū)還因為 Facebook 的 BSD license,全面封殺使用了 BSD license 的開源項目,貌似一切都很精彩,迫于前端同(da)學(lao)的淫威還有社區(qū)的強烈譴責,上周 Facebook 終于認慫了,承諾這周將 React 以及 gayhub 上面的其他幾個項目的開源協(xié)議從 BSD 改成 MIT,下圖是我腦補的場景:

鑒于對于項目中使用 Weex 的一些經驗和心得,還是希望寫出來和大家一起分享。
應用層核心組件
Weex 運行時會先注入一段位于 pre-build 下的 native-bundle-main.js 代碼。不過在注入這段代碼之前會先注冊一些默認的 Component、Module和Handler,這就是 Weex 與 Native 應用層交互最核心的部分,可以理解為“組件”。其中 Component 是為了映射 Html 的一些標簽,Module 中是提供一些 Native 的一些方法供 Weex 調用,Handler 是一些協(xié)議的實現(xiàn)。

注冊完 Weex 默認的“組件” 之后,注入剛才那段 JS,這個時候 Vue 的標簽和動作才能被 Weex 所識別和轉換。
為了便于下文的描述和理解,我把 Native 這邊的 SDK 稱作 Weex,前端的 Vue 和 Weex 庫以及 Vue 編譯后的 js 統(tǒng)稱為 Vue
轉載請注明出處:來自LeonLei的博客http://www.gaoshilei.com
1. 組件:Component
目前 Weex 一共提供了26種 Component,比較常見的有 div、image、scroller... ,有些跟 html 標簽重名,有些是 Weex 自定義的。Weex 注冊的 Component 有兩種類型,一類是有{@"append":@"tree"}屬性的標簽,另一類是沒有{@"append":@"tree"}屬性的標簽。要搞清楚這兩類標簽有什么不同,我們就要看一下 Component 的注冊的源碼實現(xiàn)。
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
首先通過一個工廠類WXComponentFactory注冊 Component,
這個工廠類(單例)中管理了所有的 Component ,注冊的每一個 Component 都會用一個對應的
WXComponentConfig來保存標簽name、對應的class和屬性,最后由WXComponentFactory來統(tǒng)一管理這些WXComponentConfig
這一步同時注冊了 Component 中的 methods,關于 method 也有兩類,一類是包含wx_export_method_sync_前綴的同步方法,另一類是包含wx_export_method_前綴的異步方法(這兩種方法有什么不同,后面會有介紹)。在WXComponentConfig的父類WXInvocationConfig儲存了 Component 的方法map:
@property (nonatomic, strong) NSMutableDictionary *asyncMethods;
@property (nonatomic, strong) NSMutableDictionary *syncMethods;
然后再從WXComponentFactory拿到對應 Component 的方法列表字典,需要注意的是這里拿到的方法列表只是異步方法,得到的是這樣的字典:
{
methods = (
resetLoadmore
);
type = scroller;
}
不過大部分 Component 并沒有wx_export前綴的 method,所以很多這里拿到的方法都為空。
最后也是最關鍵的一步,要將 Component 注冊到WXBridgeContext中。
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
最后將 Component 注冊到了JSContext中,
還記得文章開頭介紹的
native-bundle-main.js嗎?這里的注冊調用了js中的registerComponents方法,這個 Component 與 Vue 就聯(lián)系起來了,在 Vue 就可以使用這個 Component。
并且從上面的這段代碼可以看出來,Component 的注冊操作是在 JSFramework 加載完成才會進行,如果native-bundle-main.js沒有加載完成,所有的 Component 的方法注冊操作都會被加到隊列中等待。其中的第二個參數(shù)args就是上面我們拿到的字典。不過有屬性的 和沒屬性的有點區(qū)別,有屬性的會將屬性添加到之前拿到的字典中作為args再去注冊。
要搞清楚這個屬性干嘛的,我們先看一下WXComponentManager中的相關源碼:
- (void)_recursivelyAddComponent:(NSDictionary *)componentData toSupercomponent:(WXComponent *)supercomponent atIndex:(NSInteger)index appendingInTree:(BOOL)appendingInTree {
...
BOOL appendTree = !appendingInTree && [component.attributes[@"append"] isEqualToString:@"tree"];
// if ancestor is appending tree, child should not be laid out again even it is appending tree.
for(NSDictionary *subcomponentData in subcomponentsData){
[self _recursivelyAddComponent:subcomponentData toSupercomponent:component atIndex:-1 appendingInTree:appendTree || appendingInTree];
}
if (appendTree) {
// If appending tree,force layout in case of too much tasks piling up in syncQueue
[self _layoutAndSyncUI];
}
}
這個方法是 Vue 頁面渲染時所調用的方法,這個方法會遞歸添加 Component,同時會向視圖中添加與 Component 相對應的 UIView。從代碼的后半部分可以看到,如果當前 Component 有{@"append":@"tree"}屬性并且它的父 Component 沒有這個屬性將會強制對頁面進行重新布局??梢钥吹竭@樣做是為了防止UI繪制任務太多堆積在一起影響同步隊列任務的執(zhí)行。
搞清楚了 Component 的注冊機制,下面重點扒一下 Component 的運行原理:Vue 標簽是如何加載以及渲染到視圖上的。
從剛才的注冊過程中發(fā)現(xiàn),最后一步是通過_jsBridge調用callJSMethod這個方法來注冊的,而且從WXBridgeContext中可以看到,這個_jsBridge就是WXJSCoreBridge的實例。WXJSCoreBridge可以認為是 Weex 與 Vue 進行通信的最底層的部分。在調用callJSMethod方法之前,_jsBridge向 JavaScriptCore 中注冊了很多全局 function,具體請看精簡后的源碼:
[_jsBridge registerCallNative:^NSInteger(NSString *instance, NSArray *tasks, NSString *callback) {
...
}];
[_jsBridge registerCallAddElement:^NSInteger(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index) {
...
}];
[_jsBridge registerCallCreateBody:^NSInteger(NSString *instanceId, NSDictionary *bodyData) {
...
}];
[_jsBridge registerCallRemoveElement:^NSInteger(NSString *instanceId, NSString *ref) {
...
}];
[_jsBridge registerCallMoveElement:^NSInteger(NSString *instanceId,NSString *ref,NSString *parentRef,NSInteger index) {
...
}];
[_jsBridge registerCallUpdateAttrs:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *attrsData) {
...
}];
[_jsBridge registerCallUpdateStyle:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *stylesData) {
...
}];
[_jsBridge registerCallAddEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
...
}];
[_jsBridge registerCallRemoveEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
...
}];
[_jsBridge registerCallCreateFinish:^NSInteger(NSString *instanceId) {
...
}];
[_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
...
}];
[_jsBridge registerCallNativeComponent:^void(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options) {
...
}];
從這些方法名看,大多數(shù)都是一些與 Dom 更新相關的方法,我們在WXJSCoreBridge中更細致的看一下是怎么實現(xiàn)的:
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) {
NSString *instanceIdString = [instanceId toString];
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
[WXTracingManager startTracingWithInstanceId:instanceIdString ref:componentData[@"ref"] className:nil name:WXTJSCall phase:WXTracingBegin functionName:@"addElement" options:nil];
WXLogDebug(@"callAddElement...%@, %@, %@, %ld", instanceIdString, parentRef, componentData, (long)insertIndex);
return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
};
_jsContext[@"callAddElement"] = callAddElementBlock;
}
這是一個更新 Dom 添加 UIView 的方法,這里需要把 Native 的方法暴露給 JS 調用。但是有一個問題:
OC 的方法參數(shù)格式和 JS 的不一樣,不能直接提供給 JS 調用。
所以這里用了兩個 Block 嵌套的方式,在 JS 中調用方法時會先 invoke 里層的 callAddElementBlock,這層 Block 將 JS 傳進來的參數(shù)轉換成 OC 的參數(shù)格式,再執(zhí)行 callAddElement 并返回一個 JSValue 給 JS,callAddElement Block中是在WXComponentManager中完成的關于 Component 的一些操作,這在上面介紹 Component 包含 tree屬性問題時已經介紹過了。
至此,簡單來說就是:Weex 的頁面渲染是通過先向 JSCore 注入方法,Vue 加載完成就可以調用這些方法并傳入相應的參數(shù)完成 Component 的渲染和視圖的更新。
要注意,每一個 WXSDKInstance 對應一個 Vue 頁面,Vue 加載之前就會創(chuàng)建對應的 WXSDKInstance,所有的 Component 都繼承自WXComponent,他們的初始化方法都是
-(instancetype)initWithRef:(NSString *)ref
type:(NSString *)type
styles:(NSDictionary *)styles
attributes:(NSDictionary *)attributes
events:(NSArray *)events
weexInstance:(WXSDKInstance *)weexInstance
這個方法會在 JS 調用callCreateBody時被 invoke。
2. 組件:Module
Module 注冊流程和 Component 基本一致,首先通過WXModuleFactory注冊 Module
- (NSString *)_registerModule:(NSString *)name withClass:(Class)clazz
{
WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
[_moduleLock lock];
//allow to register module with the same name;
WXModuleConfig *config = [[WXModuleConfig alloc] init];
config.name = name;
config.clazz = NSStringFromClass(clazz);
[config registerMethods];
[_moduleMap setValue:config forKey:name];
[_moduleLock unlock];
return name;
}
注冊 Moudle 的registerMethods方法與注冊 Component 是一樣的,都是將方法注冊到WXInvocationConfig中,wx_export_method_sync_前綴的同步方法注冊到 syncMethods 中,wx_export_method_前綴的異步方法注冊到 asyncMethods 中。再將 Moudle 的同步和異步方法取出來調用registerComponents注入到JSContext中
{
dom = (
addEventListener,
removeAllEventListeners,
addEvent,
removeElement,
getComponentRect,
updateFinish,
scrollToElement,
addRule,
updateAttrs,
addElement,
createFinish,
createBody,
updateStyle,
removeEvent,
refreshFinish,
moveElement
);
}
這是WXDomModule中所有的方法,Moudle 中的方法注冊比 Component 更有意義,因為 Moudle 中基本上都是暴露給 Vue 調用的 Native 方法。
接下來我們來看一下 Moudle 的方法如何被調用以及 syncMethods 和 asyncMethods 有什么不同。
在前面的jsBridge懶加載中,有一個注冊方法是跟 Moudle 中方法有關的,Moudle 中的方法會在這個注冊方法的回調中被 invoke,換言之,Vue 調用 Moudle 中的方法會在這個回調中被喚起
[_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
if (!instance) {
WXLogInfo(@"instance not found for callNativeModule:%@.%@, maybe already destroyed", moduleName, methodName);
return nil;
}
WXModuleMethod *method = [[WXModuleMethod alloc] initWithModuleName:moduleName methodName:methodName arguments:arguments instance:instance];
if(![moduleName isEqualToString:@"dom"] && instance.needPrerender){
[WXPrerenderManager storePrerenderModuleTasks:method forUrl:instance.scriptURL.absoluteString];
return nil;
}
return [method invoke];
}];
在WXModuleMethod中可以看到-(NSInvocation *)invoke這個方法,Moudle 中的方法將會通過這個方法被 invoke
...
Class moduleClass = [WXModuleFactory classWithModuleName:_moduleName];
if (!moduleClass) {
NSString *errorMessage = [NSString stringWithFormat:@"Module:%@ doesn't exist, maybe it has not been registered", _moduleName];
WX_MONITOR_FAIL(WXMTJSBridge, WX_ERR_INVOKE_NATIVE, errorMessage);
return nil;
}
id<WXModuleProtocol> moduleInstance = [self.instance moduleForClass:moduleClass];
WXAssert(moduleInstance, @"No instance found for module name:%@, class:%@", _moduleName, moduleClass);
BOOL isSync = NO;
SEL selector = [WXModuleFactory selectorWithModuleName:self.moduleName methodName:self.methodName isSync:&isSync];
if (![moduleInstance respondsToSelector:selector]) {
// if not implement the selector, then dispatch default module method
if ([self.methodName isEqualToString:@"addEventListener"]) {
[self.instance _addModuleEventObserversWithModuleMethod:self];
} else if ([self.methodName isEqualToString:@"removeAllEventListeners"]) {
[self.instance _removeModuleEventObserverWithModuleMethod:self];
} else {
NSString *errorMessage = [NSString stringWithFormat:@"method:%@ for module:%@ doesn't exist, maybe it has not been registered", self.methodName, _moduleName];
WX_MONITOR_FAIL(WXMTJSBridge, WX_ERR_INVOKE_NATIVE, errorMessage);
}
return nil;
}
[self commitModuleInvoke];
NSInvocation *invocation = [self invocationWithTarget:moduleInstance selector:selector];
if (isSync) {
[invocation invoke];
return invocation;
} else {
[self _dispatchInvocation:invocation moduleInstance:moduleInstance];
return nil;
}
先通過 WXModuleFactory 拿到對應的方法 Selector,然后再拿到這個方法對應的 NSInvocation ,最后 invoke 這個 NSInvocation。對于 syncMethods 和 asyncMethods 有兩種 invoke 方式。如果是 syncMethod 會直接 invoke ,如果是 asyncMethod,會將它派發(fā)到某個指定的線程中進行 invoke,這樣做的好處是不會阻塞當前線程。到這里 Moudle 的大概的運行原理都清楚了,不過還有一個問題,Moudle 的方法是怎么暴露給 Vue 的呢?
在 Moudle 中我們通過 Weex 提供的宏可以將方法暴露出來:
#define WX_EXPORT_METHOD(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_)
#define WX_EXPORT_METHOD_SYNC(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_sync_)
分別提供了 syncMethod 和 asyncMethod 的宏,展開其實是這樣的
#define WX_EXPORT_METHOD_INTERNAL(method, token) \
+ (NSString *)WX_CONCAT_WRAPPER(token, __LINE__) { \
return NSStringFromSelector(method); \
}
這里會自動將方法名和當前的行數(shù)拼成一個新的方法名,這樣做的好處是可以保證方法的唯一性,例如 WXDomModule 中的 createBody: 方法利用宏暴露出來,最終展開形式是這樣的
+ (NSString *)wx_export_method_40 { \
return NSStringFromSelector(createBody:); \
}
在WXInvocationConfig中調用- (void)registerMethods注冊方法的時候,首先拿到當前 class 中所有的類方法(宏包裝成的方法,并不是實際要注冊的方法),然后通過判斷有無wx_export_method_sync_前綴和wx_export_method_前綴來判斷是否為暴露的方法,然后再調用該類方法,獲得最終的實例方法字符串
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
拿到需要注冊的實例方法字符串,再將方法字符串注冊到WXInvocationConfig的對應方法 map 中。
3. 組件:Handlers
Handlers 的注冊和使用非常簡單,直接將對應的 class 注冊到 WXHandlerFactory map中
[[WXHandlerFactory sharedInstance].handlers setObject:handler forKey:NSStringFromProtocol(protocol)];
需要使用的時候也非常簡單粗暴,通過WXHandlerFactory的方法和相應的 protocol
+ (id)handlerForProtocol:(Protocol *)
{
id handler = [[WXHandlerFactory sharedInstance].handlers objectForKey:NSStringFromProtocol(protocol)];
return handler;
}
直接拿出即可。