您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
深入了解 Weex
 
  15158  次浏览      18
  2018-12-28
 
编辑推荐:
本文来自aliyun,文章介绍了Weex 的优缺点、Weex的原理以及Weex的运行机制等相关内容。

Weex

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

为什么要选择 Weex

首先想聊一聊我们为什么选择Weex。上一篇文章结尾对Weex和ReactNative进行了简要的分析,在我们做技术选型时大环境下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想要自己扩展也是需要经历这个阵痛的。

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

接入成本与学习成本

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

基于Vue的技术栈,让我们写业务的同学能很快适应,拆分组件,widget插件化,mixins这些相关的使用都能直接用上,剩下需要学习的就是Weex的Component和Module的使用及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相关的loader,Eros 对打包好的js bundle生成了zip包,还会生成差分包的逻辑。不管生成的是什么文件,最后都是将js bundle部署到服务器或者CDN节点上。

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

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

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

Weex SDK

上面我们分析了大概的Weex架构,也简单介绍了一下运行起来的流程,接下来我们基于 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已经不存在了)

// 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 原型链方法

创建于客户端通信桥

弥补环境差异

挂载全局属性方法及 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实例的初始化。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实例。下一步就是调用各自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,具体的执行步骤在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,这里就是一个递归遍历的过程。

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绘制任务太多堆积在一起影响同步队列任务的执行。

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实例对应的生命周期方法。

至此页面就已经渲染出来了,页面渲染完成之后,那么点击事件是怎么做的呢?

事件传递

全局事件

在了解事件如何发生传递之前,我们先看看事件有几种类型,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",就会在创建组件的时候通过callAddEvent将event传递给native,但是不会传递事件的回调方法,因为客户端根本就不识别事件回调的方法,客户端发现有事件属性之后,就会对原生的事件进行事件绑定,在渲染组件的时候,每个组件都会生成一个组件ID,就是ref,type就是事件类型比如:click、longpress等。

// https://github.com/apache/incubator-weex
/blob/master/runtime/frameworks/legacy/vm/compiler.js
if (!vm._rootEl) {
vm._rootEl = element
// bind event earlier because of lifecycle issues
const binding = vm._externalBinding || {}
const target = binding.template
const parentVm = binding.parent
if (target && target.events && parentVm && element) {
for (const type in target.events) {
const handler = parentVm[target.events[type]]
if (handler) {
element.addEvent(type, bind(handler, parentVm))
}
}
}
}

// https://github.com/apache/incubator-weex/blob/master/runtime/vdom/Element.js
addEvent (type, handler, params) {
if (!this.event) {
this.event = {}
}
if (!this.event[type]) {
this.event[type] = { handler, params }
const taskCenter = getTaskCenter(this.docId)
if (taskCenter) {
taskCenter.send(
'dom',
{ action: 'addEvent' },
[this.ref, type]
)
}
}
}

