上一篇文章講到了混合應用簡單的發展史,本文以Weex
爲例分析一下混合應用,本文並不是是介紹Weex
是怎麼使用的,若是想要了解怎麼使用,不如瞭解一下 Eros 的解決方案,主要想剖析一下Weex
的原理,瞭解Weex
的運行機制。javascript
首先想聊一聊咱們爲何選擇Weex
。上一篇文章結尾對Weex
和ReactNative
進行了簡要的分析,在咱們作技術選型時大環境下RN
無論從哪方面來講都是一個更好的方案,更多的對比能夠去 weex&ReactNative對比 看看,在作技術選型的時候也在不斷的問,爲何?最後大概從下面幾個方面獲得了一個相對好的選擇。css
首先確定須要看看優缺點,優勢用來判斷本身的場景適不適合作這個技術,缺點來看本身的場景會不會被限制住,有沒有辦法解決和繞開。html
優勢:前端
- js 能寫業務,跨平臺,熱更新
- Weex 能用 Vue 的 framework,貼近咱們的技術棧
- Weex 比 RN 更輕量,能夠分包,每一個頁面一個實例性能更好
- Weex 解決了 RN 已經存在的一些問題,在 RN 的基礎上進行開發
- 有良好的擴展性,比較好擴展新的 Component 和 Module
缺點:vue
- 文檔不全,資料少,社區幾乎等於沒有,issue 堆積,後臺 issue 的方式改到了 JIRA 上,不少開發者都不瞭解
- bug 多,不穩定,遇到屢次斷崖式更新
- Component 和 Module 不足以覆蓋功能
其實總結起來就是起步晚的國產貨,優勢就不贅述了。主要看缺點會不會限制住業務場景,有沒有對應的解決方案。java
相關資料比較少,好在能看到源碼,有了源碼多花點時間琢磨,確定是能繼續下去的,順着源碼看過去,文檔不全的問題也解決了,主要是發現了Weex
提供了很是多文檔上沒有寫的好屬性和方法。node
項目起步比較晚,bug
比較多,更新也是斷崖式的,咱們最後採用源碼集成的方法,發現有bug
就修源碼,並給官方提PR
,咱們團隊提的不少PR
也被官方採納,主要仍是每次版本更新比較浪費時間,一方面要看更新日誌,還要對源碼進行diff
,若是官方已經修復了就刪除咱們本身的補丁。這塊確實是會浪費時間一點,可是RN
想要本身擴展也是須要經歷這個陣痛的。android
提供的Component
和Module
不足以完成業務需求,固然官方也提供了擴展對應插件化的方式,嘗試擴展了幾個插件具有原生知識擴展起來也比較快,而且咱們一開始就決定儘可能少用官方的Module
,儘可能Module
都由咱們的客戶端本身擴展,一方面不會受到官方的Module bug
或者不向下兼容時的影響,另外一方面在擴展原生Module
的同時能瞭解其機制,還能讓擴展的Module
都配合咱們的業務。webpack
咱們主要的技術棧是圍繞着Vue
創建的,本身作了統一的腳手架,已經適配了後臺系統、微信公衆號、小程序、自助機等多端的項目,就差APP
的解決方案了,若是能用Vue
的基礎去接入,就完善了整個前端技術鏈,配合腳手架和Vue
的語法基礎項目間的切換成本就會很低,開發效率會很高。ios
基於Vue
的技術棧,讓咱們寫業務的同窗能很快適應,拆分組件,widget
插件化,mixins
這些相關的使用都能直接用上,剩下須要學習的就是Weex
的Component
和Module
的使用及css
的支持性,咱們腳手架接入以後也直接支持sass/less/styule
,整個過程讓新同窗上手,半天的時候見能搭建出一個完整的demo
頁面,上手開發很快。整體來講,成本對於咱們來講是一個大的優點
上圖是咱們經過改進最後給出的 Eros 開發的方案,以腳手架爲核心的開發模式。
開發體驗基於Vue
的方式,各類語法都已經在腳手架那層抹平了,開發起來和以前的開發模式基本一致,開發調試的方式Weex
提供了獨立的模塊支持,瞭解原理以後,咱們很快作了保存即刷新的功能,加上自己Weex debug
提供的debug
頁面,js
也能進行調試,客戶端也支持了日誌輸出,開發體驗總體來看還比較流暢,確實是不如web
開發那麼天然,可是咱們經過對腳手架的改造,對客戶端支持熱刷新功能,及原生提供的一些工具,大大的改善了開發體驗。
用戶體驗方面總體性能對比RN
有了提升,站在RN
的肩膀上,確實解決了不少性能的問題,首次的白屏時間,咱們採用的是內置包,而且配合咱們的熱更新機制,是能保證客戶端打開的時候,必定是有對應的內容的,不須要額外去加載資源,白屏時間也有了保證。頁面切換的時候咱們採用多頁面的方式去實現Weex
,配合咱們本身擴展的路由機制每一個頁面是一個單獨的Weex
實例,因此每一個頁面單獨渲染的性能和效率要更好,而且咱們也一直在作預加載的方案,雖說對於性能改善的效果不是很明顯,可是每一小步都是能夠減小頁面間切換的白屏時間的。
Weex
本身自己就作了不少性能監控,只須要對性能數據接入咱們的監控系統,就能展現出對應的性能數據,目前從監控效果上來看確實實現了Weex
對性能的承諾。
容災處理用於處理jsBundle
訪問失敗的狀況,Weex
本身具有容災處理的方案,須要開發者本身作改造進行降級處理,展現頁⾯面時,客戶端會加載對應若是客戶端加載js bundle
失敗能夠啓用webView
訪問,展現HTML
端,可是體驗會很是很差,咱們採用內置包 + 熱更新的機制,保證咱們不會出現包解析失敗或者訪問不到的問題,若是發佈的包有問題,能夠緊急再發布,用戶立馬會接收到更新,而且根據配置告知用戶是否立馬更新,想要作的更好,能夠保存一個穩定版本的包在用戶手機中,遇到解析錯誤崩潰的問題,當即啓用穩定版本的內置包,可是這樣會致使包比較大,若是須要穩定的容災處理能夠考慮這樣去實現。
在完成了方案調研和簡單的demo
測試,咱們就開始落地,圍繞的Weex
也作了很是多的周邊環境的建設,好比現有腳手架的改造以支持Weex
的開發、熱更新機制如何構建、客戶端底層須要哪些支持、如何作擴展能與源碼進行解耦等等。
仍是說回正題,接下來介紹一下Weex
總體的架構。
從上面這個圖能夠看出Weex
總體的運行原理,這裏對流程作一個大概的介紹,後面每一步都會有詳細的介紹。
Weex
提供不一樣的framework
解析,能夠用.we
和.vue
文件寫業務,而後經過webpack
進行打包編譯生成js bundle
,編譯過程當中主要是用了weex
相關的loader
,Eros 對打包好的js bundle
生成了zip
包,還會生成差分包的邏輯。無論生成的是什麼文件,最後都是將js bundle
部署到服務器或者CDN
節點上。
客戶端啓動時發現引入了Weex sdk
,首先會初始化環境及一些監控,接着會運行本地的main.js
即js framework
,js framework
會初始化一些環境,當js framework
和客戶端都準備好以後,就開始等待客戶端何時展現頁面。
當須要展現頁面時,客戶端會初始化Weex
實例,就是WXSDKInstance
,Weex
實例會加載對應的js bundle
文件,將整個js bundle
文件當成一個字符串傳給js framework
,還會傳遞一些環境參數。js framework
開始在JavaScript Core
中執行js bundle
,將js bundle
執行翻譯成virtual DOM
,準備好數據雙綁,同時將vDOM
進行深度遍歷解析成vNode
,對應成一個個的渲染指令經過js Core
傳遞給客戶端。
js framework
調用Weex SDK
初始化時準備好的callNative
、addElement
等方法,將指令傳遞給 native,找到指令對應的Weex Component
執行渲染繪製,每渲染一個組件展現一個,Weex
性能瓶頸就是來自於逐個傳遞組件的過程,調用module
要稍微複雜一些,後面會詳解,事件綁定後面也會詳解。至此一個頁面就展現出來了。
上面咱們分析了大概的Weex
架構,也簡單介紹了一下運行起來的流程,接下來咱們基於 Eros 的源碼來詳細看一下每一步是如何進行的,Eros 是基於Weex
的二次封裝,客戶端運行的第一個部分就是初始化Weex
的sdk
。
初始化Weex sdk
主要完成下面四個事情:
- 關鍵節點記錄監控信息
- 初始化 SDK 環境,加載並運行 js framework
- 註冊 Components、Modules、Handlers
- 若是是在開發環境初始化模擬器嘗試鏈接本地 server
Eros 在Weex
的基礎上作了不少擴展,Weex
的主要流程就是上面一些,Eros 主要的代碼流程就是下面這樣的。
+ (void)configDefaultData
{
/* 啓動網絡變化監控 */
AFNetworkReachabilityManager *reachability = [AFNetworkReachabilityManager sharedManager];
[reachability startMonitoring];
/** 初始化Weex */
[BMConfigManager initWeexSDK];
BMPlatformModel *platformInfo = TK_PlatformInfo();
/** 設置sdimage減少內存佔用 */
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];
[[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO];
/** 設置統一請求url */
[[YTKNetworkConfig sharedConfig] setBaseUrl:platformInfo.url.request];
[[YTKNetworkConfig sharedConfig] setCdnUrl:platformInfo.url.image];
/** 應用最新js資源文件 */
[[BMResourceManager sharedInstance] compareVersion];
/** 初始化數據庫 */
[[BMDB DB] configDB];
/** 設置 HUD */
[BMConfigManager configProgressHUD];
/* 監聽截屏事件 */
// [[BMScreenshotEventManager shareInstance] monitorScreenshotEvent];
}
複製代碼
Weex
其中一個優勢就是自帶監控,本身會記錄一下簡單的性能指標,好比初始化SDK
時間,請求成功和失敗,js
報錯這些信息,都會自動記錄到WXMonitor
中。
Weex
將錯誤分紅兩類,一類是global
,一類是instance
。在iOS
中WXSDKInstance
初始化以前,全部的全局的global
操做都會放在WXMonitor
的globalPerformanceDict
中。當WXSDKInstance
初始化以後,即 WXPerformanceTag中
instance如下的全部操做都會放在
instance.performanceDict`中。
global
的監控
- SDKINITTIME:SDK 初始化監控
- SDKINITINVOKETIME:SDK 初始化 invoke 監控
- JSLIBINITTIME:js 資源初始化監控
instance
監控
- NETWORKTIME:網絡請求監控
- COMMUNICATETIME:交互事件監控
- FIRSETSCREENJSFEXECUTETIME:首屏 js 加載監控
- SCREENRENDERTIME:首屏渲染時間監控
- TOTALTIME:渲染總時間
- JSTEMPLATESIZE:js 模板大小
若是想要接入本身的監控系統,閱讀一下WXMonitor
相關的代碼,能夠採用一些AOP
的模式將錯誤記錄到本身的監控中,這部分代碼不是運行重點有興趣的同窗就本身研究吧。
這是最主要的一部初始化工做,經過 [BMConfigManager initWeexSDK];Eros 也是在這個時機注入擴展。咱們將咱們的擴展放在registerBmComponents
、registerBmModules
、registerBmHandlers
這三個方法中,而後統一注入,避免與Weex
自己的代碼耦合太深。
+ (void)initWeexSDK
{
[WXSDKEngine initSDKEnvironment];
[BMConfigManager registerBmHandlers];
[BMConfigManager registerBmComponents];
[BMConfigManager registerBmModules];
#ifdef DEBUG
[WXDebugTool setDebug:YES];
[WXLog setLogLevel:WeexLogLevelLog];
[[BMDebugManager shareInstance] show];
// [[ATManager shareInstance] show];
#else
[WXDebugTool setDebug:NO];
[WXLog setLogLevel:WeexLogLevelError];
#endif
}
複製代碼
下面是咱們部分的擴展,詳細的擴展能夠看看咱們的源碼,爲了與官方的源碼集成擴展解耦咱們將咱們的注入時機放在了Weex initSDKEnvironment
以後。
// 擴展 Component
+ (void)registerBmComponents
{
NSDictionary *components = @{
@"bmmask": NSStringFromClass([BMMaskComponent class]),
@"bmpop": NSStringFromClass([BMPopupComponent class])
...
};
for (NSString *componentName in components) {
[WXSDKEngine registerComponent:componentName withClass:NSClassFromString([components valueForKey:componentName])];
}
}
// 擴展 Moudles
+ (void)registerBmModules
{
NSDictionary *modules = @{
@"bmRouter" : NSStringFromClass([BMRouterModule class]),
@"bmAxios": NSStringFromClass([BMAxiosNetworkModule class])
...
};
for (NSString *moduleName in modules.allKeys) {
[WXSDKEngine registerModule:moduleName withClass:NSClassFromString([modules valueForKey:moduleName])];
}
}
// 擴展 Handlers
+ (void)registerBmHandlers
{
[WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)];
[WXSDKEngine registerHandler:[WXBMNetworkDefaultlpml new] withProtocol:@protocol(WXResourceRequestHandler)];
...
}
複製代碼
初始化SDK
就是執行WXSDKEngine
這個文件的內容,最主要註冊當前的Components
、Modules
、handlers
。
+ (void)registerDefaults
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self _registerDefaultComponents];
[self _registerDefaultModules];
[self _registerDefaultHandlers];
});
}
複製代碼
小白同窗可能會比較疑惑爲何Weex
只支持一些特定的標籤,不是HTML
裏的全部標籤都支持,首先標籤的解析確定須要與原生有一個對應關係,這些對應關係的標籤才能支持。這個對應關係從哪兒來,就是首先 Weex 會初始化一些Components
,首先要告訴Weex SDK
我支持哪些標籤,這其中就包括Weex
提供的一些標籤,和咱們經過Weex Component
的擴展方法擴展出來的標籤。
咱們來看看Components
是怎麼註冊的,就是上面方法中的_registerDefaultComponents
,下面是這些方法的部分代碼
// WXSDKEngine.m
+ (void)_registerDefaultComponents
{
[self registerComponent:@"container" withClass:NSClassFromString(@"WXDivComponent") withProperties:nil];
[self registerComponent:@"cell-slot" withClass:NSClassFromString(@"WXCellSlotComponent") withProperties: @{@"append":@"tree", @"isTemplate":@YES}];
...
}
複製代碼
上面方法中二者有一些差異,withProperties
參數不一樣,若是是帶有@{@"append":@"tree"}
,先渲染子節點;isTemplate
是個boolean
值,若是爲true
,就會將該標籤下的全部子模板所有傳遞過去。後面也會詳細分析這兩個參數的做用
在初始化WeexSDK
的時候,Weex
會調用_registerDefaultComponents
方法將Weex
官方擴展好的組件進行註冊;繼續看一下registerComponent:withClass:withProperties:
方法
+ (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties
{
if (!name || !clazz) {
return;
}
WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !");
// 註冊組件的方法
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
// 遍歷出組件的異步方法
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
// 將組件放到 bridge 中,準備註冊到 js framework 中。
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
}
複製代碼
首先看一下參數,name
爲註冊在jsfm
中Component
的名字(即標籤的名字),clazz
爲Component
對應的類,properties
爲一些擴展屬性;
在這個方法中又調用了WXComponentFactory
的方法registerComponent:name withClass:clazz withPros:properties
來註冊Component
,WXComponentFactory
是一個單例,負責解析Component
的方法,並保存全部註冊的Component
對應的方法;繼續到 WXComponentFactory
中看一下 registerComponent:name withClass:clazz withPros:properties
方法的實現:
// 類
- (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros
{
WXAssert(name && clazz, @"name or clazz must not be nil for registering component.");
WXComponentConfig *config = nil;
[_configLock lock];
config = [_componentConfigs objectForKey:name];
if(config){
WXLogInfo(@"Overrider component name:%@ class:%@, to name:%@ class:%@",
config.name, config.class, name, clazz);
}
// 實例 WXComponentConfig 並保存到 _componentConfigs 中
config = [[WXComponentConfig alloc] initWithName:name class:NSStringFromClass(clazz) pros:pros];
[_componentConfigs setValue:config forKey:name];
[config registerMethods];
[_configLock unlock];
}
複製代碼
該方法中會實例化一個WXComponentConfig
對象config
,每一個Component
都會有一個與之綁定的WXComponentConfig
實例,而後將config
實例做爲value
,key
爲Component
的name
保存到 _componentConfigs
中(_componentConfigs
是一個字典),config
中保存了Component
的全部暴露給js的方法,繼續看一下WXComponentConfig
的registerMethods
方法:
- (void)registerMethods
{
// 獲取類
Class currentClass = NSClassFromString(_clazz);
if (!currentClass) {
WXLogWarning(@"The module class [%@] doesn't exit!", _clazz);
return;
}
while (currentClass != [NSObject class]) {
unsigned int methodCount = 0;
// 獲取方法列表
Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount);
// 遍歷方法列表
for (unsigned int i = 0; i < methodCount; i++) {
// 獲取方法名稱
NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
BOOL isSyncMethod = NO;
// 同步方法
if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
isSyncMethod = YES;
// 異步方法
} else if ([selStr hasPrefix:@"wx_export_method_"]) {
isSyncMethod = NO;
// 其餘未暴露方法
} else {
continue;
}
NSString *name = nil, *method = nil;
SEL selector = NSSelectorFromString(selStr);
// 獲取方法實現
if ([currentClass respondsToSelector:selector]) {
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
}
if (method.length <= 0) {
WXLogWarning(@"The module class [%@] doesn't has any method!", _clazz);
continue;
}
NSRange range = [method rangeOfString:@":"];
if (range.location != NSNotFound) {
name = [method substringToIndex:range.location];
} else {
name = method;
}
// 將方法保持到對應的字典中
NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods;
[methods setObject:method forKey:name];
}
free(methodList);
currentClass = class_getSuperclass(currentClass);
}
}
複製代碼
WXComponentConfig
中有兩個字典_asyncMethods
與_syncMethods
,分別保存異步方法和同步方法;registerMethods
方法中就是經過遍歷Component
類獲取全部暴露給jsfm
的方法;而後讓咱們在回到WXSDKEngine
的registerComponent:withClass:withProperties:
方法中。
+ (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties
{
if (!name || !clazz) {
return;
}
WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !");
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
// ↑ 到這裏 Component 的方法已經解析完畢,並保持到了 WXComponentFactory 中
// 獲取 Component 的異步方法
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
// 最後將 Component 註冊到 jsfm 中
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
}
複製代碼
Component
解析完畢後,會調用WXSDKManager
中的bridgeMgr
的registerComponents:
方法;WXSDKManager
持有一個WXBridgeManager
,這個WXBridgeManager
又有一個的屬性是WXBridgeContext
,WXBridgeContext
又持有一個js Bridge
的引用,這個就是咱們常說的Bridge
。下面是相關的主要代碼和bridge
之間的關係。(如今WXDebugLoggerBridge
已經不存在了)
// WXSDKManager
@interface WXSDKManager ()
@property (nonatomic, strong) WXBridgeManager *bridgeMgr;
@property (nonatomic, strong) WXThreadSafeMutableDictionary *instanceDict;
@end
// WXBridgeManager
@interface WXBridgeManager ()
@property (nonatomic, strong) WXBridgeContext *bridgeCtx;
@property (nonatomic, assign) BOOL stopRunning;
@property (nonatomic, strong) NSMutableArray *instanceIdStack;
@end
// WXBridgeContext
@interface WXBridgeContext ()
@property (nonatomic, strong) id<WXBridgeProtocol> jsBridge;
@property (nonatomic, strong) id<WXBridgeProtocol> devToolSocketBridge;
@property (nonatomic, assign) BOOL debugJS;
//store the methods which will be executed from native to js
@property (nonatomic, strong) NSMutableDictionary *sendQueue;
//the instance stack
@property (nonatomic, strong) WXThreadSafeMutableArray *insStack;
//identify if the JSFramework has been loaded
@property (nonatomic) BOOL frameworkLoadFinished;
//store some methods temporarily before JSFramework is loaded
@property (nonatomic, strong) NSMutableArray *methodQueue;
// store service
@property (nonatomic, strong) NSMutableArray *jsServiceQueue;
@end
複製代碼
上面大體介紹了一下三個類的屬性,從屬性看也能夠看出大體的做用,各自間的調用關係也比較明確了,經過調用WXBridgeManager
調用registerComponents
方法,而後再調用WXBridgeContext
的registerComponents
方法,進行組件的註冊。
// WXBridgeManager
- (void)registerComponents:(NSArray *)components
{
if (!components) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx registerComponents:components];
});
}
// WXBridgeContext
- (void)registerComponents:(NSArray *)components
{
WXAssertBridgeThread();
if(!components) return;
[self callJSMethod:@"registerComponents" args:@[components]];
}
複製代碼
WXPerformBlockOnBridgeThread
這個線程是一個jsThread
,這是一個全局惟一線程,可是此時若是直接調用callJSMethod
,確定會失敗,由於這個時候js framework
可能尚未執行完畢。
若是此時js framework
尚未執行完成,就會把要註冊的方法都放到_methodQueue
緩存起來,js framework
加載完成以後會再次遍歷這個_methodQueue
,執行全部緩存的方法。
- (void)callJSMethod:(NSString *)method args:(NSArray *)args
{
// 若是 js frameworkLoadFinished 就當即注入 Component
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
// 若是沒有執行完,就將方法放到 _methodQueue 隊列中
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
}
- (void)callJSMethod:(NSString *)method args:(NSArray *)args onContext:(JSContext*)context completion:(void (^)(JSValue * value))complection
{
NSMutableArray *newArg = nil;
if (!context) {
if ([self.jsBridge isKindOfClass:[WXJSCoreBridge class]]) {
context = [(NSObject*)_jsBridge valueForKey:@"jsContext"];
}
}
if (self.frameworkLoadFinished) {
newArg = [args mutableCopy];
if ([newArg containsObject:complection]) {
[newArg removeObject:complection];
}
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
JSValue *value = [[context globalObject] invokeMethod:method withArguments:args];
if (complection) {
complection(value);
}
} else {
newArg = [args mutableCopy];
if (complection) {
[newArg addObject:complection];
}
[_methodQueue addObject:@{@"method":method, @"args":[newArg copy]}];
}
}
// 當 js framework 執行完畢以後會回來調用 WXJSCoreBridge 這個方法
- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}
複製代碼
接下來就是調用js framework
的registerComponents
註冊全部相關的Components
,下面會詳細分析這部份內容,按照執行順序接着會執行Modules
的註冊。
入口仍是WXSDKEngine
,調用_registerDefaultModules
,讀全部的Modules
進行註冊,註冊調用registerModule
方法,一樣的會註冊模塊,拿到WXModuleFactory
的實例,而後一樣遍歷全部的同步和異步方法,最後調用WXBridgeManager
,將模塊註冊到WXBridgeManager
中。
+ (void)_registerDefaultModules
{
[self registerModule:@"dom" withClass:NSClassFromString(@"WXDomModule")];
[self registerModule:@"locale" withClass:NSClassFromString(@"WXLocaleModule")];
...
}
+ (void)registerModule:(NSString *)name withClass:(Class)clazz
{
WXAssert(name && clazz, @"Fail to register the module, please check if the parameters are correct !");
if (!clazz || !name) {
return;
}
NSString *moduleName = [WXModuleFactory registerModule:name withClass:clazz];
NSDictionary *dict = [WXModuleFactory moduleMethodMapsWithName:moduleName];
[[WXSDKManager bridgeMgr] registerModules:dict];
}
複製代碼
註冊模塊也是經過WXModuleFactory
,將全部的module
經過_registerModule
生成ModuleMap
。註冊模塊不容許同名模塊。將name
爲key
,value
爲WXModuleConfig
存入_moduleMap
字典中,WXModuleConfig
存了該Module
相關的屬性,若是重名,註冊的時候後註冊的會覆蓋先註冊的。
@interface WXModuleFactory ()
@property (nonatomic, strong) NSMutableDictionary *moduleMap;
@property (nonatomic, strong) NSLock *moduleLock;
@end
- (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;
}
複製代碼
當把全部的Module
實例化以後,遍歷全部的方法,包括同步和異步方法,下面的方法能夠看到,在遍歷方法以前,就已經有一些方法在_defaultModuleMethod
對象中了,這裏至少有兩個方法addEventListener
和removeAllEventListeners
,因此這裏返回出來的方法都具有上面兩個方法。
- (NSMutableDictionary *)_moduleMethodMapsWithName:(NSString *)name
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSMutableArray *methods = [self _defaultModuleMethod];
[_moduleLock lock];
[dict setValue:methods forKey:name];
WXModuleConfig *config = _moduleMap[name];
void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) {
[methods addObject:mKey];
};
[config.syncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[_moduleLock unlock];
return dict;
}
- (NSMutableArray*)_defaultModuleMethod
{
return [NSMutableArray arrayWithObjects:@"addEventListener",@"removeAllEventListeners", nil];
}
複製代碼
接下來就是調用js framework
注入方法了,和registerComponent
差很少,也會涉及到線程的問題,也會經過上面WXSDKManager -> WXBridgeManager -> WXBridgeContext
。最後調用到下面這個方法。最後調用registerModules
將全部的客戶端Module
注入到js framework
中,js framework
還會有一些包裝,業務中會使用weex.registerModule
來調用對應的方法。
- (void)registerModules:(NSDictionary *)modules
{
WXAssertBridgeThread();
if(!modules) return;
[self callJSMethod:@"registerModules" args:@[modules]];
}
複製代碼
Component
和Module
你們常用還比較能理解,可是handler
是什麼呢? Weex
規定了一些協議方法,在特定的時機會調用協議中的方法,能夠實現一個類遵循這些協議,並實現協議中的方法,而後經過handler
的方式註冊給weex
,那麼在須要調用這些協議方法的時候就會調用到你實現的那個類中。好比說 WXResourceRequestHandler
:
@protocol WXResourceRequestHandler <NSObject>
// Send a resource request with a delegate
- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate;
@optional
// Cancel the ongoing request
- (void)cancelRequest:(WXResourceRequest *)request;
@end
複製代碼
WXResourceRequestHandler
中規定了兩個方法,一個是加載資源的請求方法,一個是須要請求的方法,而後看一下WXResourceRequestHandlerDefaultImpl
類:
//
// WXResourceRequestHandlerDefaultImpl.m
//
#pragma mark - WXResourceRequestHandler
- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id<WXResourceRequestDelegate>)delegate
{
if (!_session) {
NSURLSessionConfiguration *urlSessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
if ([WXAppConfiguration customizeProtocolClasses].count > 0) {
NSArray *defaultProtocols = urlSessionConfig.protocolClasses;
urlSessionConfig.protocolClasses = [[WXAppConfiguration customizeProtocolClasses] arrayByAddingObjectsFromArray:defaultProtocols];
}
_session = [NSURLSession sessionWithConfiguration:urlSessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
_delegates = [WXThreadSafeMutableDictionary new];
}
NSURLSessionDataTask *task = [_session dataTaskWithRequest:request];
request.taskIdentifier = task;
[_delegates setObject:delegate forKey:task];
[task resume];
}
- (void)cancelRequest:(WXResourceRequest *)request
{
if ([request.taskIdentifier isKindOfClass:[NSURLSessionTask class]]) {
NSURLSessionTask *task = (NSURLSessionTask *)request.taskIdentifier;
[task cancel];
[_delegates removeObjectForKey:task];
}
}
複製代碼
WXResourceRequestHandlerDefaultImpl
遵循了WXResourceRequestHandler
協議,並實現了協議方法,而後註冊了Handler
,若是有資源請求發出來,就會走到WXResourceRequestHandlerDefaultImpl
的實現中。
客戶端初始化SDK
就完成了註冊相關的方法,上面一直都在提到最後註冊是註冊到js
環境中,將方法傳遞給js framework
進行調用,可是js framework
一直都尚未調用,下面就是加載這個文件了。
在官方GitHub
中 runtime 目錄下放着一堆js
,這堆js
最後會被打包成一個叫native-bundle-main.js
的文件,咱們暫且稱之爲main.js
,這段js
就是咱們常說的js framework
,在SDK
初始化時,會將整段代碼當成字符串傳遞給WXSDKManager
並放到JavaScript Core
中去執行。咱們先看看這個runtime
下的文件都有哪些
|-- api:凍結原型鏈,提供給原生調用的方法,好比 registerModules
|-- bridge:和客戶端相關的接口調用,調用客戶端的時候有一個任務調度
|-- entries:客戶端執行 js framework 的入口文件,WXSDKEngine 調用的方法
|-- frameworks:核心文件,初始化 js bundle 實例,對實例進行管理,dom 調度轉換等
|-- services:js service 存放,broadcast 調度轉換等
|-- shared:polyfill 和 console 這些差別性的方法
|-- vdom:將 VDOM 轉化成客戶端能渲染的指令
複製代碼
看起來和咱們上一篇文章提到的js bridge
的功能很類似,可是爲何Weex
的這一層有這麼多功能呢,首先Weex
是要兼容三端的,因此iOS
、android
、web
的差別性一定是須要去抹平的,他們接受指令的方式和方法都有可能不一樣,好比:客戶端設計的是createBody
和addElement
,而web
是createElement
、appendChild
等。
除了指令的差別,還有上層業務語言的不一樣,好比Weex
支持Vue
和Rax
,甚至可能支持React
,只要是符合js framework
的實現,就能夠經過不一樣的接口渲染在不一樣的宿主環境下。咱們能夠稱這一層爲DSL
,咱們也看看js framework
這層的主要代碼
|-- index.js:入口文件
|-- legacy:關於 VM 相關的主要方法
| |-- api:相關 vm 定義的接口
| |-- app:管理頁面間頁面實例的方法
| |-- core:實現數據監聽的方法
| |-- static:靜態方法
| |-- util:工具類函數
| |-- vm:解析指令相關
|-- vanilla:與客戶端交互的一些方法
複製代碼
首先註冊完上面所提到的三個模塊以後,WXSDKEngine
繼續往下執行,仍是先會調用到WXBridgeManager
中的executeJsFramework
,再調用到WXBridgeContext
的executeJsFramework
,而後在子線程中執行js framework
。
// WXSDKEngine
[[WXSDKManager bridgeMgr] executeJsFramework:script];
// WXBridgeManager
- (void)executeJsFramework:(NSString *)script
{
if (!script) return;
__weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx executeJsFramework:script];
});
}
// WXBridgeContext
- (void)executeJsFramework:(NSString *)script
{
WXAssertBridgeThread();
WXAssertParam(script);
WX_MONITOR_PERF_START(WXPTFrameworkExecute);
// 真正的執行 js framework
[self.jsBridge executeJSFramework:script];
WX_MONITOR_PERF_END(WXPTFrameworkExecute);
if ([self.jsBridge exception]) {
NSString *exception = [[self.jsBridge exception] toString];
NSMutableString *errMsg = [NSMutableString stringWithFormat:@"[WX_KEY_EXCEPTION_SDK_INIT_JSFM_INIT_FAILED] %@",exception];
[WXExceptionUtils commitCriticalExceptionRT:@"WX_KEY_EXCEPTION_SDK_INIT" errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_SDK_INIT] function:@"" exception:errMsg extParams:nil];
WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_EXECUTE, errMsg);
} else {
WX_MONITOR_SUCCESS(WXMTJSFramework);
//the JSFramework has been load successfully.
// 執行完 js
self.frameworkLoadFinished = YES;
// 執行緩存在 _jsServiceQueue 中的方法
[self executeAllJsService];
// 獲取 js framework 版本號
JSValue *frameworkVersion = [self.jsBridge callJSMethod:@"getJSFMVersion" args:nil];
if (frameworkVersion && [frameworkVersion isString]) {
[WXAppConfiguration setJSFrameworkVersion:[frameworkVersion toString]];
}
// 計算 js framework 的字節大小
if (script) {
[WXAppConfiguration setJSFrameworkLibSize:[script lengthOfBytesUsingEncoding:NSUTF8StringEncoding]];
}
//execute methods which has been stored in methodQueue temporarily.
// 開始執行以前緩存在隊列緩存在 _methodQueue 的方法
for (NSDictionary *method in _methodQueue) {
[self callJSMethod:method[@"method"] args:method[@"args"]];
}
[_methodQueue removeAllObjects];
WX_MONITOR_PERF_END(WXPTInitalize);
};
}
複製代碼
上面執行過程當中比較核心的是如何執行js framework
的,其實就是加載native-bundle-main.js
文件,執行完了以後也不須要有返回值,或者持有對js framework
的引用,只是放在內存中,隨時準備被調用。在執行先後也會有日誌記錄
// WXBridgeContext
- (void)executeJSFramework:(NSString *)frameworkScript
{
WXAssertParam(frameworkScript);
if (WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
[_jsContext evaluateScript:frameworkScript withSourceURL:[NSURL URLWithString:@"native-bundle-main.js"]];
}else{
[_jsContext evaluateScript:frameworkScript];
}
}
複製代碼
咱們先拋開js framework
自己的執行,先看看執行完成以後,客戶端接着會完成什麼工做,要開始加載以前緩存在_jsServiceQueue
和_methodQueue
中的方法了。
// WXBridgeContext
- (void)executeAllJsService
{
for(NSDictionary *service in _jsServiceQueue) {
NSString *script = [service valueForKey:@"script"];
NSString *name = [service valueForKey:@"name"];
[self executeJsService:script withName:name];
}
[_jsServiceQueue removeAllObjects];
}
for (NSDictionary *method in _methodQueue) {
[self callJSMethod:method[@"method"] args:method[@"args"]];
}
[_methodQueue removeAllObjects];
複製代碼
_methodQueue
比較好理解,前面哪些原生註冊方法都是緩存在_methodQueue
中的,_jsServiceQueue
是從哪兒來的呢?js service
下面還會詳細說明,broadcastChannel
就是Weex
提供的一種js service
,官方用例也 提供了擴展js service
的方式,由此能夠看出js service
只會加載一次,js service
只是一堆字符串,因此直接執行就行。
// WXSDKEngine
NSDictionary *jsSerices = [WXDebugTool jsServiceCache];
for(NSString *serviceName in jsSerices) {
NSDictionary *service = [jsSerices objectForKey:serviceName];
NSString *serviceName = [service objectForKey:@"name"];
NSString *serviceScript = [service objectForKey:@"script"];
NSDictionary *serviceOptions = [service objectForKey:@"options"];
[WXSDKEngine registerService:serviceName withScript:serviceScript withOptions:serviceOptions];
}
// WXBridgeContext
- (void)executeJsService:(NSString *)script withName:(NSString *)name
{
if(self.frameworkLoadFinished) {
WXAssert(script, @"param script required!");
[self.jsBridge executeJavascript:script];
if ([self.jsBridge exception]) {
NSString *exception = [[self.jsBridge exception] toString];
NSMutableString *errMsg = [NSMutableString stringWithFormat:@"[WX_KEY_EXCEPTION_INVOKE_JSSERVICE_EXECUTE] %@",exception];
[WXExceptionUtils commitCriticalExceptionRT:@"WX_KEY_EXCEPTION_INVOKE" errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_INVOKE] function:@"" exception:errMsg extParams:nil];
WX_MONITOR_FAIL(WXMTJSService, WX_ERR_JSFRAMEWORK_EXECUTE, errMsg);
} else {
// success
}
}else {
[_jsServiceQueue addObject:@{
@"name": name,
@"script": script
}];
}
}
複製代碼
_methodQueue
隊列的執行是調用callJSMethod
,往下會調用WXJSCoreBridge
的invokeMethod
,這個就是就是調用對應的js framework
提供的方法,同時會發現一個WXJSCoreBridge
文件,這裏就是Weex
的bridge
,_jsContext
就是提供的所有客戶端和js framework
真正交互的全部方法了,這些方法都是提供給js framework
來調用的,主要的方法後面都會詳細講到。
js framework
執行的入口文件/runtime/entries/index.js
,會調用/runtime/entries/setup.js
,這裏的js
模塊化粒度很細,咱們就不一一展現代碼了,能夠去Weex
項目的裏看源碼。
/** * Setup frameworks with runtime. * You can package more frameworks by * passing them as arguments. */
export default function (frameworks) {
const { init, config } = runtime
config.frameworks = frameworks
const { native, transformer } = subversion
for (const serviceName in services) {
runtime.service.register(serviceName, services[serviceName])
}
runtime.freezePrototype()
// register framework meta info
global.frameworkVersion = native
global.transformerVersion = transformer
// init frameworks
const globalMethods = init(config)
// set global methods
for (const methodName in globalMethods) {
global[methodName] = (...args) => {
const ret = globalMethods[methodName](...args)
if (ret instanceof Error) {
console.error(ret.toString())
}
return ret
}
}
}
複製代碼
咱們主要看,js framework
的執行完成了哪些功能,主要是下面三個功能:
- 掛載全局屬性方法及 VM 原型鏈方法
- 建立於客戶端通訊橋
- 彌補環境差別
剛纔已經講了DSL
是什麼,js framework
中很是重要的功能就是作好不一樣宿主環境和語言中的兼容。主要是經過一些接口來與客戶端進行交互,適配前端框架其實是爲了適配iOS
、android
和瀏覽器。這裏主要講一講和客戶端進行適配的接口。
- getRoot:獲取頁面節點
- receiveTasks:監聽客戶端任務
- registerComponents:註冊 Component
- registerMoudles:註冊 Module
- init: 頁面內部生命週期初始化
- createInstance: 頁面內部生命週期建立
- refreshInstance: 頁面內部生命週期刷新
- destroyInstance: 頁面內部生命週期銷燬 ...
這些接口均可以在WXBridgeContext
裏看到,都是js framework
提供給客戶端調用的。其中Weex SDK
初始化的時候,提到的registerComponents
和registerMoudles
也是調用的這個方法。
js framework
中registerComponents
的實現能夠看出,前端只是作了一個map
緩存起來,等待解析vDOM
的時候進行映射,而後交給原生組件進行渲染。
// /runtime/frameworks/legacy/static/register.js
export function registerComponents (components) {
if (Array.isArray(components)) {
components.forEach(function register (name) {
/* istanbul ignore if */
if (!name) {
return
}
if (typeof name === 'string') {
nativeComponentMap[name] = true
}
/* istanbul ignore else */
else if (typeof name === 'object' && typeof name.type === 'string') {
nativeComponentMap[name.type] = name
}
})
}
}
複製代碼
registerMoudles
時也差很少,放在了nativeModules
這個對象上緩存起來,可是使用的時候要複雜一些,後面也會講到。
// /runtime/frameworks/legacy/static/register.js
export function registerModules (modules) {
/* istanbul ignore else */
if (typeof modules === 'object') {
initModules(modules)
}
}
// /runtime/frameworks/legacy/app/register.js
export function initModules (modules, ifReplace) {
for (const moduleName in modules) {
// init `modules[moduleName][]`
let methods = nativeModules[moduleName]
if (!methods) {
methods = {}
nativeModules[moduleName] = methods
}
// push each non-existed new method
modules[moduleName].forEach(function (method) {
if (typeof method === 'string') {
method = {
name: method
}
}
if (!methods[method.name] || ifReplace) {
methods[method.name] = method
}
})
}
}
複製代碼
js framework
是客戶端和前端業務代碼溝通的橋樑,因此更重要的也是bridge
,基本的橋的設計上一篇也講了,Weex
選擇的是直接提供方法供js
調用,也直接調用js
的方法。
客戶端調用js
直接使用callJs
,callJs
是js
提供的方法,放在當前線程中,供客戶端調用,包括DOM
事件派發、module
調用時的時間回調,都是經過這個接口通知js framework
,而後再調用緩存在js framework
中的方法。
js
調用客戶端使用callNative
,客戶端也會提供不少方法給js framework
供,framework
調用,這些方法均可以在WXBridgeContext
中看到,callNative
只是其中的一個方法,實際代碼中還有不少方法,好比addElement
、updateAttrs
等等
除了用於完成功能的主要方法,客戶端還提供一些方法來彌補上層框架在js
中調用時沒有的方法,就是環境的差別,彌補兼容性的差別,setTimeout
、nativeLog
等,客戶端提供了對應的方法,js framework
也沒法像在瀏覽器中調用這些方法同樣去調用這些方法,因此須要雙方採用兼容性的方式去支持。
還有一些ployfill
的方法,好比Promise
,Object.assign
,這些ployfill
能保證一部分環境和瀏覽器同樣,下降咱們寫代碼的成本。
執行js framework
其餘的過程就不一一展開了,主要是一些前端代碼之間的互相調用,這部分也承接了不少Weex
歷史遺留的一些兼容問題,有時候發現一些神奇的寫法,多是當時爲了解決一些神奇的bug
吧,以及各類istanbul ignore
的註釋。
執行完js framework
以後客戶端frameworkLoadFinished
會被置位 YES
,以前遺留的任務也都會在js framework
執行完畢以後執行,以完成整個初始化的流程。
客戶端會先執行js-service
,由於js-service
只是須要在JavaScript Core
中執行字符串,因此直接執行executeAllJsService
就好了,並不須要調用js framework
的方法,只是讓當前內存環境中有js service
的變量對象。
而後將_methodQueue
中的任務拿出來遍歷執行。這裏就是執行緩存隊列中的registerComponents
、registerModules
、registerMethods
。上面也提到了具體二者是怎麼調用的,詳細的代碼都是在這裏。
執行完畢以後,按理說這個js Thread
應該關閉,而後被回收,可是咱們還須要讓這個js framework
一直運行在js Core
中,因此這個就須要給js Thread
開啓了一個runloop
,讓這個js Thread
一直處於執行狀態
前面鋪墊了很是多的初始化流程,就是爲了在將一個頁面是如何展現的過程當中能清晰一點,前面至關於在作準備工做,這個時候咱們來看Weex
實例的初始化。Eros 經過配置文件將首頁的 URL 配置在配置文件中,客戶端能直接拿到首頁直接進行初始化。
客戶端經過 _renderWithURL
去加載首頁的URL
,這個URL
無論是放在本地仍是服務器上,其實就是一個js bundle
文件,就是一個通過特殊loader
打包的js
文件,加載到這個文件以後,將這個調用到js framework
中的 createInstance。
/* id:Weex 實例的 id code:js bundle 的代碼 config:配置參數 data:參數 */
function createInstance (id, code, config, data) {
// 判斷當前實例是否已經建立過了
if (instanceTypeMap[id]) {
return new Error(`The instance id "${id}" has already been used!`)
}
// 獲取當前 bundle 是那種框架
const bundleType = getBundleType(code)
instanceTypeMap[id] = bundleType
// 初始化 instance 的 config
config = JSON.parse(JSON.stringify(config || {}))
config.env = JSON.parse(JSON.stringify(global.WXEnvironment || {}))
config.bundleType = bundleType
// 獲取當前的 DSL
const framework = runtimeConfig.frameworks[bundleType]
if (!framework) {
return new Error(`[JS Framework] Invalid bundle type "${bundleType}".`)
}
if (bundleType === 'Weex') {
console.error(`[JS Framework] COMPATIBILITY WARNING: `
+ `Weex DSL 1.0 (.we) framework is no longer supported! `
+ `It will be removed in the next version of WeexSDK, `
+ `your page would be crash if you still using the ".we" framework. `
+ `Please upgrade it to Vue.js or Rax.`)
}
// 得到對應的 WeexInstance 實例,提供 Weex.xx 相關的方法
const instanceContext = createInstanceContext(id, config, data)
if (typeof framework.createInstance === 'function') {
// Temporary compatible with some legacy APIs in Rax,
// some Rax page is using the legacy ".we" framework.
if (bundleType === 'Rax' || bundleType === 'Weex') {
const raxInstanceContext = Object.assign({
config,
created: Date.now(),
framework: bundleType
}, instanceContext)
// Rax 或者 Weex DSL 調用初始化的地方
return framework.createInstance(id, code, config, data, raxInstanceContext)
}
// Rax 或者 Weex DSL 調用初始化的地方
return framework.createInstance(id, code, config, data, instanceContext)
}
// 當前 DSL 沒有提供 createInstance 支持
runInContext(code, instanceContext)
}
複製代碼
上面就是調用的第一步,不一樣的DSL
已經在這兒就開始區分,生成不一樣的Weex
實例。下一步就是調用各自DSL
的createInstance
,並把對應須要的參數都傳遞過去
// /runtime/frameworks/legacy/static/create.js
export function createInstance (id, code, options, data, info) {
const { services } = info || {}
resetTarget()
let instance = instanceMap[id]
/* istanbul ignore else */
options = options || {}
let result
/* istanbul ignore else */
if (!instance) {
// 建立 APP 實例,並將實例放到 instanceMap 上
instance = new App(id, options)
instanceMap[id] = instance
result = initApp(instance, code, data, services)
}
else {
result = new Error(`invalid instance id "${id}"`)
}
return (result instanceof Error) ? result : instance
}
// /runtime/frameworks/legacy/app/instance.js
export default function App (id, options) {
this.id = id
this.options = options || {}
this.vm = null
this.customComponentMap = {}
this.commonModules = {}
// document
this.doc = new renderer.Document(
id,
this.options.bundleUrl,
null,
renderer.Listener
)
this.differ = new Differ(id)
}
複製代碼
主要的仍是initAPP
這個方法,這個方法中作了不少補全原型鏈的方法,好比bundleDefine
、bundleBootstrap
等等,這些都挺重要的,你們能夠看看 init 方法,就完成了上述的操做。
最主要的仍是下面這個方法,這裏會是最終執行js bundle
的地方。執行完成以後將 Weex
的單個頁面的實例放在instanceMap
,new Function
是最核心的方法,這裏就是將整個JS bundle
由代碼到執行生成VDOM
,而後轉換成一個個VNode
發送到原生模塊進行渲染。
if (!callFunctionNative(globalObjects, functionBody)) {
// If failed to compile functionBody on native side,
// fallback to callFunction.
callFunction(globalObjects, functionBody)
}
// 真正執行 js bundle 的方法
function callFunction (globalObjects, body) {
const globalKeys = []
const globalValues = []
for (const key in globalObjects) {
globalKeys.push(key)
globalValues.push(globalObjects[key])
}
globalKeys.push(body)
// 全部的方法都是經過 new Function() 的方式被執行的
const result = new Function(...globalKeys)
return result(...globalValues)
}
複製代碼
js bundle
就是寫的業務代碼了,你們能夠寫一個簡單的代碼保存一下看看,因爲使用了Weex
相關的loader
,具體的代碼確定和常規的js
代碼不同,通過轉換主要仍是<template>
和<style>
部分,這兩部分會被轉換成兩個JSON
,放在兩個閉包中。上面已經說到了最後是執行了new Function
,具體的執行步驟在init,因爲代碼太長,咱們主要看核心的部分。
const globalObjects = Object.assign({
define: bundleDefine,
require: bundleRequire,
bootstrap: bundleBootstrap,
register: bundleRegister,
render: bundleRender,
__weex_define__: bundleDefine, // alias for define
__weex_bootstrap__: bundleBootstrap, // alias for bootstrap
__weex_document__: bundleDocument,
__weex_require__: bundleRequireModule,
__weex_viewmodel__: bundleVm,
weex: weexGlobalObject
}, timerAPIs, services)
複製代碼
上述這些代碼是被執行的核心部分, bundleDefine 部分,這裏是解析組件的部分,分析哪些是和Weex
對應的Component
,哪些是用戶自定義的Component
,這裏就是一個遞歸遍歷的過程。
bundleRequire
和bundleBootstrap
,這裏調用到了 bootstrap和 Vm,這裏有一步我不是很明白。bootstrap
主要的功能是校驗參數和環境信息,這部分你們能夠看一下源碼。
Vm
是根據Component
新建對應的ViewModel
,這部分作的事情就很是多了,基本上是解析整個VM
的核心。主要完成了初始化生命週期、數據雙綁、構建模板、UI
繪製。
// bind events and lifecycles
initEvents(this, externalEvents)
console.debug(`[JS Framework] "init" lifecycle in Vm(${this._type})`)
this.$emit('hook:init')
this._inited = true
// proxy data and methods
// observe data and add this to vms
this._data = typeof data === 'function' ? data() : data
if (mergedData) {
extend(this._data, mergedData)
}
initState(this)
console.debug(`[JS Framework] "created" lifecycle in Vm(${this._type})`)
this.$emit('hook:created')
this._created = true
// backward old ready entry
if (options.methods && options.methods.ready) {
console.warn('"exports.methods.ready" is deprecated, ' +
'please use "exports.created" instead')
options.methods.ready.call(this)
}
if (!this._app.doc) {
return
}
// if no parentElement then specify the documentElement
this._parentEl = parentEl || this._app.doc.documentElement
build(this)
複製代碼
代碼實現;這個過程當中初始化了4個生命週期的鉤子,init
、created
、ready
、destroyed
。除了生命週期,這裏還綁定了vm
的事件機制,組件間互相通訊的方式。
代碼實現;Vue DSL
數據雙綁能夠參考一下Vue
的數據雙綁實現原理,Rax
也是大同小異,將數據進行代理,而後添加數據監聽,初始化計算屬性,掛載_method
方法,建立getter/setter
,重寫數組的方法,遞歸綁定...這部分主要是Vue
的內容,以前也有博客詳細說明了Vue
的數據雙綁機制。
代碼實現;這裏也是Vue
的模板解析機制之一,大部分是對Vue
模板語法的解析,好比v-for
、:class
解析語法的過程是一個深度遍歷的過程,這個過程完成以後js bundle
就變成了VDOM
,這個VDOM
更像是符合某種約定格式的JSON
數據,由於客戶端和js framework
可共用的數據類型很少,JSON
是最好的方式,因此最終將模板轉換成JSON
的描述方式傳遞給客戶端。
代碼實現;經過differ.flush
調用,會觸發VDOM
的對比,對比的過程是一個同級對比的過程,將節點也就是VNode
逐一diff
傳遞給客戶端。先對比外層組件,若是有子節點再遞歸子節點,對比不一樣的部分都傳遞給客戶端,首次渲染全是新增,後面更新UI
的時候會有用到remove
、update
等API
。
最終繪製調用 appendChild,這裏封裝了全部和native
有交互的方法。DOM
操做大體就是addElement
、removeElement
等方法,調用taskCenter.send
,這裏是一個任務調度,最終全部的方法都是經過這裏調用客戶端提供的對應的接口。
send (type, params, args, options) {
const { action, component, ref, module, method } = params
// normalize args and options
args = args.map(arg => this.normalize(arg))
if (typof(options) === 'Object') {
options = this.normalize(options, true)
}
switch (type) {
case 'dom':
return this[action](this.instanceId, args)
case 'component':
return this.componentHandler(this.instanceId, ref, method, args, Object.assign({ component }, options))
default:
return this.moduleHandler(this.instanceId, module, method, args, options)
}
}
複製代碼
調用客戶端以後,回顧以前Weex SDK
初始化的時候,addElement
是已經在客戶端注入的方法,而後將對應的Component
映射到對應的解析原生方法中。原生再找到對應Component
進行渲染。
因爲Weex
渲染完成父級以後纔會渲染子,因此傳遞的順序是先傳父,再傳子,父渲染完成以後,任務調度給一個渲染完成的回調,而後再進行遞歸,渲染子節點的指令,這樣可能會比較慢,上面提到註冊Component
的時候會有兩個參數append=tree
和istemplate=true
,這兩種方式都是優化性能的方案,上面提到在Components
註冊的時候有這兩個參數。
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];
}
[component _didInserted];
if (appendTree) {
// If appending tree,force layout in case of too much tasks piling up in syncQueue
[self _layoutAndSyncUI];
}
複製代碼
Weex
的渲染方式有兩種一種是node
,一種是tree
,node
是先渲染父節點,再渲染子節點,而tree
是先渲染子節點,最後一次性layout
渲染父節點。渲染性能上講,剛開始的繪製時間,append="node"
比較快,可是從總的時間來講,append="tree"
用的時間更少。
若是當前Component
有{@"append":@"tree"}
屬性而且它的父Component
沒有這個屬性將會強制對頁面進行從新佈局。能夠看到這樣作是爲了防止UI
繪製任務太多堆積在一塊兒影響同步隊列任務的執行。
WXComponentConfig *config = [WXComponentFactory configWithComponentName:type];
BOOL isTemplate = [config.properties[@"isTemplate"] boolValue] || (supercomponent && supercomponent->_isTemplate);
if (isTemplate) {
bindingProps = [self _extractBindingProps:&attributes];
bindingStyles = [self _extractBindings:&styles];
bindingAttibutes = [self _extractBindings:&attributes];
bindingEvents = [self _extractBindingEvents:&events];
}
複製代碼
那麼客戶端在渲染的時候,會將整個Component
子節點獲取過來,而後經過DataBinding
轉換成表達式,存在bindingMap
中,相關的解析都在WXJSASTParser.m
文件中,涉及到比較複雜的模板解析,表達式解析和轉換,綁定數據與原生UI
的關係。
渲染過程當中客戶端和js framework
還有事件的溝通,經過橋傳遞createFinished
和renderFinished
事件,js framework
會去執行Weex
實例對應的生命週期方法。
至此頁面就已經渲染出來了,頁面渲染完成以後,那麼點擊事件是怎麼作的呢?
在瞭解事件如何發生傳遞以前,咱們先看看事件有幾種類型,Eros 封裝了路由的事件,將這些事件封裝在組件上,在Vue
模板上提供一個 Eros 對象,在Weex
建立實例的時候綁定這些方法注入回調等待客戶端回調,客戶端在發生對應的事件的手經過全局事件來通知到js framework
執行weex
實例上的回調方法。
// app 先後臺相關 start
appActive() {
console.log('appActive');
},
appDeactive() {
console.log('appDeactive');
},
// app 先後臺相關 end
// 頁面週期相關 start
beforeAppear (params, options) {
console.log('beforeAppear');
},
beforeBackAppear (params, options) {
console.log('beforeBackAppear');
},
appeared (params, options) {
console.log('appeared');
},
backAppeared (params, options) {
console.log('backAppeared');
},
beforeDisappear (options) {
console.log('beforeDisappear');
},
disappeared (options) {
console.log('disappeared');
},
// 頁面週期相關 end
複製代碼
全局事件 Eros 是經過相似node js
的處理,在js core
中放一個全局對象,也是相似使用Module
的方式去使用,經過封裝相似js
的事件機制的方式去觸發。
咱們主要分析的是頁面交互的事件,好比點擊事件;客戶端在發生事件的時候,怎麼能執行咱們在Vue
實例上定義的方法呢?這個過程首先點擊事件須要註冊,也就是說是在初始化的時候,js framework
就已經告訴客戶端哪些組件是有事件綁定回調的,若是客戶端無論接受到什麼事件都拋給js
,性能確定會不好。
js framework
在解析模板的時候發現有事件標籤@xxx="callback"
,就會在建立組件的時候經過callAddEvent
將event
傳遞給native
,可是不會傳遞事件的回調方法,由於客戶端根本就不識別事件回調的方法,客戶端發現有事件屬性以後,就會對原生的事件進行事件綁定,在渲染組件的時候,每一個組件都會生成一個組件ID
,就是ref
,type
就是事件類型好比:click
、longpress
等。
// https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/vm/compiler.js
if (!vm._rootEl) {
vm._rootEl = element
// bind event earlier because of lifecycle issues
const binding = vm._externalBinding || {}
const target = binding.template
const parentVm = binding.parent
if (target && target.events && parentVm && element) {
for (const type in target.events) {
const handler = parentVm[target.events[type]]
if (handler) {
element.addEvent(type, bind(handler, parentVm))
}
}
}
}
// https://github.com/apache/incubator-weex/blob/master/runtime/vdom/Element.js
addEvent (type, handler, params) {
if (!this.event) {
this.event = {}
}
if (!this.event[type]) {
this.event[type] = { handler, params }
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
taskCenter.send(
'dom',
{ action: 'addEvent' },
[this.ref, type]
)
}
}
}
複製代碼
上面能夠看出只傳遞了一個ref
過去,綁定完畢至全部組件渲染完成以後,當視圖發生對應的事件以後,客戶端捕獲到了事件以後經過fireEvent
將對應的事件,傳遞四個參數,ref
、type
、event
、domChanges
,經過bridge
將這些參數傳遞給js framework
的bridge
,可是到底層的時候還會攜帶一個Weex
實例的ID
,由於此時可能存在多個weex
實例,經過Weex ID找到對應的
weex`實例。
若是事件綁定有多個ref
,還須要遍歷遞歸一下,也是一個深度遍歷的過程,而後找到對應的事件,觸發對應的事件,事件裏可能有對雙綁數據的改變,進而改變DOM
,因此事件觸發以後再次進行differ.flush
。對比生成新的VDOM
,而後渲染新的頁面樣式。
// https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/ctrl/misc.js
export function fireEvent (app, ref, type, e, domChanges) {
console.debug(`[JS Framework] Fire a "${type}" event on an element(${ref}) in instance(${app.id})`)
if (Array.isArray(ref)) {
ref.some((ref) => {
return fireEvent(app, ref, type, e) !== false
})
return
}
const el = app.doc.getRef(ref)
if (el) {
const result = app.doc.fireEvent(el, type, e, domChanges)
app.differ.flush()
app.doc.taskCenter.send('dom', { action: 'updateFinish' }, [])
return result
}
return new Error(`invalid element reference "${ref}"`)
}
複製代碼
app.doc.fireEvent(el, type, e, domChanges)
主要來看看這個方法,首先是獲取到當時的事件回調,而後執行事件回調,原生的組件不會有事件冒泡,可是js
是有事件冒泡機制的,因此下面模擬了一個事件冒泡機制,繼續觸發了父級的fireEvent
,逐個冒泡到父級,這部分是在js framework
中完成的。
// https://github.com/apache/incubator-weex/blob/master/runtime/vdom/Element.js
fireEvent (type, event, isBubble, options) {
let result = null
let isStopPropagation = false
const eventDesc = this.event[type]
if (eventDesc && event) {
const handler = eventDesc.handler
event.stopPropagation = () => {
isStopPropagation = true
}
if (options && options.params) {
result = handler.call(this, ...options.params, event)
}
else {
result = handler.call(this, event)
}
}
if (!isStopPropagation
&& isBubble
&& (BUBBLE_EVENTS.indexOf(type) !== -1)
&& this.parentNode
&& this.parentNode.fireEvent) {
event.currentTarget = this.parentNode
this.parentNode.fireEvent(type, event, isBubble) // no options
}
return result
}
複製代碼
上述就完成了一次完整的事件觸發,若是是簡單的事件,相似click
這樣的一次傳遞完成一次事件回調,不會有太大的問題,可是若是是滾動這樣的事件傳遞不免會有性能問題,因此客戶端在處理滾動事件的時候,確定會有一個最小時間間隔,確定不是無時無刻的觸發。
更好的處理是Weex
也引入了expression binding
,將js
的事件回調處理成表達式,在綁定的時候一併傳給客戶端,因爲是表達式,因此客戶端也能夠識別表達式,客戶端在監聽原生事件觸發的時候,就直接執行表達式。這樣就省去了傳遞的過程。Weex
的bingdingX
也是能夠用來處理相似頻繁觸發的js
和客戶端之間的交互的,好比動畫。
上面已經講了module
的註冊,最終調用js framework
的registerModules
注入全部module
方法,並將方法存儲在nativeModules
對象上,註冊的過程就算完成了。
// https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/static/register.js
export function registerModules (modules) {
/* istanbul ignore else */
if (typeof modules === 'object') {
initModules(modules)
}
}
// https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/register.js
export function initModules (modules, ifReplace) {
for (const moduleName in modules) {
// init `modules[moduleName][]`
let methods = nativeModules[moduleName]
if (!methods) {
methods = {}
nativeModules[moduleName] = methods
}
// push each non-existed new method
modules[moduleName].forEach(function (method) {
if (typeof method === 'string') {
method = {
name: method
}
}
if (!methods[method.name] || ifReplace) {
methods[method.name] = method
}
})
}
}
複製代碼
咱們經過weex.requireModule('xxx')
來獲取module
,首先咱們須要瞭解一下weex
這個全局變量是哪兒來的,上面在渲染的過程當中的時候會生成一個weex
實例,這個信息會被保存在一個全局變量中weexGlobalObject
,在callFunction
的時候,這個對象會被綁定在js bundle
執行時的weex
對象上,具體以下。
const globalObjects = Object.assign({
...
weex: weexGlobalObject
}, timerAPIs, services)
複製代碼
weex
這個對象上還有會不少方法和屬性,其中就有能調用到module
的方法就是requireModule
,這個方法和上面客戶端注入Module
時的方法是放在同一個模塊中的,也就是同一個閉包中的,因此能夠共享nativeModules
這個對象。
//https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/index.js
App.prototype.requireModule = function (name) {
return requireModule(this, name)
}
// https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/register.js
export function requireModule (app, name) {
const methods = nativeModules[name]
const target = {}
for (const methodName in methods) {
Object.defineProperty(target, methodName, {
configurable: true,
enumerable: true,
get: function moduleGetter () {
return (...args) => app.callTasks({
module: name,
method: methodName,
args: args
})
},
set: function moduleSetter (value) {
if (typeof value === 'function') {
return app.callTasks({
module: name,
method: methodName,
args: [value]
})
}
}
})
}
return target
}
複製代碼
上面爲何沒有使用簡單的call
或者apply
方法呢?而是在返回的時候對這個對象全部方法進行了相似雙綁的操做。首先確定是爲了不對象被污染,這個nativeModules
是全部weex
實例共用的對象,若是一旦能夠直接獲取,前端對象都是引用,就有可能被重寫,這樣的確定是很差的。
這裏還用了一個callTasks
,這個前面初始化的時候都已經說明過了,其實就是調用對應native
的方法,taskCenter.send
就會去查找客戶端對應的方法,上面有taskCenter
相關的代碼,最後經過callNativeModule
調用到客戶端的代碼。
// https://github.com/apache/incubator-weex/blob/master/runtime/frameworks/legacy/app/ctrl/misc.js
export function callTasks (app, tasks) {
let result
/* istanbul ignore next */
if (typof(tasks) !== 'array') {
tasks = [tasks]
}
tasks.forEach(task => {
result = app.doc.taskCenter.send(
'module',
{
module: task.module,
method: task.method
},
task.args
)
})
return result
}
複製代碼
完成調用以後就等待客戶端處理,客戶端處理完成以後進行返回。這裏雖然是一個forEach
的遍歷,可是返回的result
都是同步的最後一個result
。這裏不是很嚴謹,可是咱們看上層結構又不會有問題,tasks
傳過來通常是一個一個的任務,不會傳array
過來,而且大部分的客戶端調用方法都是異步的,不多有同步回調,因此只能說不嚴謹。
經過上面的梳理,咱們能夠看到Weex
運行原理的細節,總體流程也梳理清楚了,咱們經過一年的實踐,無論是純Weex
應用仍是現有APP
接入都有實踐,支撐了咱們上百個頁面的業務,同時開發效率獲得了很是大的提高,也完善了咱們基於Vue
的前端技術棧。
如今Weex
自己也在不斷的更新,至少咱們的業務上線以後讓咱們相信Weex
是可行的,雖然各類缺點不斷的被詬病,可是哪一個優秀的技術的沒有經歷這樣的發展呢。摘掉咱們前端技術的鄙視鏈眼鏡,讓技術更好的爲業務服務。
最後咱們在經過業務實踐和積累以後,也概括總結出了基於Weex
的技術解決方案 Eros並開源出來,解決了被你們所詬病的環境問題,提供更多豐富的Component
和Module
解決實際的業務問題。目前已有上千開發者有過開發體驗,在不斷吐槽中改進咱們的方案,穩定了底層方案,構建了新的插件化方式,目前已經有開發者貢獻了一些插件,也收集到開發者已上線的40+ APP
的案例,還有很是多的APP
在開發過程當中。但願咱們的方案能幫助到APP
開發中的你。
下面是一些經過 Eros 上線的APP
案例