深入了解 Weex

栏目: 编程语言 · 发布时间: 5年前

内容简介:Weex本文以Weex为例分析一下混合应用,本文并非是介绍Weex是怎么使用的,如果想要了解怎么使用,不如了解一下 Eros 的解决方案,主要想剖析一下Weex的原理,了解Weex的运行机制。为什么要选择 Weex
编辑推荐:
本文来自aliyun,文章介绍了Weex 的优缺点、Weex的原理以及Weex的运行机制等相关内容。

Weex

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

为什么要选择 Weex

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

深入了解 Weex

Weex 的优缺点

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

深入了解 Weex

优点:

js 能写业务,跨平台,热更新

Weex 能用 Vue 的 framework,贴近我们的技术栈

Weex 比 RN 更轻量,可以分包,每个页面一个实例性能更好

Weex 解决了 RN 已经存在的一些问题,在 RN 的基础上进行开发

有良好的扩展性,比较好扩展新的 Component 和 Module

缺点:

文档不全,资料少,社区几乎等于没有,issue 堆积,后台 issue 的方式改到了 JIRA 上,很多开发者都不了解

bug 多,不稳定,遇到多次断崖式更新

Component 和 Module 不足以覆盖功能

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

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

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

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

接入成本与学习成本

深入了解 Weex

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

基于Vue的技术栈,让我们写业务的同学能很快适应,拆分组件,widget插件化,mixins这些相关的使用都能直接用上,剩下需要学习的就是Weex的Component和Module的使用及css的支持性,我们脚手架接入之后也直接支持sass/less/styule,整个过程让新同学上手,半天的时候见能搭建出一个完整的demo页面,上手开发很快。总体来说,成本对于我们来说是一个大的优势

开发体验与用户体验

深入了解 Weex

上图是我们通过改进最后给出的 Eros 开发的方案,以脚手架为核心的开发模式。

开发体验基于Vue的方式,各种语法都已经在脚手架那层抹平了,开发起来和之前的开发模式基本一致,开发调试的方式Weex提供了独立的模块支持,了解原理之后,我们很快做了保存即刷新的功能,加上本身Weex debug提供的debug页面,js也能进行调试,客户端也支持了日志输出,开发体验整体来看还比较流畅,确实是不如web开发那么自然,但是我们通过对脚手架的改造,对客户端支持热刷新功能,及原生提供的一些工具,大大的改善了开发体验。

用户体验方面整体性能对比RN有了提高,站在RN的肩膀上,确实解决了很多性能的问题,首次的白屏时间,我们采用的是内置包,并且配合我们的热更新机制,是能保证客户端打开的时候,一定是有对应的内容的,不需要额外去加载资源,白屏时间也有了保证。页面切换的时候我们采用多页面的方式去实现Weex,配合我们自己扩展的路由机制每个页面是一个单独的Weex实例,所以每个页面单独渲染的性能和效率要更好,并且我们也一直在做预加载的方案,虽然说对于性能改善的效果不是很明显,但是每一小步都是可以减少页面间切换的白屏时间的。

性能监控和容灾处理

Weex自己本身就做了很多性能监控,只需要对性能数据接入我们的监控系统,就能展示出对应的性能数据,目前从监控效果上来看确实实现了Weex对性能的承诺。

深入了解 Weex

容灾处理用于处理jsBundle访问失败的情况,Weex自己具备容灾处理的方案,需要开发者自己做改造进行降级处理,展示页?面时,客户端会加载对应如果客户端加载js bundle失败可以启用webView访问,展示HTML端,但是体验会非常不好,我们采用内置包 + 热更新的机制,保证我们不会出现包解析失败或者访问不到的问题,如果发布的包有问题,可以紧急再发布,用户立马会接收到更新,并且根据配置告知用户是否立马更新,想要做的更好,可以保存一个稳定版本的包在用户手机中,遇到解析错误崩溃的问题,立即启用稳定版本的内置包,但是这样会导致包比较大,如果需要稳定的容灾处理可以考虑这样去实现。

在完成了方案调研和简单的demo测试,我们就开始落地,围绕的Weex也做了非常多的周边环境的建设,比如现有脚手架的改造以支持Weex的开发、热更新机制如何构建、客户端底层需要哪些支持、如何做扩展能与源码进行解耦等等。

