传统的移动端开发, 一个完整的业务需要维护三份终端代码: AndroidiOSH5, 这带来了极大的开发成本以及维护成本尤其是对处于业务初创期需要快速试错的业务以及需要支持定期运营活动的业务所以业界也一直在探索跨平台方案, 旨在通过一套代码完成各个终端的业务逻辑相关方案经过不断演化, 从早期的 H5Hybrid 到如今的 Cloud Native(云原生), 在开发效率和用户体验上都在一点点逼近最初的设想
早期 H5 和 Hybrid 方案的核心是利用终端的内置浏览器 (webview) 功能, 通过开发 web 应用满足跨平台需求该方案可以解决跨平台问题, 同时可以提升发版效率但其最大的弊端在于用户体验相较于 native 开发的 app 存在较大差距, 经常出现页面卡顿, 加载慢等问题
于是后来业界开始探索依旧利用 web 技术栈开发出媲美原生体验 app 的方案, 于是以 WEEX 为代表云原生开发框架开始出现所谓云原生 (Cloud Native) 指可以通过云端快速发布(与远程 web 应用发布流程类似), 同时还可以达到媲美原生 App 体验的方案 WEEX 依旧采取传统的 web 开发技术栈进行开发, 同时 app 在终端的运行体验不输 native app 其同时解决了开发效率发版速度以及用户体验三个核心问题那么 WEEX 是如何实现的? 目前 WEEX 已经完全开源, 并捐给 Apache 基金会, 我们可以通过分析其源码来一探究竟
WEEX 框架主要分为两部分:
前端 JavaScript 框架
Native SDK
在上一篇博客中, 我们介绍了 Native SDK 的原理, 本文主要介绍其前端 JavaScript 框架原理
1 整体架构
首先还是再来看下 WEEX 开发的整体架构:
可以看到在 JS-Native Bridge 将渲染指令发送给 Android 或者 iOS 渲染引擎之前, 我们的业务代码运行在 JSCore/v8 的执行引擎之中, 而在该执行引擎之中除了执行业务 JSBundle, 还运行着 JS Framework,JS Framework 不仅提供了 jsbundle 必要的运行时环境, 同时还提供了与 native 通信的接口
而这个 JS Framework 就是我们今天介绍的重点
这是前端框架的主要架构:
FRONTEND FRAMEWORK/DSL: 这是 WEEX 的开发框架, 目前 WEEX 主要是使用 vue.js 进行开发
WEEX-vue-LOADER: 前端编译器, 将 vue 文件编译成 es5 代码
WEEX-VUE-FRAMWORK:WEEX 核心框架, 主要负责将 virtual dom 转换成 weex 的 native dom api
WEEX-RUNTIME: 负责与 native 渲染引擎对接, 将 native dom api 转换成对应平台 (AndroidiOS) 的 platform api, 然后传递给 native 渲染引擎, 由后者负责渲染工作
2 Vue.js
首先来看下前端开发框架 Vue.js,Vue.js 目前与 React Angular 构成了三大最流行的前端开发框架, Vue.js 具有组件化 virtual dom 以及 MVVM 三大特性, 还学习 React 的 Redux, 引入了状态管理模块 Vuex 同时相比起 React 主要基于 JSX,Vue.js 的开发模式更加清晰, 简单, 上手速度更快. vue 文件通常可以分为三部分:<template> <style> 和 <script>,<template> 是必须要有的, 其他可选其中 <script > 中的代码会保留或者被转换成 ES5 的语法;<style> 中的 CSS 在 Weex 平台上会被转换成 JSON 格式的样式声明, 放到组件的定义中去;<template> 会被编译生成组件定义中 render 函数, 可以理解为 render 函数的语法糖下文是一个简单的. vue 文件:
- <template>
- <div style="justify-content:center;">
- <text class="freestyle">Hello World!</text>
- </div>
- </template>
- <style scoped>
- .freestyle {
- text-align: center;
- font-size: 200px;
- }
- </style>
- 3 WEEX-VUE-LOADER
由于. vue 文件并不是标准的 JavaScript 代码, 该代码不能直接被 JS 执行引擎识别, 所以在编译过程中需要将. vue 文件转化成标准的 es5 代码而负责完成这一操作的就是 WEEX-VUE-LOADER
4 WEEX-VUE-FRAMEWORK&WEEX-RUNTIME
完成编译后, 输出的 bundle.js 即可被 JS 执行引擎所识别, 可以执行其逻辑了那么接下来就来看下 bundle.js 的执行过程
4.1 初始化
首先来看下初始化过程
图中画出了 Weex SDK 的部分内容其中 weex-vue-framework 和 Vue.js 是对等的, 语法和内部机制都是一样的, 只不过 Vue.js 最终创建的是 DOM 元素, 而 weex-vue-framework 则是向原生端发送渲染指令, 最终渲染生成的是原生组件 Weex Runtime 用来对接上层前端框架 ( Vue.js ) 并且负责和原生端之间的通信 Render Engine 就是针对各个端开发的原生渲染器, 包含了 Weex 内置组件和模块的实现, 可扩展
4.2 创建组件
weex 接收到 bundle.js 之后, 首先检查其合法性, 如果发现用的是 Vue 版本(weex 早期版本使用. we 语法), 就会调用 weex-vue-framework 中提供的 createInstance 创建实例一个 bundle.js 对应一个 weex 实例, 且每一个实例都有唯一的 instanceId, 与之对应实例与实例之间相互隔离, 互不干扰
在创建实例的过程中, bundle.js 会执行 new Vue()创建一个 vue 组件, 并通过其 render 函数创建 VNode 节点, 即 virtual dom 节点第二节中的示例代码会最终生成类似如下的 vnode 节点:
- {
- tag: 'div',
- data: {
- staticStyle: {
- justifyContent: 'center'
- }
- },
- children: [{
- tag: 'text',
- data: {
- staticClass: 'freestyle'
- },
- context: {
- $options: {
- style: {
- freestyle: {
- textAlign: 'center',
- fontSize: 200
- }
- }
- }
- },
- children: [{
- tag: '',
- text: 'Hello World!'
- }]
- }]
- }
- 4.3 patch
生成了 VNode 之后, 接下来需要将 VNode 同步到真实的 Dom 之上, 该过程在 Vue.js 中被称为 patch,patch 会比较新旧 VNode 之间的差异, 最小化操作集最后再将 Virual Dom 整体更新到真实 Dom 之上在执行 patch 之前的过程都是 Web 和 Weex 通用的, 所以文件格式打包编译过程模板指令组件的生命周期数据绑定等上层语法都是一致的
然而由于目标执行环境不同(浏览器和 Weex 容器), 在渲染真实 UI 的时候调用的接口也不同
在 vue.js 内部, weex 平台和 web 平台的 patch 方法有所不同简单来讲, 在 web 平台, patch 需要将 Virtual Dom 利用标准 Dom API 更新到真实 Dom 即可接下来的 UI 更新就交给浏览器即可
但是由于在 weex 平台下, 最终的 UI 渲染是由 native 渲染引擎执行的, native 渲染引擎不能识别 Dom API, 所以在 weex 平台下, 需要将 Virtual DOM 转化成 native 渲染引擎可以识别的 Native DOM API
4.3 发送渲染指令
在上层前端框架调用了 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: 'Hello World!' },
- style: { textAlign: 'center', fontSize: 200 }
- }]
- }
4.4 渲染原生组件
接下来就是 WEEX NATIVE SDK 的工作了, 具体逻辑在上一篇博客已经详细说明了, 此处仅简单说明
原生渲染器接收上层传来的渲染指令, 并且逐步将其渲染成原生组件
渲染指令分很多类, 文章中提到的两个都是用来创建节点的, 其他还有 moveElement updateAttrs addEvent 等各种指令原生渲染器先是解析渲染指令的描述, 然后分发给不同的模块关于 UI 绘制的指令都属于 "dom" 模块中, 在 SDK 内部有组件的实现, 其他还有一些无界面的功能模块, 如 stream navigator 等模块, 也可以通过发送指令的方式调用
这个例子里, 第一个 createBody 的指令就创建了一个 <div> 的原生组件, 同时也将样式应用到了改组件上第二个 addElement 指令向 <div> 中添加一个 <text> 组件, 同时也声明了组件的样式和属性值
上述过程不是分阶段一个一个执行的, 而是可以实现流式渲染的, 有可能第一个 <div> 的原生组件还没渲染好,<text> 的渲染指令又发过来了当一个页面特别大时, 能看到一块一块的内容逐渐渲染出来的过程
来源: https://yq.aliyun.com/articles/495695