天天看點

深入了解 WeexWeex

Weex

上一篇文章

講到了混合應用簡單的發展史,本文以

Weex

為例分析一下混合應用,本文并非是介紹

Weex

是怎麼使用的,如果想要了解怎麼使用,不如了解一下

Eros

的解決方案,主要想剖析一下

Weex

的原理,了解

Weex

的運作機制。

為什麼要選擇 Weex

首先想聊一聊我們為什麼選擇

Weex

。上一篇文章結尾對

Weex

ReactNative

進行了簡要的分析,在我們做技術選型時大環境下

RN

不管從哪方面來說都是一個更好的方案,更多的對比可以去

weex&ReactNative對比

看看,在做技術選型的時候也在不斷的問,為什麼?最後大概從下面幾個方面得到了一個相對好的選擇。

深入了解 WeexWeex

Weex 的優缺點

首先肯定需要看看優缺點,優點用來判斷自己的場景适不适合做這個技術,缺點來看自己的場景會不會被限制住,有沒有辦法解決和繞開。

深入了解 WeexWeex

優點:

  • 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

都配合我們的業務。

接入成本與學習成本

深入了解 WeexWeex

我們主要的技術棧是圍繞着

Vue

建立的,自己做了統一的腳手架,已經适配了背景系統、微信公衆号、小程式、自助機等多端的項目,就差

APP

的解決方案了,如果能用

Vue

的基礎去接入,就完善了整個前端技術鍊,配合腳手架和

Vue

的文法基礎項目間的切換成本就會很低,開發效率會很高。

基于

Vue

的技術棧,讓我們寫業務的同學能很快适應,拆分元件,

widget

插件化,

mixins

這些相關的使用都能直接用上,剩下需要學習的就是

Weex

Component

Module

的使用及

css

的支援性,我們腳手架接入之後也直接支援

sass/less/styule

,整個過程讓新同學上手,半天的時候見能搭建出一個完整的

demo

頁面,上手開發很快。總體來說,成本對于我們來說是一個大的優勢

開發體驗與使用者體驗

深入了解 WeexWeex

上圖是我們通過改進最後給出的

開發的方案,以腳手架為核心的開發模式。

開發體驗基于

Vue

的方式,各種文法都已經在腳手架那層抹平了,開發起來和之前的開發模式基本一緻,開發調試的方式

Weex

提供了獨立的子產品支援,了解原理之後,我們很快做了儲存即重新整理的功能,加上本身

Weex debug

debug

頁面,

js

也能進行調試,用戶端也支援了日志輸出,開發體驗整體來看還比較流暢,确實是不如

web

開發那麼自然,但是我們通過對腳手架的改造,對用戶端支援熱重新整理功能,及原生提供的一些工具,大大的改善了開發體驗。

使用者體驗方面整體性能對比

RN

有了提高,站在

RN

的肩膀上,确實解決了很多性能的問題,首次的白屏時間,我們采用的是内置包,并且配合我們的熱更新機制,是能保證用戶端打開的時候,一定是有對應的内容的,不需要額外去加載資源,白屏時間也有了保證。頁面切換的時候我們采用多頁面的方式去實作

Weex

,配合我們自己擴充的路由機制每個頁面是一個單獨的

Weex

執行個體,是以每個頁面單獨渲染的性能和效率要更好,并且我們也一直在做預加載的方案,雖然說對于性能改善的效果不是很明顯,但是每一小步都是可以減少頁面間切換的白屏時間的。

性能監控和容災處理

Weex

自己本身就做了很多性能監控,隻需要對性能資料接入我們的監控系統,就能展示出對應的性能資料,目前從監控效果上來看确實實作了

Weex

對性能的承諾。

深入了解 WeexWeex

容災處理用于處理

jsBundle

通路失敗的情況,

Weex

自己具備容災處理的方案,需要開發者自己做改造進行降級處理,展示頁⾯面時,用戶端會加載對應如果用戶端加載

js bundle

失敗可以啟用

webView

通路,展示

HTML

端,但是體驗會非常不好,我們采用内置包 + 熱更新的機制,保證我們不會出現包解析失敗或者通路不到的問題,如果釋出的包有問題,可以緊急再釋出,使用者立馬會接收到更新,并且根據配置告知使用者是否立馬更新,想要做的更好,可以儲存一個穩定版本的包在使用者手機中,遇到解析錯誤崩潰的問題,立即啟用穩定版本的内置包,但是這樣會導緻包比較大,如果需要穩定的容災處理可以考慮這樣去實作。

在完成了方案調研和簡單的

demo

測試,我們就開始落地,圍繞的

Weex

也做了非常多的周邊環境的建設,比如現有腳手架的改造以支援

Weex

的開發、熱更新機制如何建構、用戶端底層需要哪些支援、如何做擴充能與源碼進行解耦等等。

還是說回正題,接下來介紹一下

Weex

整體的架構。

Weex 整體架構

深入了解 WeexWeex

從上面這個圖可以看出

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

和用戶端都準備好之後,就開始等待用戶端什麼時候展示頁面。

深入了解 WeexWeex

當需要展示頁面時,用戶端會初始化

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

深入了解 WeexWeex

上面我們分析了大概的

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

已經不存在了)

深入了解 WeexWeex
// 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 執行個體初始化

深入了解 WeexWeex

前面鋪墊了非常多的初始化流程,就是為了在将一個頁面是如何展示的過程中能清晰一點,前面相當于在做準備工作,這個時候我們來看

Weex

執行個體的初始化。

通過配置檔案将首頁的 URL 配置在配置檔案中,用戶端能直接拿到首頁直接進行初始化。

用戶端通過

_renderWithURL

去加載首頁的

URL

URL

不管是放在本地還是伺服器上,其實就是一個

js bundle

檔案,就是一個經過特殊

loader

打包的

js

檔案,加載到這個檔案之後,将這個調用到

js framework

createInstance
深入了解 WeexWeex
/*
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

執行個體對應的生命周期方法。

至此頁面就已經渲染出來了,頁面渲染完成之後,那麼點選事件是怎麼做的呢?

事件傳遞

深入了解 WeexWeex

全局事件

在了解事件如何發生傳遞之前,我們先看看事件有幾種類型,

封裝了路由的事件,将這些事件封裝在元件上,在

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 的使用

深入了解 WeexWeex

上面已經講了

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

案例

深入了解 WeexWeex
深入了解 WeexWeex
深入了解 WeexWeex

原文釋出時間為:2018年06月08日

原文作者:還是怕麻煩

本文來源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需轉載請聯系原作者

繼續閱讀