深入了解 Weex

Weex

上一篇文章讲到了混合应用简单的发展史,本文以Weex为例分析一下混合应用,本文并非是介绍Weex是怎么使用的,如果想要了解怎么使用,不如了解一下 Eros 的解决方案,主要想剖析一下Weex的原理,了解Weex的运行机制。

为什么要选择 Weex

首先想聊一聊我们为什么选择Weex。上一篇文章结尾对WeexReactNative进行了简要的分析,在我们做技术选型时大环境下RN不管从哪方面来说都是一个更好的方案,更多的对比可以去 weex&ReactNative对比 看看,在做技术选型的时候也在不断的问,为什么?最后大概从下面几个方面得到了一个相对好的选择。

Weex 的优缺点

首先肯定需要看看优缺点,优点用来判断自己的场景适不适合做这个技术,缺点来看自己的场景会不会被限制住,有没有办法解决和绕开。

优点:

  • js 能写业务,跨平台,热更新
  • Weex 能用 Vue 的 framework,贴近我们的技术栈
  • Weex 比 RN 更轻量,可以分包,每个页面一个实例性能更好
  • Weex 解决了 RN 已经存在的一些问题,在 RN 的基础上进行开发
  • 有良好的扩展性,比较好扩展新的 Component 和 Module

缺点:

  • 文档不全,资料少,社区几乎等于没有,issue 堆积,后台 issue 的方式改到了 JIRA 上,很多开发者都不了解
  • bug 多,不稳定,遇到多次断崖式更新
  • Component 和 Module 不足以覆盖功能

其实总结起来就是起步晚的国产货,优点就不赘述了。主要看缺点会不会限制住业务场景,有没有对应的解决方案。

相关资料比较少,好在能看到源码,有了源码多花点时间琢磨,肯定是能继续下去的,顺着源码看过去,文档不全的问题也解决了,主要是发现了Weex提供了非常多文档上没有写的好属性和方法。

项目起步比较晚,bug比较多,更新也是断崖式的,我们最后采用源码集成的方法,发现有bug就修源码,并给官方提PR,我们团队提的很多PR也被官方采纳,主要还是每次版本更新比较浪费时间,一方面要看更新日志,还要对源码进行diff,如果官方已经修复了就删除我们自己的补丁。这块确实是会浪费时间一点,但是RN想要自己扩展也是需要经历这个阵痛的。

提供的ComponentModule不足以完成业务需求,当然官方也提供了扩展对应插件化的方式,尝试扩展了几个插件具备原生知识扩展起来也比较快,并且我们一开始就决定尽量少用官方的Module,尽量Module都由我们的客户端自己扩展,一方面不会受到官方的Module bug或者不向下兼容时的影响,另一方面在扩展原生Module的同时能了解其机制,还能让扩展的Module都配合我们的业务。

接入成本与学习成本

我们主要的技术栈是围绕着Vue建立的,自己做了统一的脚手架,已经适配了后台系统、微信公众号、小程序、自助机等多端的项目,就差APP的解决方案了,如果能用Vue的基础去接入,就完善了整个前端技术链,配合脚手架和Vue的语法基础项目间的切换成本就会很低,开发效率会很高。

基于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案例

关注下面的标签,发现更多相似文章
评论
说说你的看法