2016年4月21日,阿里巴巴在Qcon大会上宣布开源跨平台移动开发工具Weex,Weex能够完美兼顾性能与动态性,让移动开发者通过简捷的前端语法写出Native级别的性能体验,并支持iOS、安卓、YunOS及Web等多端部署。
对于移动开发者来说,Weex主要解决了频繁发版和多端研发两大痛点,同时解决了前端语言性能差和显示效果受限的问题。开发者只需要在自己的APP中嵌入Weex的SDK,就可以通过撰写HTML/CSS/JavaScript来开发Native级别的Weex界面。Weex界面的生成码其实就是一段很小的JS,可以像发布网页一样轻松部署在服务端,然后在APP中请求执行。
与 现有的开源跨平台移动开放项目如Facebook的React Native和微软的Cordova相比,Weex更加轻量,体积小巧。因为基于web conponent标准,使得开发更加简洁标准,方便上手。Native组件和API都可以横向扩展,方便根据业务灵活定制。Weex渲染层具备优异的性 能表现,能够跨平台实现一致的布局效果和实现。对于前端开发来说,Weex能够实现组件化开发、自动化数据绑定,并拥抱Web标准。
突出特点:
致力于移动端,充分调度 native 的能力
充分解决或回避性能瓶颈
灵活扩展,多端统一,优雅“降级”到 HTML5
保持较低的开发成本和学习成本
快速迭代,轻量实时发布
融入现有的 native 技术体系
轻量化,体积小巧,语法简单,方便接入和上手
可扩展,业务方可去中心化横向定制组件和功能模块
原生渲染、体验流畅
此系列文章主要面向对Weex感兴趣的前端同学,通过解读Weex的JS源码来剖析实现原理,也便于在使用Weex过程中发挥框架优势,同时尽量避开短板。
首先简单说一下weex文件的编译。
Weex框架目前支持2种文件格式:.we 和 .vue。本小节主要简单讲一讲.we文件的编译过程。
.we文件的一个示例如下
<template>
<div class="container">
<div style="height: 2000; background-color: #ff0000;">div>
<text onclick="foo">{{x}}text>
div>
template>
<style>
.container {
flex-direction: column;
flex: 1;
}
style>
<script>
var dom = require('@weex-module/dom')
module.exports = {
data: function () {
return {
x: 1
}
},
methods: {
foo: function (e) {
dom.scrollToElement(this.$el('r'), { offset: 0 })
}
}
}
script>
可以看到,.we文件主要由template(UI模板), style(样式)和script(脚本)三个部分组成,编译器分别对这3个部分单独处理,最后合并为一个单独的jsbundle文件。templdate部分采用html5解析器转换为AST,然后从AST中提取有用的信息生成JSON对象(可以看作为简化的AST);style部分同样也通过AST输出为JSON对象;script部分则会处理require语句,并对代码进行封装,方便被框架调用。
接下来详细讲一讲编译好的jsbundle文件是如何在框架中加载运行的。jsbundle文件在被加载运行之前,首先是Weex框架的初始化工作。
首先在Application的onCreate函数,设置自定义配置项,然后调用initialize函数完成Engine的初始化工作
WXSDKEngine.addCustomOptions("appName", "WXSample");
WXSDKEngine.addCustomOptions("appGroup", "WXApp");
WXSDKEngine.initialize(this,
new InitConfig.Builder()
.setImgAdapter(new ImageAdapter())
.setDebugAdapter(new PlayDebugAdapter())
.build()
);
addCustomOptions函数将自定义配置保存在WXEnvironment类中静态Map中。initialize函数调用doInitInternal(application,config)完成初始化工作,并修改状态。
InitConfig对象用来管理Engine使用的各个适配器对象,例如Http网络请求、图片加载、本地存储等等。
doInitInternal函数中,主要的工作是调用WXSDKManager的initScriptsFramework函数在JS端完成JS框架初始化工作
WXBridgeManager.getInstance().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!=null?config.getFramework():null);
WXEnvironment.sSDKInitExecuteTime = System.currentTimeMillis() - start;
WXLogUtils.renderPerformanceLog("SDKInitExecuteTime", WXEnvironment.sSDKInitExecuteTime);
}
});
register();
initScriptsFramework函数实际上调用WXBridgeManager的initScriptsFramework函数发送消息异步完成JS框架初始化,其中 framework为使用的框架名,what为消息类型编号 。INIT_FRAMEWORK消息在WXBridgeManager.handleMessage函数中被处理,调用initFramework函数来完成框架初始化工作。
Message msg = mJSHandler.obtainMessage();
msg.obj = framework;
msg.what = WXJSBridgeMsgType.INIT_FRAMEWORK;
msg.setTarget(mJSHandler);
msg.sendToTarget();
消息类型的定义为:
public static final int INIT_FRAMEWORK = 0x07;
initFramework真正执行的函数是在WeexV8中实现C++函数Java_com_taobao_weex_bridge_WXBridge_initFramework,这个函数的主要工作是获取Android应用相关信息,在JSCore中设置全局变量,例如platform,deviceWidth等。并且调用java端的getOptions函数,将option信息写入jscore的全局变量
jint Java_com_taobao_weex_bridge_WXBridge_initFramework(
JNIEnv *env, jobject object, jstring script, jobject params) {
...
jmethodID m_platform = env->GetMethodID(c_params, "getPlatform", "()Ljava/lang/String;");
jobject platform = env->CallObjectMethod(params, m_platform);
WXEnvironment->Set("platform", jString2V8String(env, (jstring) platform));
env->DeleteLocalRef(platform);
...
}
register函数完成所有组件、API模块和DOM对象的注册;为了节省开销,利用BatchOperationHelper来批处理执行;
registerComponent(
new SimpleComponentHolder(
WXText.class,
new WXText.Creator()
),
false,
WXBasicComponentType.TEXT
);
registerComponent(WXBasicComponentType.A, WXA.class, false);
registerModule("modal", WXModalUIModule.class, false);
registerModule("webview", WXWebViewModule.class, true);
registerDomObject(WXBasicComponentType.TEXT, WXTextDomObject.class);
registerDomObject(WXBasicComponentType.INPUT, BasicEditTextDomObject.class);
组件注册由registerComponent函数完成,UI组件类封装为SimpleComponentHolder,并赋予一个或多个key值,注册项由WXComponentRegistry类进行管理,每个组件同时注册到原生端和JS端;
registerNativeComponent(type, holder);
registerJSComponent(registerInfo);
registerNativeComponent函数调用SimpleComponentHolder的loadIfNonLazy函数来完成初始化工作,主要是生成该组件API的元信息Map
try {
registerNativeModule(moduleName, factory);
} catch (WXException e) {
WXLogUtils.e("", e);
}
registerJSModule(moduleName, factory);
registerNativeModule函数将模块信息和模块工厂类保存在sModuleFactoryMap中。
registerJSModule函数则调用WXBridgeManager.registerModules函数,在JS端注册模块信息,将模块信息转换为JSON字符串,通过JS函数registerModules完成注册,将模块信息保存在nativeModules中。同样,在注册时JSFramework尚未初始化完成,则放入mRegisterModuleFailList中,等创建App时在重新注册
DOM对象注册由registerDomObject函数完成,调用WXDomRegistry.registerDomObject函数完成注册工作,DOM对象信息保存在sDom中。
一个Weex应用运行在AbstractWeexActivity容器中,AbstractWeexActivity实现了IWXRenderListener接口,
public interface IWXRenderListener {
void onViewCreated(WXSDKInstance instance, View view);
void onRenderSuccess(WXSDKInstance instance, int width, int height);
void onRefreshSuccess(WXSDKInstance instance, int width, int height);
void onException(WXSDKInstance instance, String errCode, String msg);
}
AbstractWeexActivity在onCreate函数中创建一个WXSDKInstance实例,并注册registerRenderListener为自身。
createWeexInstance();
mInstance.onActivityCreate();
AbstractWeexActivity的renderPage函数完成jsbundle的加载和渲染工作,将jsbundle的url地址设置为option参数,并通过WXSDKInstance的render函数来加载和渲染页面。
protected void renderPage(String template,String source,String jsonInitData){
Map<String, Object> options = new HashMap<>();
options.put(WXSDKInstance.BUNDLE_URL, source);
mInstance.render(
getPageName(),
template,
options,
jsonInitData,
ScreenUtil.getDisplayWidth(this),
ScreenUtil.getDisplayHeight(this),
WXRenderStrategy.APPEND_ASYNC);
}
WXSDKInstance的renderByUrl函数,首先判断URL类型,如果是本地文件,则直接通过render函数开始渲染,否则通过WXRequest去下载URL。
public void renderByUrl(String pageName, final String url, Map options, final String jsonInitData, final int width, final int height, final WXRenderStrategy flag) {
pageName = wrapPageName(pageName, url);
mBundleUrl = url;
if (options == null) {
options = new HashMap();
}
if (!options.containsKey(BUNDLE_URL)) {
options.put(BUNDLE_URL, url);
}
Uri uri=Uri.parse(url);
if(uri!=null && TextUtils.equals(uri.getScheme(),"file")){
render(pageName, WXFileUtils.loadAsset(assembleFilePath(uri), mContext),options,jsonInitData,width,height,flag);
return;
}
IWXHttpAdapter adapter=WXSDKManager.getInstance().getIWXHttpAdapter();
WXRequest wxRequest = new WXRequest();
wxRequest.url = url;
if (wxRequest.paramMap == null) {
wxRequest.paramMap = new HashMap();
}
wxRequest.paramMap.put("user-agent", WXHttpUtil.assembleUserAgent(mContext,WXEnvironment.getConfig()));
adapter.sendRequest(wxRequest, new WXHttpListener(pageName, options, jsonInitData, width, height, flag, System.currentTimeMillis()));
mWXHttpAdapter = adapter;
}
render函数会创建一个唯一实例Id,然后调用WXSDKManager的createInstance创建App实例。
mInstanceId = WXSDKManager.getInstance().generateInstanceId();
WXSDKManager.getInstance().createInstance(this, template, options, jsonInitData);
mRendered = true;
WXSDKManager的createInstance函数首先在WXRenderManager注册当前实例,然后通过WXBridgeManager的createInstance函数在JS端创建App实例,具体工作由 invokeCreateInstance函数来实现。
void createInstance(WXSDKInstance instance, String code, Map<String, Object> options, String jsonInitData) {
mWXRenderManager.registerInstance(instance);
mBridgeManager.createInstance(instance.getInstanceId(), code, options, jsonInitData);
}
invokeCreateInstance首先调用initFramework来初始化框架,然后调用JS函数createInstance创建一个App实例。
private void invokeCreateInstance(String instanceId, String template,
Map options, String data) {
initFramework("");
if (mMock) {
mock(instanceId);
} else {
...
try {
WXJSObject instanceIdObj = new WXJSObject(WXJSObject.String,
instanceId);
WXJSObject instanceObj = new WXJSObject(WXJSObject.String,
template);
WXJSObject optionsObj = new WXJSObject(WXJSObject.JSON,
options == null ? "{}"
: WXJsonUtils.fromObjectToJSONString(options));
WXJSObject dataObj = new WXJSObject(WXJSObject.JSON,
data == null ? "{}" : data);
WXJSObject[] args = {instanceIdObj, instanceObj, optionsObj,
dataObj};
invokeExecJS(instanceId, null, METHOD_CREATE_INSTANCE, args);
} catch (Throwable e) {
...
}
}
}
initFramework函数从本地文件读取main.js获取框架名,然后调用WXBridge的initFramework函数完成指定框架的初始化,initFramework函数由C++实现,通过JNI调用(在libweexv8.so中).
private void initFramework(String framework){
if (!isJSFrameworkInit()) {
if (TextUtils.isEmpty(framework)) {
if (WXEnvironment.isApkDebugable()) {
WXLogUtils.d("weex JS framework from assets");
}
framework = WXFileUtils.loadAsset("main.js", WXEnvironment.getApplication());
}
...
try {
long start = System.currentTimeMillis();
if(mWXBridge.initFramework(framework, assembleDefaultOptions())==INIT_FRAMEWORK_OK){
WXEnvironment.sJSLibInitTime = System.currentTimeMillis() - start;
WXLogUtils.renderPerformanceLog("initFramework", WXEnvironment.sJSLibInitTime);
WXEnvironment.sSDKInitTime = System.currentTimeMillis() - WXEnvironment.sSDKInitStart;
WXLogUtils.renderPerformanceLog("SDKInitTime", WXEnvironment.sSDKInitTime);
mInit = true;
execRegisterFailTask();
WXEnvironment.JsFrameworkInit = true;
registerDomModule();
commitAlert(IWXUserTrackAdapter.JS_FRAMEWORK,WXErrorCode.WX_SUCCESS);
}else{
...
}
} catch (Throwable e) {
...
}
}
}
execRegisterFailTask函数尝试重新注册之前注册失败的模块和组件,
private void execRegisterFailTask() {
int moduleCount = mRegisterModuleFailList.size();
if (moduleCount > 0) {
for (int i = 0; i < moduleCount; ++i) {
registerModules(mRegisterModuleFailList.get(i));
}
mRegisterModuleFailList.clear();
}
int componentCount = mRegisterComponentFailList.size();
if (componentCount > 0) {
for (int i = 0; i < componentCount; ++i) {
registerComponents(mRegisterComponentFailList.get(i));
}
mRegisterComponentFailList.clear();
}
}
registerDomModule函数
private void registerDomModule() throws WXException {
if (sDomModule == null)
sDomModule = new WXDomModule();
/** Tell Javascript Framework what methods you have. This is Required.**/
Map domMap=new HashMap<>();
domMap.put(WXDomModule.WXDOM,WXDomModule.METHODS);
registerModules(domMap);
}
当框架初始化完成,组件和API模块都注册成功,调用invokeExecJS函数执行JS端的createInstance函数,创建一个App实例。
JS端的createInstance函数创建一个App对象,保存在instanceMap中,然后调用InitApp初始化对象。
export function createInstance (id, code, options, data) {
resetTarget()
let instance = instanceMap[id]
/* istanbul ignore else */
options = options || {}
let result
/* istanbul ignore else */
if (!instance) {
instance = new App(id, options)
instanceMap[id] = instance
result = initApp(instance, code, data)
}
else {
result = new Error(`invalid instance id "${id}"`)
}
return result
}
initApp函数(也就是init函数)将jsbundle代码封装给一个函数,并定义上下文环境(包括 ‘define’,’require’,’weex_define‘, 等等),并执行模块函数。
export function init (app, code, data) {
console.debug('[JS Framework] Intialize an instance with:\n', data)
let result
// prepare app env methods
const bundleDefine = (...args) => defineFn(app, ...args)
const bundleBootstrap = (name, config, _data) => {
result = bootstrap(app, name, config, _data || data)
updateActions(app)
app.doc.listener.createFinish()
console.debug(`[JS Framework] After intialized an instance(${app.id})`)
}
const bundleVm = Vm
/* istanbul ignore next */
const bundleRegister = (...args) => register(app, ...args)
/* istanbul ignore next */
const bundleRender = (name, _data) => {
result = bootstrap(app, name, {}, _data)
}
/* istanbul ignore next */
const bundleRequire = name => _data => {
result = bootstrap(app, name, {}, _data)
}
const bundleDocument = app.doc
/* istanbul ignore next */
const bundleRequireModule = name => app.requireModule(removeWeexPrefix(name))
// prepare code
let functionBody
/* istanbul ignore if */
if (typeof code === 'function') {
// `function () {...}` -> `{...}`
// not very strict
functionBody = code.toString().substr(12)
}
/* istanbul ignore next */
else if (code) {
functionBody = code.toString()
}
// wrap IFFE and use strict mode
functionBody = `(function(global){"use strict"; ${functionBody} })(Object.create(this))`
// run code and get result
const { WXEnvironment } = global
/* istanbul ignore if */
if (WXEnvironment && WXEnvironment.platform !== 'Web') {
// timer APIs polyfill in native
const timer = app.requireModule('timer')
const timerAPIs = {
setTimeout: (...args) => {
const handler = function () {
args[0](...args.slice(2))
}
timer.setTimeout(handler, args[1])
return app.uid.toString()
},
setInterval: (...args) => {
const handler = function () {
args[0](...args.slice(2))
}
timer.setInterval(handler, args[1])
return app.uid.toString()
},
clearTimeout: (n) => {
timer.clearTimeout(n)
},
clearInterval: (n) => {
timer.clearInterval(n)
}
}
const fn = new Function(
'define',
'require',
'document',
'bootstrap',
'register',
'render',
'__weex_define__', // alias for define
'__weex_bootstrap__', // alias for bootstrap
'__weex_document__', // alias for bootstrap
'__weex_require__',
'__weex_viewmodel__',
'setTimeout',
'setInterval',
'clearTimeout',
'clearInterval',
functionBody
)
fn(
bundleDefine,
bundleRequire,
bundleDocument,
bundleBootstrap,
bundleRegister,
bundleRender,
bundleDefine,
bundleBootstrap,
bundleDocument,
bundleRequireModule,
bundleVm,
timerAPIs.setTimeout,
timerAPIs.setInterval,
timerAPIs.clearTimeout,
timerAPIs.clearInterval)
}
else {
const fn = new Function(
'define',
'require',
'document',
'bootstrap',
'register',
'render',
'__weex_define__', // alias for define
'__weex_bootstrap__', // alias for bootstrap
'__weex_document__', // alias for bootstrap
'__weex_require__',
'__weex_viewmodel__',
functionBody
)
fn(
bundleDefine,
bundleRequire,
bundleDocument,
bundleBootstrap,
bundleRegister,
bundleRender,
bundleDefine,
bundleBootstrap,
bundleDocument,
bundleRequireModule,
bundleVm)
}
return result
}