内容简介: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 的优缺点
首先肯定需要看看优缺点,优点用来判断自己的场景适不适合做这个技术,缺点来看自己的场景会不会被限制住,有没有办法解决和绕开。
优点:
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
@property (nonatomic, strong) id
@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
@optional
// Cancel the ongoing request
- (void)cancelRequest:(WXResourceRequest *)request;
@end
WXResourceRequestHandler中规定了两个方法,一个是加载资源的请求方法,一个是需要请求的方法,然后看一下WXResourceRequestHandlerDefaultImpl类:
//
// WXResourceRequestHandlerDefaultImpl.m
//
#pragma mark - WXResourceRequestHandler
- (void)sendRequest:(WXResourceRequest *)request withDelegate:(id
{
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代码不一样,经过转换主要还是和