weex日记

weex在开源以后获得了广泛的关注,也有很多人深入研究,并且把它和react native进行了对比,总之它在社区的活跃度十分之高,最近的一个星期我也入了这个坑,看了看源码,上了上手,在这里写一篇文章记录一下所感所得。

weex是什么

首先我们看看weex的GitHub上是如何描述的。weex

很简单,它就是一个为了跨平台而生的框架,大气点说——write once,run anywhere。因此很多人把它和facebook的react native进行比较,这样的比较是合理的,毕竟它们两个做的事在宏观上来说是差不多的,weex确实也借鉴了rn的一些理念,不过如果你仔细了解过weex,你会发现其实在很多细节上它和rn还是存在不同的。

weex怎么用

下面我简单的说一下weex的初始化配置吧。

很简单,首先,你需要安装一个官方的app,playground,它可以向你展示weex的一些demo,并且提供了二维码的功能,让你方便的调试自己的程序。

然后,你需要安装一个工具Weex Toolkit。这个工具的作用是能够将你产出的.we文件转换成对应的js bundle,从而给app进行解析。在安装这个工具之前请确保你的电脑上已经有node了。

最后,就可以开始编写代码了,你写好的代码应该是.we结尾的,然后在控制台中输入

1
weex yourprofile.we

你就可以在浏览器中看到效果了,如果你想在移动端使用,则输入

1
weex yourprofile.we --qr

然后使用playground去扫在控制台出现的二维码就可以了。

更多和Weex Toolkit相关的使用大家可以去它的GitHub上查看。需要注意的是,如果你在使用该工具的时候控制台报出了错误,请根据错误的提示安装相关的依赖,或者将Weex Toolkit版本升级。

以上就是所有weex的初始化配置,如果你有过开发rn的经验,你会发现它比rn的配置要简单很多。

weex的工作原理

在第一章节中简单的说了一下weex是什么,在这一章节中,会从源码角度去看看,weex是如何工作的。

对于工作原理,主要要了解的就是weex的js端和native端是如何通信的。在看这里之前,你最好已经看过了我另外一篇文章其实没那么复杂!探究react-native通信机制。因为我前面说过,weex在原理机制上其实是借鉴了rn的,所以在通信模块上它们有很多相似的地方,在理解rn通信机制的文章里我已经讲的非常详细了,所以这里我会比较简单的讲一下。

首先,在weex官方的playground源码里,在application中进行了weex初始化。

1
WXSDKEngine.initialize(this,new InitConfig.Builder().setImgAdapter(new ImageAdapter()).setDebugAdapter(new PlayDebugAdapter()).build());

在WXSDKEngine的initialize方法中会调用doInitInternal去进行真正的初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void doInitInternal(final Application application,final InitConfig config){
WXEnvironment.sApplication = application;
WXEnvironment.JsFrameworkInit = false;

WXBridgeManager.getInstance().getJSHandler().post(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
WXSDKManager sm = WXSDKManager.getInstance();
if(config != null ) {
sm.setIWXHttpAdapter(config.getHttpAdapter());
sm.setIWXImgLoaderAdapter(config.getImgAdapter());
sm.setIWXUserTrackAdapter(config.getUtAdapter());
sm.setIWXDebugAdapter(config.getDebugAdapter());
sm.setIWXStorageAdapter(config.getStorageAdapter());
if(config.getDebugAdapter()!=null){
config.getDebugAdapter().initDebug(application);
}
}
WXSoInstallMgrSdk.init(application);
boolean isSoInitSuccess = WXSoInstallMgrSdk.initSo(V8_SO_NAME, 1, config!=null?config.getUtAdapter():null);
if (!isSoInitSuccess) {
return;
}
sm.initScriptsFramework(config.getFramework());

WXEnvironment.sSDKInitExecuteTime = System.currentTimeMillis() - start;
WXLogUtils.renderPerformanceLog("SDKInitExecuteTime", WXEnvironment.sSDKInitExecuteTime);
}
});
register();
}

在这个方法中,主要是三个操作:

