Weex
上一篇文章講到了混合應用簡單的發展史,本文以
Weex
為例分析一下混合應用,本文并非是介紹
Weex
是怎麼使用的,如果想要了解怎麼使用,不如了解一下
Eros的解決方案,主要想剖析一下
Weex
的原理,了解
Weex
的運作機制。
為什麼要選擇 Weex
首先想聊一聊我們為什麼選擇
Weex
。上一篇文章結尾對
Weex
和
ReactNative
進行了簡要的分析,在我們做技術選型時大環境下
RN
不管從哪方面來說都是一個更好的方案,更多的對比可以去
weex&ReactNative對比看看,在做技術選型的時候也在不斷的問,為什麼?最後大概從下面幾個方面得到了一個相對好的選擇。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicWZwpmL0ITYzQDZmdTO2EGZjBTMhJzYmRWM2M2N0QTN2UTNjRjZkdjM4UmZ58CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.jpeg)
Weex 的優缺點
首先肯定需要看看優缺點,優點用來判斷自己的場景适不适合做這個技術,缺點來看自己的場景會不會被限制住,有沒有辦法解決和繞開。
優點:
- js 能寫業務,跨平台,熱更新
- Weex 能用 Vue 的 framework,貼近我們的技術棧
- Weex 比 RN 更輕量,可以分包,每個頁面一個執行個體性能更好
- Weex 解決了 RN 已經存在的一些問題,在 RN 的基礎上進行開發
- 有良好的擴充性,比較好擴充新的 Component 和 Module
缺點:
- 文檔不全,資料少,社群幾乎等于沒有,issue 堆積,背景 issue 的方式改到了 JIRA 上,很多開發者都不了解
- bug 多,不穩定,遇到多次斷崖式更新
- Component 和 Module 不足以覆寫功能
其實總結起來就是起步晚的國産貨,優點就不贅述了。主要看缺點會不會限制住業務場景,有沒有對應的解決方案。
相關資料比較少,好在能看到源碼,有了源碼多花點時間琢磨,肯定是能繼續下去的,順着源碼看過去,文檔不全的問題也解決了,主要是發現了
Weex
提供了非常多文檔上沒有寫的好屬性和方法。
項目起步比較晚,
bug
比較多,更新也是斷崖式的,我們最後采用源碼內建的方法,發現有
bug
就修源碼,并給官方提
PR
,我們團隊提的很多
PR
也被官方采納,主要還是每次版本更新比較浪費時間,一方面要看更新日志,還要對源碼進行
diff
,如果官方已經修複了就删除我們自己的更新檔。這塊确實是會浪費時間一點,但是
RN
想要自己擴充也是需要經曆這個陣痛的。
提供的
Component
Module
不足以完成業務需求,當然官方也提供了擴充對應插件化的方式,嘗試擴充了幾個插件具備原生知識擴充起來也比較快,并且我們一開始就決定盡量少用官方的
Module
,盡量
Module
都由我們的用戶端自己擴充,一方面不會受到官方的
Module bug
或者不向下相容時的影響,另一方面在擴充原生
Module
的同時能了解其機制,還能讓擴充的
Module
都配合我們的業務。
接入成本與學習成本
我們主要的技術棧是圍繞着
Vue
建立的,自己做了統一的腳手架,已經适配了背景系統、微信公衆号、小程式、自助機等多端的項目,就差
APP
的解決方案了,如果能用
Vue
的基礎去接入,就完善了整個前端技術鍊,配合腳手架和
Vue
的文法基礎項目間的切換成本就會很低,開發效率會很高。
基于
Vue
的技術棧,讓我們寫業務的同學能很快适應,拆分元件,
widget
插件化,
mixins
這些相關的使用都能直接用上,剩下需要學習的就是
Weex
的
Component
Module
的使用及
css
的支援性,我們腳手架接入之後也直接支援
sass/less/styule
,整個過程讓新同學上手,半天的時候見能搭建出一個完整的
demo
頁面,上手開發很快。總體來說,成本對于我們來說是一個大的優勢
開發體驗與使用者體驗
上圖是我們通過改進最後給出的
開發的方案,以腳手架為核心的開發模式。
開發體驗基于
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
整體的運作原理,這裡對流程做一個大概的介紹,後面每一步都會有詳細的介紹。
Weex
提供不同的
framework
解析,可以用
.we
.vue
檔案寫業務,然後通過
webpack
進行打包編譯生成
js bundle
,編譯過程中主要是用了
weex
相關的
loader
,
對打包好的
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 SDK
上面我們分析了大概的
Weex
架構,也簡單介紹了一下運作起來的流程,接下來我們基于
的源碼來詳細看一下每一步是如何進行的,
是基于
Weex
的二次封裝,用戶端運作的第一個部分就是初始化
Weex
sdk
。
初始化
Weex sdk
主要完成下面四個事情:
- 關鍵節點記錄監控資訊
- 初始化 SDK 環境,加載并運作 js framework
- 注冊 Components、Modules、Handlers
- 如果是在開發環境初始化模拟器嘗試連接配接本地 server
在
Weex
的基礎上做了很多擴充,
Weex
的主要流程就是上面一些,
主要的代碼流程就是下面這樣的。
+ (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
的模式将錯誤記錄到自己的監控中,這部分代碼不是運作重點有興趣的同學就自己研究吧。
初始化 SDK 環境
這是最主要的一部初始化工作,通過 [BMConfigManager initWeexSDK];
也是在這個時機注入擴充。我們将我們的擴充放在
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];
});
}
Components 注冊
小白同學可能會比較疑惑為什麼
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
的注冊。
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]];
}
handler 注入
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
一直都還沒有調用,下面就是加載這個檔案了。
加載并運作 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:與用戶端互動的一些方法
運作 framework
首先注冊完上面所提到的三個子產品之後,
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 執行過程
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
也是調用的這個方法。
registerComponents
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
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 執行個體初始化
前面鋪墊了非常多的初始化流程,就是為了在将一個頁面是如何展示的過程中能清晰一點,前面相當于在做準備工作,這個時候我們來看
Weex
執行個體的初始化。
通過配置檔案将首頁的 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 的執行
js bundle
就是寫的業務代碼了,大家可以寫一個簡單的代碼儲存一下看看,由于使用了
Weex
loader
,具體的代碼肯定和正常的
js
代碼不一樣,經過轉換主要還是
<template>
<style>
部分,這兩部分會被轉換成兩個
JSON
,放在兩個閉包中。上面已經說到了最後是執行了
new Function
,具體的執行步驟在
,由于代碼太長,我們主要看核心的部分。
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
的描述方式傳遞給用戶端。
繪制 Native UI
;通過
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
注冊的時候有這兩個參數。
append=tree
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
繪制任務太多堆積在一起影響同步隊列任務的執行。
istemplate=true
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
執行個體對應的生命周期方法。
至此頁面就已經渲染出來了,頁面渲染完成之後,那麼點選事件是怎麼做的呢?
事件傳遞
全局事件
在了解事件如何發生傳遞之前,我們先看看事件有幾種類型,
封裝了路由的事件,将這些事件封裝在元件上,在
Vue
模闆上提供一個
對象,在
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
是通過類似
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 的使用
上面已經講了
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
}
})
}
}
requireModule
我們通過
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
的技術解決方案
并開源出來,解決了被大家所诟病的環境問題,提供更多豐富的
Component
Module
解決實際的業務問題。目前已有上千開發者有過開發體驗,在不斷吐槽中改進我們的方案,穩定了底層方案,建構了新的插件化方式,目前已經有開發者貢獻了一些插件,也收集到開發者已上線的
40+ APP
的案例,還有非常多的
APP
在開發過程中。希望我們的方案能幫助到
APP
開發中的你。
下面是一些通過
上線的
APP
案例
原文釋出時間為:2018年06月08日
原文作者:還是怕麻煩
本文來源:
掘金 https://juejin.im/entry/5b3a29f95188256228041f46如需轉載請聯系原作者