还是说回正题,接下来介绍一下Weex整体的架构。

Weex 整体架构

深入了解 Weex

从上面这个图可以看出Weex整体的运行原理,这里对流程做一个大概的介绍,后面每一步都会有详细的介绍。

Weex提供不同的framework解析,可以用.we和.vue文件写业务,然后通过webpack进行打包编译生成js bundle,编译过程中主要是用了weex相关的loader,Eros 对打包好的js bundle生成了zip包,还会生成差分包的逻辑。不管生成的是什么文件,最后都是将js bundle部署到服务器或者CDN节点上。

客户端启动时发现引入了Weex sdk,首先会初始化环境及一些监控,接着会运行本地的main.js即js framework,js framework会初始化一些环境,当js framework和客户端都准备好之后,就开始等待客户端什么时候展示页面。

深入了解 Weex

当需要展示页面时,客户端会初始化Weex实例,就是WXSDKInstance,Weex实例会加载对应的js bundle文件,将整个js bundle文件当成一个字符串传给js framework,还会传递一些环境参数。js framework开始在JavaScript Core中执行js bundle,将js bundle执行翻译成virtual DOM,准备好数据双绑,同时将vDOM进行深度遍历解析成vNode,对应成一个个的渲染指令通过js Core传递给客户端。

js framework调用Weex SDK初始化时准备好的callNative、addElement 等方法,将指令传递给 native,找到指令对应的Weex Component执行渲染绘制,每渲染一个组件展示一个,Weex性能瓶颈就是来自于逐个传递组件的过程,调用module要稍微复杂一些,后面会详解,事件绑定后面也会详解。至此一个页面就展示出来了。

Weex SDK

深入了解 Weex

上面我们分析了大概的Weex架构,也简单介绍了一下运行起来的流程,接下来我们基于 Eros 的源码来详细看一下每一步是如何进行的,Eros 是基于Weex的二次封装,客户端运行的第一个部分就是初始化Weex的sdk。

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

关键节点记录监控信息

初始化 SDK 环境,加载并运行 js framework

注册 Components、Modules、Handlers

如果是在开发环境初始化模拟器尝试连接本地 server

Eros 在Weex的基础上做了很多扩展,Weex的主要流程就是上面一些,Eros 主要的代码流程就是下面这样的。

+ (void)configDefaultData

{

/* 启动网络变化监控 */

AFNetworkReachabilityManager *reachability = [AFNetworkReachabilityManager sharedManager];

[reachability startMonitoring];

/** 初始化Weex */

[BMConfigManager initWeexSDK];

BMPlatformModel *platformInfo = TK_PlatformInfo();

/** 设置sdimage减小内存占用 */

[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];

[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];

[[SDImageCache sharedImageCache] setShouldCacheImagesInMemory:NO];

/** 设置统一请求url */

[[YTKNetworkConfig sharedConfig] setBaseUrl:platformInfo.url.request];

[[YTKNetworkConfig sharedConfig] setCdnUrl:platformInfo.url.image];

/** 应用最新js资源文件 */

[[BMResourceManager sharedInstance] compareVersion];

/** 初始化数据库 */

[[BMDB DB] configDB];

/** 设置 HUD */

[BMConfigManager configProgressHUD];

/* 监听截屏事件 */

// [[BMScreenshotEventManager shareInstance] monitorScreenshotEvent];

}

初始化监控记录

Weex其中一个优点就是自带监控,自己会记录一下简单的性能指标,比如初始化SDK时间,请求成功和失败,js报错这些信息,都会自动记录到WXMonitor中。