1.调用WXSoInstallMgrSdk.initSo方法去加载so文件,从so文件的名字可以看出,weex的js引擎在android端选用了v8而并非和rn一样选用的是JSCore(不过weex在iOS端还是选了系统自带的JSCore,据说会有内存泄漏,没具体了解过)。

对于这一部分,值得一提的是weex加载so文件的方式,并非普通的system.loadLibrary,而是经过了一系列的优化措施,基本能保证加载成功,这样的方法我们是可以学习一下的,非常的好。

2.调用WXSDKManager的initScriptsFramework方法去加载整个JSFramework。

对于这一部分,我们需要知道JSFramework的源码是main.js。8000多行的源码,有兴趣的同学可以去看一下。

3.调用register()方法去注册一些系统的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
private static void register() {
try {
registerComponent(
WXText.class,
new SimpleComponentHolder(
WXText.class,
new WXText.Ceator()
),
false,
WXBasicComponentType.TEXT
);
registerComponent(
WXDiv.class,
new SimpleComponentHolder(
WXDiv.class,
new WXDiv.Ceator()
),
false,
WXBasicComponentType.CONTAINER,
WXBasicComponentType.DIV,
WXBasicComponentType.HEADER,
WXBasicComponentType.FOOTER
);
registerComponent(
WXImage.class,
new SimpleComponentHolder(
WXImage.class,
new WXImage.Ceator()
),
false,
WXBasicComponentType.IMAGE,
WXBasicComponentType.IMG
);
registerComponent( WXScroller.class,
new SimpleComponentHolder(
WXScroller.class,
new WXScroller.Ceator()
),
false,
WXBasicComponentType.SCROLLER
);
registerComponent( WXSlider.class,
new SimpleComponentHolder(
WXSlider.class,
new WXSlider.Creator()
),
true,
WXBasicComponentType.SLIDER
);
registerComponent( WXSliderNeighbor.class,
new SimpleComponentHolder(
WXSliderNeighbor.class,
new WXSliderNeighbor.Creator()
),
true,
WXBasicComponentType.SLIDER_NEIGHBOR
);
registerComponent(WXListComponent.class, false,WXBasicComponentType.LIST,WXBasicComponentType.VLIST);
registerComponent(HorizontalListComponent.class,false,WXBasicComponentType.HLIST);
registerComponent(WXBasicComponentType.CELL, WXCell.class, true);
registerComponent(WXBasicComponentType.INDICATOR, WXIndicator.class, true);
registerComponent(WXBasicComponentType.VIDEO, WXVideo.class, false);
registerComponent(WXBasicComponentType.INPUT, WXInput.class, false);
registerComponent(WXBasicComponentType.TEXTAREA, Textarea.class,false);
registerComponent(WXBasicComponentType.SWITCH, WXSwitch.class, false);
registerComponent(WXBasicComponentType.A, WXA.class, false);
registerComponent(WXBasicComponentType.EMBED, WXEmbed.class, true);
registerComponent(WXBasicComponentType.WEB, WXWeb.class);
registerComponent(WXBasicComponentType.REFRESH, WXRefresh.class);
registerComponent(WXBasicComponentType.LOADING, WXLoading.class);
registerComponent(WXBasicComponentType.LOADING_INDICATOR, WXLoadingIndicator.class);
registerComponent(WXBasicComponentType.HEADER, WXHeader.class);

registerModule("modal", WXModalUIModule.class, false);
registerModule("instanceWrap", WXInstanceWrap.class, true);
registerModule("animation", WXAnimationModule.class, true);
registerModule("webview", WXWebViewModule.class, true);
registerModule("navigator", WXNavigatorModule.class);
registerModule("stream", WXStreamModule.class);
registerModule("timer", WXTimerModule.class, true);
registerModule("storage", WXStorageModule.class, true);
registerModule("clipboard", WXClipboardModule.class, true);

registerDomObject(WXBasicComponentType.INDICATOR, WXIndicator.IndicatorDomNode.class);
registerDomObject(WXBasicComponentType.TEXT, WXTextDomObject.class);
registerDomObject(WXBasicComponentType.INPUT, BasicEditTextDomObject.class);
registerDomObject(WXBasicComponentType.TEXTAREA, TextAreaEditTextDomObject.class);
registerDomObject(WXBasicComponentType.SWITCH, WXSwitchDomObject.class);
registerDomObject(WXBasicComponentType.LIST, WXListDomObject.class);
registerDomObject(WXBasicComponentType.VLIST, WXListDomObject.class);
registerDomObject(WXBasicComponentType.HLIST, WXListDomObject.class);
registerDomObject(WXBasicComponentType.SCROLLER, WXScrollerDomObject.class);
} catch (WXException e) {
WXLogUtils.e("[WXSDKEngine] register:", e);
}
}

