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
+ (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
绘制任务太多堆积在一起影响同步队列任务的执行。
istemplate=true
WXComponentConfig *config = [WXComponentFactory configWithComponentName:type];
BOOL isTemplate = [config.properties[@"isTemplate"] boolValue] || (supercomponent && supercomponent->_isTemplate);
if (isTemplate) {
bindingProps = [self _extractBindingProps:&attributes];
bindingStyles = [self _extractBindings:&styles];
bindingAttibutes = [self _extractBindings:&attributes];
bindingEvents = [self _extractBindingEvents:&events];
}
那么客户端在渲染的时候,会将整个Component
子节点获取过来,然后通过DataBinding
转换成表达式,存在bindingMap
中,相关的解析都在WXJSASTParser.m
文件中,涉及到比较复杂的模板解析,表达式解析和转换,绑定数据与原生UI
的关系。
渲染过程中客户端和js framework
还有事件的沟通,通过桥传递createFinished
和renderFinished
事件,js framework
会去执行Weex
实例对应的生命周期方法。
至此页面就已经渲染出来了,页面渲染完成之后,那么点击事件是怎么做的呢?
事件传递
全局事件
在了解事件如何发生传递之前,我们先看看事件有几种类型,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
案例
原文作者: 还是怕麻烦
本文来源: 掘金 如需转载请联系原作者