深刻了解 Weex

Weex

上一篇文章講到了混合應用簡單的發展史,本文以Weex爲例分析一下混合應用,本文並不是是介紹Weex是怎麼使用的,若是想要了解怎麼使用,不如瞭解一下 Eros 的解決方案,主要想剖析一下Weex的原理,瞭解Weex的運行機制。javascript

爲何要選擇 Weex

首先想聊一聊咱們爲何選擇Weex。上一篇文章結尾對WeexReactNative進行了簡要的分析,在咱們作技術選型時大環境下RN無論從哪方面來講都是一個更好的方案,更多的對比能夠去 weex&ReactNative對比 看看,在作技術選型的時候也在不斷的問,爲何?最後大概從下面幾個方面獲得了一個相對好的選擇。css

Weex 的優缺點

首先確定須要看看優缺點,優勢用來判斷本身的場景適不適合作這個技術,缺點來看本身的場景會不會被限制住,有沒有辦法解決和繞開。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

提供的ComponentModule不足以完成業務需求,固然官方也提供了擴展對應插件化的方式,嘗試擴展了幾個插件具有原生知識擴展起來也比較快,而且咱們一開始就決定儘可能少用官方的Module,儘可能Module都由咱們的客戶端本身擴展,一方面不會受到官方的Module bug或者不向下兼容時的影響,另外一方面在擴展原生Module的同時能瞭解其機制,還能讓擴展的Module都配合咱們的業務。webpack

接入成本與學習成本

咱們主要的技術棧是圍繞着Vue創建的,本身作了統一的腳手架,已經適配了後臺系統、微信公衆號、小程序、自助機等多端的項目,就差APP的解決方案了,若是能用Vue的基礎去接入,就完善了整個前端技術鏈,配合腳手架和Vue的語法基礎項目間的切換成本就會很低,開發效率會很高。ios

基於Vue的技術棧,讓咱們寫業務的同窗能很快適應,拆分組件,widget插件化,mixins這些相關的使用都能直接用上,剩下須要學習的就是WeexComponentModule的使用及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總體的運行原理,這裏對流程作一個大概的介紹,後面每一步都會有詳細的介紹。

Weex提供不一樣的framework解析,能夠用.we.vue文件寫業務,而後經過webpack進行打包編譯生成js bundle,編譯過程當中主要是用了weex相關的loaderEros 對打包好的js bundle生成了zip包,還會生成差分包的邏輯。無論生成的是什麼文件,最後都是將js bundle部署到服務器或者CDN節點上。

客戶端啓動時發現引入了Weex sdk,首先會初始化環境及一些監控,接着會運行本地的main.jsjs frameworkjs framework會初始化一些環境,當js framework和客戶端都準備好以後,就開始等待客戶端何時展現頁面。

當須要展現頁面時,客戶端會初始化Weex實例,就是WXSDKInstanceWeex實例會加載對應的js bundle文件,將整個js bundle文件當成一個字符串傳給js framework,還會傳遞一些環境參數。js framework開始在JavaScript Core中執行js bundle,將js bundle執行翻譯成virtual DOM,準備好數據雙綁,同時將vDOM進行深度遍歷解析成vNode,對應成一個個的渲染指令經過js Core傳遞給客戶端。

js framework調用Weex SDK初始化時準備好的callNativeaddElement 等方法,將指令傳遞給 native,找到指令對應的Weex Component執行渲染繪製,每渲染一個組件展現一個,Weex性能瓶頸就是來自於逐個傳遞組件的過程,調用module要稍微複雜一些,後面會詳解,事件綁定後面也會詳解。至此一個頁面就展現出來了。

Weex SDK

上面咱們分析了大概的Weex架構,也簡單介紹了一下運行起來的流程,接下來咱們基於 Eros 的源碼來詳細看一下每一步是如何進行的,Eros 是基於Weex的二次封裝,客戶端運行的第一個部分就是初始化Weexsdk

初始化Weex sdk主要完成下面四個事情:

  • 關鍵節點記錄監控信息
  • 初始化 SDK 環境,加載並運行 js framework
  • 註冊 Components、Modules、Handlers
  • 若是是在開發環境初始化模擬器嘗試鏈接本地 server