主要是三部分,Component,Module和DomObject。

这三部分也是weex中最重要的三个组件。

Component中含有我们native的view,它和DomObject是一一对应的。我们可以通过范型去指定它所承载的view。

1
public abstract class  WXComponent<T extends View>

我们可以自定义一个Component继承自WXComponent。

Module表示native的Api,我们可以将Module暴露给js供其调用,例如toast等等。

DomObject表示前端template标签在dom tree里的所有信息,例如style,attr,ref等等。这里提到了dom tree,可以说一下weex和rn一样,使用了virtual dom去提高渲染的性能,具体的vdom逻辑大家可以参考这篇文章

回到上面的逻辑,经过上面提高的三个步骤之后,整个init逻辑也就执行完了。接下去,我们可以在相应的activity中做操作,在playground的IndexActivity的onCreate方法中,调用了renderPage或者renderPageByUrl方法,而这两个方法在最后都会调用WXSDKInstance的render方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void render(String pageName, String template, Map<String, Object> options, String jsonInitData, int width, int height, WXRenderStrategy flag) {
if (mRendered || TextUtils.isEmpty(template)) {
return;
}

if(options==null){
options=new HashMap<>();
}

if(WXEnvironment.sDynamicMode && !TextUtils.isEmpty(WXEnvironment.sDynamicUrl) && options!=null && options.get("dynamicMode")==null){
options.put("dynamicMode","true");
renderByUrl(pageName, WXEnvironment.sDynamicUrl, options, jsonInitData, width, height, flag);
return;
}

mWXPerformance.pageName = pageName;
mWXPerformance.JSTemplateSize = template.length() / 1024;

mRenderStartTime = System.currentTimeMillis();
mRenderStrategy = flag;
mGodViewWidth = width;
mGodViewHeight = height;
mInstanceId = WXSDKManager.getInstance().generateInstanceId();
WXSDKManager.getInstance().createInstance(this, template, options, jsonInitData);
mRendered = true;
}

在这个方法中,会调用WXSDKManager的createInstance方法。

1
2
3
4
void createInstance(WXSDKInstance instance, String code, Map<String, Object> options, String jsonInitData) {
mWXRenderManager.createInstance(instance);
mBridgeManager.createInstance(instance.getInstanceId(), code, options, jsonInitData);
}

BridgeManager的createInstance方法最后调用了invokeExecJS去执行js代码,这样就跑到了js层了。