上面可以看出只传递了一个ref过去,绑定完毕至所有组件渲染完成之后,当视图发生对应的事件之后,客户端捕获到了事件之后通过fireEvent将对应的事件,传递四个参数,ref、type、event、domChanges,通过bridge将这些参数传递给js framework的bridge,但是到底层的时候还会携带一个Weex实例的ID,因为此时可能存在多个weex实例,通过Weex ID找到对应的weex`实例。

如果事件绑定有多个ref,还需要遍历递归一下,也是一个深度遍历的过程,然后找到对应的事件,触发对应的事件,事件里可能有对双绑数据的改变,进而改变DOM,所以事件触发之后再次进行differ.flush。对比生成新的VDOM,然后渲染新的页面样式。

事件触发

// https://github.com/apache/incubator-weex
/blob/master/runtime/frameworks/legacy/app/ctrl/misc.js
export function fireEvent (app, ref, type, e, domChanges) {
console.debug(`[JS Framework] Fire a "${type}"
event on an element(${ref}) in instance(${app.id})`)
if (Array.isArray(ref)) {
ref.some((ref) => {
return fireEvent(app, ref, type, e) !== false
})
return
}
const el = app.doc.getRef(ref)
if (el) {
const result = app.doc.fireEvent(el, type, e,
domChanges)
app.differ.flush()
app.doc.taskCenter.send('dom', { action:
'updateFinish' }, [])
return result
}
return new Error(`invalid element reference "${ref}"`)
}

app.doc.fireEvent(el, type, e, domChanges)主要来看看这个方法,首先是获取到当时的事件回调,然后执行事件回调,原生的组件不会有事件冒泡,但是js是有事件冒泡机制的,所以下面模拟了一个事件冒泡机制,继续触发了父级的fireEvent,逐个冒泡到父级,这部分是在js framework中完成的。

// https://github.com/apache/incubator-weex/blob/master/runtime/vdom/Element.js
fireEvent (type, event, isBubble, options) {
let result = null
let isStopPropagation = false
const eventDesc = this.event[type]
if (eventDesc && event) {
const handler = eventDesc.handler
event.stopPropagation = () => {
isStopPropagation = true
}
if (options && options.params) {
result = handler.call(this, ...options.params, event)
}
else {
result = handler.call(this, event)
}
}

if (!isStopPropagation
&& isBubble
&& (BUBBLE_EVENTS.indexOf(type) !== -1)
&& this.parentNode
&& this.parentNode.fireEvent) {
event.currentTarget = this.parentNode
this.parentNode.fireEvent(type, event, isBubble) // no options
}

return result
}

上述就完成了一次完整的事件触发,如果是简单的事件,类似click这样的一次传递完成一次事件回调,不会有太大的问题,但是如果是滚动这样的事件传递难免会有性能问题,所以客户端在处理滚动事件的时候,肯定会有一个最小时间间隔,肯定不是无时无刻的触发。

更好的处理是Weex也引入了expression binding,将js的事件回调处理成表达式,在绑定的时候一并传给客户端,由于是表达式,所以客户端也可以识别表达式,客户端在监听原生事件触发的时候,就直接执行表达式。这样就省去了传递的过程。Weex的bingdingX也是可以用来处理类似频繁触发的js和客户端之间的交互的,比如动画。

module 的使用

上面已经讲了module的注册,最终调用js framework的registerModules注入所有module方法,并将方法存储在nativeModules对象上,注册的过程就算完成了。

// https://github.com/apache/incubator-weex/blob/master/runtime
/frameworks/legacy/static/register.js
export function registerModules (modules) {
/* istanbul ignore else */
if (typeof modules === 'object') {
initModules(modules)
}
}

// https://github.com/apache/incubator-weex/blob/master
/runtime/frameworks/legacy/app/register.js
export function initModules (modules, ifReplace) {
for (const moduleName in modules) {
// init `modules[moduleName][]`
let methods = nativeModules[moduleName]
if (!methods) {
methods = {}
nativeModules[moduleName] = methods
}

// push each non-existed new method
modules[moduleName].forEach(function (method) {
if (typeof method === 'string') {
method = {
name: method
}
}

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

requireModule

我们通过weex.requireModule('xxx')来获取module,首先我们需要了解一下weex这个全局变量是哪儿来的,上面在渲染的过程中的时候会生成一个weex实例,这个信息会被保存在一个全局变量中weexGlobalObject,在callFunction的时候,这个对象会被绑定在js bundle执行时的weex对象上,具体如下。

const globalObjects = Object.assign({
...
weex: weexGlobalObject
}, timerAPIs, services)

weex这个对象上还有会很多方法和属性,其中就有能调用到module的方法就是requireModule,这个方法和上面客户端注入Module时的方法是放在同一个模块中的,也就是同一个闭包中的,所以可以共享nativeModules这个对象。

//https://github.com/apache/incubator-weex/blob/master/runtime
/frameworks/legacy/app/index.js
App.prototype.requireModule = function (name) {
return requireModule(this, name)
}

// https://github.com/apache/incubator-weex/blob/master/runtime/
frameworks/legacy/app/register.js
export function requireModule (app, name) {
const methods = nativeModules[name]
const target = {}
for (const methodName in methods) {
Object.defineProperty(target, methodName, {
configurable: true,
enumerable: true,
get: function moduleGetter () {
return (...args) => app.callTasks({
module: name,
method: methodName,
args: args
})
},
set: function moduleSetter (value) {
if (typeof value === 'function') {
return app.callTasks({
module: name,
method: methodName,
args: [value]
})
}
}
})
}
return target
}

上面为什么没有使用简单的call或者apply方法呢?而是在返回的时候对这个对象所有方法进行了类似双绑的操作。首先肯定是为了避免对象被污染,这个nativeModules是所有weex实例共用的对象,如果一旦可以直接获取,前端对象都是引用,就有可能被重写,这样的肯定是不好的。

这里还用了一个callTasks,这个前面初始化的时候都已经说明过了,其实就是调用对应native的方法,taskCenter.send就会去查找客户端对应的方法,上面有taskCenter相关的代码,最后通过callNativeModule调用到客户端的代码。

// https://github.com/apache/incubator-weex/blob
/master/runtime/frameworks/legacy/app/ctrl/misc.js
export function callTasks (app, tasks) {
let result

/* istanbul ignore next */
if (typof(tasks) !== 'array') {
tasks = [tasks]
}

tasks.forEach(task => {
result = app.doc.taskCenter.send(
'module',
{
module: task.module,
method: task.method
},
task.args
)
})

return result
}

完成调用之后就等待客户端处理,客户端处理完成之后进行返回。这里虽然是一个forEach的遍历,但是返回的result都是同步的最后一个result。这里不是很严谨,但是我们看上层结构又不会有问题,tasks传过来一般是一个一个的任务,不会传array过来,并且大部分的客户端调用方法都是异步的,很少有同步回调,所以只能说不严谨。

总结

通过上面的梳理,我们可以看到Weex运行原理的细节,整体流程也梳理清楚了,我们通过一年的实践,不管是纯Weex应用还是现有APP接入都有实践,支撑了我们上百个页面的业务,同时开发效率得到了非常大的提升,也完善了我们基于Vue的前端技术栈。

现在Weex本身也在不断的更新,至少我们的业务上线之后让我们相信Weex是可行的,虽然各种缺点不断的被诟病,但是哪个优秀的技术的没有经历这样的发展呢。摘掉我们前端技术的鄙视链眼镜,让技术更好的为业务服务。

最后我们在通过业务实践和积累之后,也归纳总结出了基于Weex的技术解决方案 Eros并开源出来,解决了被大家所诟病的环境问题,提供更多丰富的Component和Module解决实际的业务问题。目前已有上千开发者有过开发体验,在不断吐槽中改进我们的方案,稳定了底层方案,构建了新的插件化方式,目前已经有开发者贡献了一些插件,也收集到开发者已上线的40+ APP的案例,还有非常多的APP在开发过程中。希望我们的方案能帮助到APP开发中的你。

下面是一些通过 Eros 上线的APP案例

相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

   
15158 次浏览       18