Weex将错误分成两类,一类是global,一类是instance。在iOS中WXSDKInstance初始化之前,所有的全局的global操作都会放在WXMonitor的globalPerformanceDict中。当WXSDKInstance初始化之后,即 WXPerformanceTag中instance以下的所有操作都会放在instance.performanceDict`中。

global的监控

SDKINITTIME:SDK 初始化监控

SDKINITINVOKETIME:SDK 初始化 invoke 监控

JSLIBINITTIME:js 资源初始化监控

instance监控

NETWORKTIME:网络请求监控

COMMUNICATETIME:交互事件监控

FIRSETSCREENJSFEXECUTETIME:首屏 js 加载监控

SCREENRENDERTIME:首屏渲染时间监控

TOTALTIME:渲染总时间

JSTEMPLATESIZE:js 模板大小

如果想要接入自己的监控系统,阅读一下WXMonitor相关的代码,可以采用一些AOP的模式将错误记录到自己的监控中,这部分代码不是运行重点有兴趣的同学就自己研究吧。

初始化 SDK 环境

这是最主要的一部初始化工作,通过 [BMConfigManager initWeexSDK];Eros 也是在这个时机注入扩展。我们将我们的扩展放在registerBmComponents、registerBmModules、registerBmHandlers这三个方法中,然后统一注入,避免与Weex本身的代码耦合太深。

+ (void)initWeexSDK

{

[WXSDKEngine initSDKEnvironment];

[BMConfigManager registerBmHandlers];

[BMConfigManager registerBmComponents];

[BMConfigManager registerBmModules];

#ifdef DEBUG

[WXDebugTool setDebug:YES];

[WXLog setLogLevel:WeexLogLevelLog];

[[BMDebugManager shareInstance] show];

// [[ATManager shareInstance] show];

#else

[WXDebugTool setDebug:NO];

[WXLog setLogLevel:WeexLogLevelError];

#endif

}

下面是我们部分的扩展,详细的扩展可以看看我们的源码,为了与官方的源码集成扩展解耦我们将我们的注入时机放在了Weex initSDKEnvironment之后。

// 扩展 Component

+ (void)registerBmComponents

{

NSDictionary *components = @{

@"bmmask": NSStringFromClass([BMMaskComponent class]),

@"bmpop": NSStringFromClass([BMPopupComponent class])

...

};

for (NSString *componentName in components) {

[WXSDKEngine registerComponent:componentName withClass:NSClassFromString([components valueForKey:componentName])];

}

}

// 扩展 Moudles

+ (void)registerBmModules

{

NSDictionary *modules = @{

@"bmRouter" : NSStringFromClass([BMRouterModule class]),

@"bmAxios": NSStringFromClass([BMAxiosNetworkModule class])

...

};

for (NSString *moduleName in modules.allKeys) {

[WXSDKEngine registerModule:moduleName withClass:NSClassFromString([modules valueForKey:moduleName])];

}

}

// 扩展 Handlers

+ (void)registerBmHandlers

{

[WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)];

[WXSDKEngine registerHandler:[WXBMNetworkDefaultlpml new] withProtocol:@protocol(WXResourceRequestHandler)];

...

}

初始化SDK就是执行WXSDKEngine这个文件的内容,最主要注册当前的Components、Modules、handlers。

+ (void)registerDefaults

{

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

[self _registerDefaultComponents];

[self _registerDefaultModules];

[self _registerDefaultHandlers];

});

}

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已经不存在了)

深入了解 Weex

// 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 jsBridge;

@property (nonatomic, strong) id 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;

@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

// Send a resource request with a delegate

- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id)delegate;

@optional

// Cancel the ongoing request

- (void)cancelRequest:(WXResourceRequest *)request;

@end

WXResourceRequestHandler中规定了两个方法,一个是加载资源的请求方法,一个是需要请求的方法,然后看一下WXResourceRequestHandlerDefaultImpl类:

//

// WXResourceRequestHandlerDefaultImpl.m

//

#pragma mark - WXResourceRequestHandler

- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id)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 原型链方法

创建于客户端通信桥

弥补环境差异

挂载全局属性方法及 VM 原型链方法

刚才已经讲了DSL是什么,js framework中非常重要的功能就是做好不同宿主环境和语言中的兼容。主要是通过一些接口来与客户端进行交互,适配前端框架实际上是为了适配iOS、android和浏览器。这里主要讲一讲和客户端进行适配的接口。

getRoot:获取页面节点

receiveTasks:监听客户端任务

registerComponents:注册 Component

registerMoudles:注册 Module

init: 页面内部生命周期初始化

createInstance: 页面内部生命周期创建

refreshInstance: 页面内部生命周期刷新

destroyInstance: 页面内部生命周期销毁 ...

这些接口都可以在WXBridgeContext里看到,都是js framework提供给客户端调用的。其中Weex SDK初始化的时候,提到的registerComponents和registerMoudles也是调用的这个方法。

registerComponents

js framework中registerComponents的实现可以看出,前端只是做了一个map缓存起来,等待解析vDOM的时候进行映射,然后交给原生组件进行渲染。

// /runtime/frameworks/legacy/static/register.js

export function registerComponents (components) {

if (Array.isArray(components)) {

components.forEach(function register (name) {

/* istanbul ignore if */

if (!name) {

return

}

if (typeof name === 'string') {

nativeComponentMap[name] = true

}

/* istanbul ignore else */

else if (typeof name === 'object' && typeof name.type === 'string') {

nativeComponentMap[name.type] = name

}

})

}

}

registerMoudles

registerMoudles时也差不多,放在了nativeModules这个对象上缓存起来,但是使用的时候要复杂一些,后面也会讲到。

// /runtime/frameworks/legacy/static/register.js

export function registerModules (modules) {

/* istanbul ignore else */

if (typeof modules === 'object') {

initModules(modules)

}

}

// /runtime/frameworks/legacy/app/register.js

export function initModules (modules, ifReplace) {

for (const moduleName in modules) {

// init `modules[moduleName][]`

let methods = nativeModules[moduleName]

if (!methods) {

methods = {}

nativeModules[moduleName] = methods

}

// push each non-existed new method

modules[moduleName].forEach(function (method) {

if (typeof method === 'string') {

method = {

name: method

}

}

if (!methods[method.name] || ifReplace) {

methods[method.name] = method

}

})

}

}

创建于客户端通信桥

js framework是客户端和前端业务代码沟通的桥梁,所以更重要的也是bridge,基本的桥的设计上一篇也讲了,Weex选择的是直接提供方法供js调用,也直接调用js的方法。

客户端调用js直接使用callJs,callJs是js提供的方法,放在当前线程中,供客户端调用,包括DOM事件派发、module调用时的时间回调,都是通过这个接口通知js framework,然后再调用缓存在js framework中的方法。

js调用客户端使用callNative,客户端也会提供很多方法给js framework供,framework调用,这些方法都可以在WXBridgeContext中看到,callNative只是其中的一个方法,实际代码中还有很多方法,比如addElement、updateAttrs等等

弥补环境差异

除了用于完成功能的主要方法,客户端还提供一些方法来弥补上层框架在js中调用时没有的方法,就是环境的差异,弥补兼容性的差异,setTimeout、nativeLog等,客户端提供了对应的方法,js framework也无法像在浏览器中调用这些方法一样去调用这些方法,所以需要双方采用兼容性的方式去支持。

还有一些ployfill的方法,比如Promise,Object.assign,这些ployfill能保证一部分环境和浏览器一样,降低我们写代码的成本。

执行完毕

执行js framework其他的过程就不一一展开了,主要是一些前端代码之间的互相调用,这部分也承接了很多Weex历史遗留的一些兼容问题,有时候发现一些神奇的写法,可能是当时为了解决一些神奇的bug吧,以及各种istanbul ignore的注释。

执行完js framework之后客户端frameworkLoadFinished会被置位 YES,之前遗留的任务也都会在js framework执行完毕之后执行,以完成整个初始化的流程。

客户端会先执行js-service,因为js-service只是需要在JavaScript Core中执行字符串,所以直接执行executeAllJsService就行了,并不需要调用js framework的方法,只是让当前内存环境中有js service的变量对象。

然后将_methodQueue中的任务拿出来遍历执行。这里就是执行缓存队列中的registerComponents、registerModules、registerMethods。上面也提到了具体两者是怎么调用的,详细的代码都是在这里。

执行完毕之后,按理说这个js Thread应该关闭,然后被回收,但是我们还需要让这个js framework一直运行在js Core中,所以这个就需要给js Thread开启了一个runloop,让这个js Thread一直处于执行状态

Weex 实例初始化

深入了解 Weex

前面铺垫了非常多的初始化流程,就是为了在将一个页面是如何展示的过程中能清晰一点,前面相当于在做准备工作,这个时候我们来看Weex实例的初始化。Eros 通过配置文件将首页的 URL 配置在配置文件中,客户端能直接拿到首页直接进行初始化。

客户端通过 _renderWithURL去加载首页的URL,这个URL不管是放在本地还是服务器上,其实就是一个js bundle文件,就是一个经过特殊loader打包的js文件,加载到这个文件之后,将这个调用到js framework中的 createInstance。

深入了解 Weex

/*

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代码不一样,经过转换主要还是