在js层处理完逻辑之后,会调用WXBridge的callNative方法,最终辗转到WXBridgeManager的callNative。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* Dispatch the native task to be executed.
* @param instanceId {@link WXSDKInstance#mInstanceId}
* @param tasks tasks to be executed
* @param callback next tick id
*/

public int callNative(String instanceId, String tasks, String callback) {

.........
.........

long start = System.currentTimeMillis();
JSONArray array = JSON.parseArray(tasks);

if(WXSDKManager.getInstance().getSDKInstance(instanceId)!=null) {
WXSDKManager.getInstance().getSDKInstance(instanceId).jsonParseTime(System.currentTimeMillis() - start);
}

int size = array.size();
if (size > 0) {
try {
JSONObject task;
for (int i = 0; i < size; ++i) {
task = (JSONObject) array.get(i);
if (task != null && WXSDKManager.getInstance().getSDKInstance(instanceId) != null) {
if (TextUtils.equals(WXDomModule.WXDOM, (String) task.get(WXDomModule.MODULE))) {
sDomModule = getDomModule(instanceId);
sDomModule.callDomMethod(task);
} else {
WXModuleManager.callModuleMethod(instanceId, (String) task.get(WXDomModule.MODULE),
(String) task.get(WXDomModule.METHOD), (JSONArray) task.get(WXDomModule.ARGS));
}
}
}
} catch (Exception e) {
WXLogUtils.e("[WXBridgeManager] callNative exception: ", e);
}
}

..........
return INSTANCE_RENDERING;
}

首先,js层会把想要传递的信息封装在一个json对象中,并且最终调用了WXModuleManager的callModuleMethod方法。在这个方法中,会根据js端提供的方法名字进行分发,调用对应native的方法。

所以这里我们知道,整个weex的通信机制和rn是如出一撤的,基本就是在两端维护两个类似协议的注册表,通过jni的形式(Bridge)进行通信,并且通过解析json数据来调用两端具体的方法。

在native层调用方法中,有一个比较特殊的module叫WXDomModule。这个module是和页面渲染相关的。具体就是解析json数据,将数据转换成native层已经注册的Component。最终,我们对应的Activity会注册一个IWXRenderListener这样的监听器,weex的sdk会回调这个监听器中的onViewCreated方法。我们只需要做对应的view添加就行了。

1
2
3
4
@Override
public void onViewCreated(WXSDKInstance instance, View view) {
((ViewGroup)findViewById(R.id.container)).addView(view);
}

到这里weex的通信机制就大致讲完了,我们可以用一张图来概括:

weex_communication

图片出自weex的官方教程

weex vs rn

上面讲完了weex的整个框架,可以看到在[通信]这一层面,它和rn没有多大的区别,并且也都用了virtual dom去提升性能,那么它和rn到底又有哪些区别呢?

首先我先来说说我认识weex所具备的优势:

1.我们知道rn是先出的iOS,后出的android,在很多东西上都有它的平台独立性,比如navigator,在很多时候我们在写js代码的时候需要去判断是iOS还是android平台。而weex在这方面做的比较好,换句话说,封装的比较完善。

2.在前端框架选择上,rn选择了react而weex选择了vue。对于这两个框架的好与坏我这样的前端新手不做评价,但是对于我来说,vue的模板风格是我更能接受的,基本就是原生h5的开发,而react所采用jsx则复杂很多,所有的代码都挤在一起,看起来就有些费劲。当然这都是见仁见智的~

3.前面也说过,weex在配置上比rn要简单一些。

4.rn原生上是不支持多bundle的,并且它的整个sdk会和业务代码打成一个js bundle,这样就会导致整个js bundle过大,想要做到分包就要自己去修改代码和造分包工具,比较麻烦。weex天生支持分包,并且sdk的bundle会抽离出来,业务方打出来的bundle只含有业务代码。

5.rn在使用上基本就是替换整个app,而weex则更灵活,可以做到[嵌入app]内使用,仅仅替换一个页面,或者一个view。

6.weex在页面渲染方面支持tree模式和node模式,开发者可以灵活的选择。

综上,可以看到weex相比较于rn,显得更加轻便灵活,对开发者也更佳友好。但是weex也有自己的缺点:

1.由于开源的比较晚,社区活跃度肯定是不及rn的,所以weex现在的坑肯定要比rn多,说不定你用着用着就会发现一个奇葩的问题没有人能解决。

2.这也是我在实际编码过程中发现的,当时在写rn的时候,对于异步回调习惯了用promise而不是callback,转移到weex之后,发现它好像只支持callback而不支持promise,不知道是出于什么原因。不过既然存在这个问题,我相信在其他编码的细节方面weex肯定也会有不如rn的地方,这很正常。

总结

总而言之,weex是一个值得深入学习的框架,希望能越做越好吧,也感谢小敏哥和教父在weex上对我的帮助,谢谢大神~