ErosWeex的基礎上作了不少擴展,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。在iOSWXSDKInstance初始化以前,全部的全局的global操做都會放在WXMonitorglobalPerformanceDict中。當WXSDKInstance初始化以後,即 WXPerformanceTaginstance如下的全部操做都會放在instance.performanceDict`中。

global的監控

  • SDKINITTIME:SDK 初始化監控
  • SDKINITINVOKETIME:SDK 初始化 invoke 監控
  • JSLIBINITTIME:js 資源初始化監控

instance監控

  • NETWORKTIME:網絡請求監控
  • COMMUNICATETIME:交互事件監控
  • FIRSETSCREENJSFEXECUTETIME:首屏 js 加載監控
  • SCREENRENDERTIME:首屏渲染時間監控
  • TOTALTIME:渲染總時間
  • JSTEMPLATESIZE:js 模板大小

若是想要接入本身的監控系統,閱讀一下WXMonitor相關的代碼,能夠採用一些AOP的模式將錯誤記錄到本身的監控中,這部分代碼不是運行重點有興趣的同窗就本身研究吧。

初始化 SDK 環境

這是最主要的一部初始化工做,經過 [BMConfigManager initWeexSDK];Eros 也是在這個時機注入擴展。咱們將咱們的擴展放在registerBmComponentsregisterBmModulesregisterBmHandlers這三個方法中,而後統一注入,避免與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這個文件的內容,最主要註冊當前的ComponentsModuleshandlers

+ (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爲註冊在jsfmComponent的名字(即標籤的名字),clazzComponent對應的類,properties爲一些擴展屬性;

在這個方法中又調用了WXComponentFactory的方法registerComponent:name withClass:clazz withPros:properties來註冊ComponentWXComponentFactory是一個單例,負責解析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實例做爲valuekeyComponentname保存到 _componentConfigs中(_componentConfigs 是一個字典),config中保存了Component的全部暴露給js的方法,繼續看一下WXComponentConfigregisterMethods方法:

- (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的方法;而後讓咱們在回到WXSDKEngineregisterComponent: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中的bridgeMgrregisterComponents:方法;WXSDKManager持有一個WXBridgeManager,這個WXBridgeManager又有一個的屬性是WXBridgeContextWXBridgeContext又持有一個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方法,而後再調用WXBridgeContextregisterComponents方法,進行組件的註冊。

// 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 frameworkregisterComponents註冊全部相關的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。註冊模塊不容許同名模塊。將namekeyvalueWXModuleConfig存入_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對象中了,這裏至少有兩個方法addEventListenerremoveAllEventListeners,因此這裏返回出來的方法都具有上面兩個方法。

- (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 注入

ComponentModule你們常用還比較能理解,可是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

在官方GitHubruntime 目錄下放着一堆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是要兼容三端的,因此iOSandroidweb的差別性一定是須要去抹平的,他們接受指令的方式和方法都有可能不一樣,好比:客戶端設計的是createBodyaddElement,而webcreateElementappendChild等。

除了指令的差別,還有上層業務語言的不一樣,好比Weex支持VueRax,甚至可能支持React,只要是符合js framework的實現,就能夠經過不一樣的接口渲染在不一樣的宿主環境下。咱們能夠稱這一層爲DSL,咱們也看看js framework這層的主要代碼

|-- index.js:入口文件
    |-- legacy:關於 VM 相關的主要方法
    |   |-- api:相關 vm 定義的接口
    |   |-- app:管理頁面間頁面實例的方法
    |   |-- core:實現數據監聽的方法
    |   |-- static:靜態方法
    |   |-- util:工具類函數
    |   |-- vm:解析指令相關
    |-- vanilla:與客戶端交互的一些方法
複製代碼

運行 framework

首先註冊完上面所提到的三個模塊以後,WXSDKEngine繼續往下執行,仍是先會調用到WXBridgeManager中的executeJsFramework,再調用到WXBridgeContextexecuteJsFramework,而後在子線程中執行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,往下會調用WXJSCoreBridgeinvokeMethod,這個就是就是調用對應的js framework提供的方法,同時會發現一個WXJSCoreBridge文件,這裏就是Weexbridge_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 原型鏈方法
  • 建立於客戶端通訊橋
  • 彌補環境差別

掛載全局屬性方法及 VM 原型鏈方法

剛纔已經講了DSL是什麼,js framework中很是重要的功能就是作好不一樣宿主環境和語言中的兼容。主要是經過一些接口來與客戶端進行交互,適配前端框架其實是爲了適配iOSandroid和瀏覽器。這裏主要講一講和客戶端進行適配的接口。

  • getRoot:獲取頁面節點
  • receiveTasks:監聽客戶端任務
  • registerComponents:註冊 Component
  • registerMoudles:註冊 Module
  • init: 頁面內部生命週期初始化
  • createInstance: 頁面內部生命週期建立
  • refreshInstance: 頁面內部生命週期刷新
  • destroyInstance: 頁面內部生命週期銷燬 ...

這些接口均可以在WXBridgeContext裏看到,都是js framework提供給客戶端調用的。其中Weex SDK初始化的時候,提到的registerComponentsregisterMoudles也是調用的這個方法。

registerComponents

js frameworkregisterComponents的實現能夠看出,前端只是作了一個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直接使用callJscallJsjs提供的方法,放在當前線程中,供客戶端調用,包括DOM事件派發、module調用時的時間回調,都是經過這個接口通知js framework,而後再調用緩存在js framework中的方法。

js調用客戶端使用callNative,客戶端也會提供不少方法給js framework供,framework調用,這些方法均可以在WXBridgeContext中看到,callNative只是其中的一個方法,實際代碼中還有不少方法,好比addElementupdateAttrs等等

彌補環境差別

除了用於完成功能的主要方法,客戶端還提供一些方法來彌補上層框架在js中調用時沒有的方法,就是環境的差別,彌補兼容性的差別,setTimeoutnativeLog等,客戶端提供了對應的方法,js framework也沒法像在瀏覽器中調用這些方法同樣去調用這些方法,因此須要雙方採用兼容性的方式去支持。

還有一些ployfill的方法,好比PromiseObject.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中的任務拿出來遍歷執行。這裏就是執行緩存隊列中的registerComponentsregisterModulesregisterMethods。上面也提到了具體二者是怎麼調用的,詳細的代碼都是在這裏

執行完畢以後,按理說這個js Thread應該關閉,而後被回收,可是咱們還須要讓這個js framework一直運行在js Core中,因此這個就須要給js Thread開啓了一個runloop,讓這個js Thread一直處於執行狀態

Weex 實例初始化

前面鋪墊了很是多的初始化流程,就是爲了在將一個頁面是如何展現的過程當中能清晰一點,前面至關於在作準備工做,這個時候咱們來看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實例。下一步就是調用各自DSLcreateInstance,並把對應須要的參數都傳遞過去

// /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這個方法,這個方法中作了不少補全原型鏈的方法,好比bundleDefinebundleBootstrap等等,這些都挺重要的,你們能夠看看 init 方法,就完成了上述的操做。

最主要的仍是下面這個方法,這裏會是最終執行js bundle的地方。執行完成以後將 Weex的單個頁面的實例放在instanceMapnew 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,具體的執行步驟在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,這裏就是一個遞歸遍歷的過程。

bundleRequirebundleBootstrap,這裏調用到了 bootstrapVm,這裏有一步我不是很明白。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個生命週期的鉤子,initcreatedreadydestroyed。除了生命週期,這裏還綁定了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的時候會有用到removeupdateAPI

最終繪製調用 appendChild,這裏封裝了全部和native有交互的方法。DOM操做大體就是addElementremoveElement等方法,調用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=treeistemplate=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,一種是treenode是先渲染父節點,再渲染子節點,而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還有事件的溝通,經過橋傳遞createFinishedrenderFinished事件,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",就會在建立組件的時候經過callAddEventevent傳遞給native,可是不會傳遞事件的回調方法,由於客戶端根本就不識別事件回調的方法,客戶端發現有事件屬性以後,就會對原生的事件進行事件綁定,在渲染組件的時候,每一個組件都會生成一個組件ID,就是reftype就是事件類型好比:clicklongpress等。

// 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將對應的事件,傳遞四個參數,reftypeeventdomChanges,經過bridge將這些參數傳遞給js frameworkbridge,可是到底層的時候還會攜帶一個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的事件回調處理成表達式,在綁定的時候一併傳給客戶端,因爲是表達式,因此客戶端也能夠識別表達式,客戶端在監聽原生事件觸發的時候,就直接執行表達式。這樣就省去了傳遞的過程。WeexbingdingX也是能夠用來處理相似頻繁觸發的js和客戶端之間的交互的,好比動畫。

module 的使用

上面已經講了module的註冊,最終調用js frameworkregisterModules注入全部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的技術解決方案 Eros並開源出來,解決了被你們所詬病的環境問題,提供更多豐富的ComponentModule解決實際的業務問題。目前已有上千開發者有過開發體驗,在不斷吐槽中改進咱們的方案,穩定了底層方案,構建了新的插件化方式,目前已經有開發者貢獻了一些插件,也收集到開發者已上線的40+ APP的案例,還有很是多的APP在開發過程當中。但願咱們的方案能幫助到APP開發中的你。

下面是一些經過 Eros 上線的APP案例

相關文章
相關標籤/搜索