这篇文章介绍了一个 Weex 页面的渲染过程,涉及很多框架内部的细节。
“哟”
这是一个使用 Vue.js 2.x 语法写的一个小例子,极其简单,就一个字,可以借助 Weex 在移动端中渲染生成原生组件。
这也是实现文字水平垂直居中的最简例子。
源代码
组件代码:
<!-- yo.vue -->
<template>
<div style="justify-content:center;">
<text class="freestyle">哟</text>
</div>
</template>
<style scoped>
.freestyle {
text-align: center;
font-size: 200px;
}
</style>
除了组件代码以外,还需要一个入口文件指定挂载点并触发渲染:
// entry.js
import Vue from 'vue'
import Yo from 'yo.vue'
Yo.el = '#root'
new Vue(Yo)
编译
.vue
文件是无法被直接执行的,必须要编译成 .js
格式的文件才可以被 Web 或 Weex 平台执行。
.vue
文件通常可以分为三部分:<template>
、<style>
和 <script>
,<template>
是必须要有的,其他可选。其中 <script>
中的代码会保留或者被转换成 ES5 的语法;<style>
中的 CSS 在 Weex 平台上会被转换成 JSON 格式的样式声明,放到组件的定义中去;<template>
会被编译生成组件定义中 render 函数,可以理解为 render 函数的语法糖。
上述例子真实生成的代码是这样的,比较乱,把模块解开将其简化一下,和下边的代码等价:
// { "framework": "Vue" }
new Vue({
el: '#root',
style: {
freestyle: {
textAlign: 'center',
fontSize: 200
}
},
render: function (h) {
return h(
'div',
{ staticStyle: { justifyContent: 'center' } },
[h(
'text',
{ staticClass: ['freestyle'] },
['哟']
)]
)
}
})
执行
初始化执行环境
要想在移动端上执行上述代码,就需要集成 Weex SDK。
在应用启动时就会初始化 Weex SDK,准备好执行环境,然后可以从网络或者本地加载打包好的 js 文件,调用 SDK 提供的 render 或者 renderWithURL 方法启动渲染。
图中画出了 Weex SDK 的部分内容。其中 weex-vue-framework
和 Vue.js
是对等的,语法和内部机制都是一样的,只不过 Vue.js
最终创建的是 DOM 元素,而 weex-vue-framework
则是向原生端发送渲染指令,最终渲染生成的是原生组件。Weex Runtime 用来对接上层前端框架(如 Vue.js 和 Rax)并且负责和原生端之间的通信。Render Engine 就是针对各个端开发的原生渲染器,包含了 Weex 内置组件和模块的实现,可扩展。
创建组件
Weex 接收到 js 文件以后,会先检查它的格式,发现用的是 Vue 版本,就会调用 weex-vue-framework
中提供的 createInstance
方法创建实例。
代码里 new Vue()
会创建一个组件,通过其 render
函数创建 VNode 节点,并且触发相应的生命周期,如果指定了 el
属性也会执行挂载(mount),根据 Virtual DOM 在指定平台中生成真实的 UI 组件。
上述代码只有一个组件两个标签和一些简单样式,最终生成的 VNode 节点如下(数据结构有简化):
{
tag: 'div',
data: {
staticStyle: { justifyContent: 'center' }
},
children: [{
tag: 'text',
data: {
staticClass: 'freestyle'
},
context: {
$options: {
style: {
freestyle: {
textAlign: 'center',
fontSize: 200
}
}
}
},
children: [{
tag: '',
text: '哟'
}]
}]
}
Patch
再生成了 VNode 节点之后,还需要执行 “patch” 将虚拟 DOM 绘制成真实的 UI。在执行 patch 之前的过程都是 Web 和 Weex 通用的,所以文件格式、打包编译过程、模板指令、组件的生命周期、数据绑定等上层语法都是一致的。
然而由于目标执行环境不同(浏览器和 Weex 容器),在渲染真实 UI 的时候调用的接口也不同。
在 Vue.js 内部,Web 平台和 Weex 平台中的 patch
方法是不同的,但是都是由 createPatchFunction
这个方法生成的,它支持传递 nodeOps
参数,在其中代理了所有 DOM 操作。在 Web 平台中 nodeOps
背后调用的都是 Web API,在 Weex 平台中则调用的是 Weex Runtime 提供的 Native DOM API。触发 DOM 渲染的入口一致,但是不同平台的实现方式不同。
例如 nodeOps
中的 createElement
的操作,在 Web 平台中实际调用的是 document.createElement(tagName)
这个接口(参考代码);而在 Weex 平台中实际执行的是 new renderer.Element(tagName)
(参考代码)。
发送渲染指令
上述页面的 patch 过程不仅限于 Vue,在 Rax 中也调用了 Weex 的 Native DOM API,实现原理是一致的。发送渲染指令的过程是所有上层前端框架通用的,上层使用 Vue 还是 Rax 对于原生渲染器而言是透明的,只是语法和构建 Virtual DOM 的方式有差异而已。
在上层前端框架调用了 Weex 平台提供的 Native DOM API 之后,Weex Runtime 会构建一个用于渲染的节点树,并将操作转换成渲染指令发送给客户端。
回顾文中提到的 “哟” 例子,上层框架调用了 Weex Runtime 中 createBody
、createElement
、appendChild
这三个接口,简单构建了一个用于渲染的节点树,最终生成了两条渲染指令。
图中的 Platform API 指的是原生环境提供的 API,这些 API 是 Weex SDK 中原生模块提供的,不是 js 中方法,也不是浏览器中的接口,是 Weex 内部不同模块之间的约定。
目前来说渲染指令是基于 JSON 描述的,具体格式大致如下所示:
{
module: 'dom',
method: 'createBody',
args: [{
ref: '_root',
type: 'div',
style: { justifyContent: 'center' }
}]
}
{
module: 'dom',
method: 'addElement',
args: ['_root', {
ref: '2',
type: 'text',
attr: { value: '哟' },
style: { textAlign: 'center', fontSize: 200 }
}]
}
渲染原生组件
原生渲染器接收上层传来的渲染指令,并且逐步将其渲染成原生组件。
渲染指令分很多类,文章中提到的两个都是用来创建节点的,其他还有 moveElement
、updateAttrs
、addEvent
等各种指令。原生渲染器先是解析渲染指令的描述,然后分发给不同的模块。关于 UI 绘制的指令都属于 "dom"
模块中,在 SDK 内部有组件的实现,其他还有一些无界面的功能模块,如 stream 、navigator 等模块,也可以通过发送指令的方式调用。
这个例子里,第一个 createBody
的指令就创建了一个 <div>
的原生组件,同时也将样式应用到了改组件上。第二个 addElement
指令向 <div>
中添加一个 <text>
组件,同时也声明了组件的样式和属性值。
上述过程不是分阶段一个一个执行的,而是可以实现“流式”渲染的,有可能第一个 <div>
的原生组件还没渲染好,<text>
的渲染指令又发过来了。当一个页面特别大时,能看到一块一块的内容逐渐渲染出来的过程。
总结
没啥可总结的,都是细节,而且是框架内部的细节,以后很可能还会变,对于如何写好 Weex 的代码没有半毛钱帮助。