weex的工作原理

标签: | 发表时间:2021-06-15 14:27 | 作者:
出处:https://juejin.cn

18年的时候,公司有一段时间在推行weex技术栈,我们这边刚好有一个项目,于是有幸体验了一把weex开发。今天想把之前整理&总结一些的关于weex内容输出成文,同时回顾一下weex的工作原理。btw,白天上班,晚上写文,保持每日一篇真的好难🤦

weex介绍

Weex是一套支持跨平台、动态更新的使用Javascript进行原生APP开发的解决方案。 Weex的终极目标是带来iOS端、Android端和H5端一致的开发体验与代码复用。

当然了,到目前为止,weex离她的终极目标还是有一定的距离,那当然是祝福她早日实现目标了,但是她解决了快速发版,提高性能,统一三端三个难点。

Weex实现了统一的JSEngine和DOM API,因此并不完全限定在其上层使用的JS框架,理论上Weex允许在其上层使用Vue、React和Angular,我司用的是上层框架用的是Vue。

weex工作原理

1. 将weex源码生成JS Bundle

Weex首先将编写的Weex源码,也就是后缀名为.we的文件,由template、style 和 script等标签组织好的内容,通过transformer(转换器,weex-toolkit提供的工具)转换成JS Bundle。

这个过程分为三步:

  1. 把template中的内容转换为类JSON的树状数据结构, 转换数据绑定为返回数据的函数原型
  2. 把style转换为类JSON的树状数据结构
  3. 把上面两部分的内容和script中的内容结合成一个JavaScript AMD模块(AMD:异步模块规范)

除此之外,转换器还会做一些额外的事情:合并Bundle,添加引导函数,配置外部数据等等。当前大部分Weex工具最终输出的JSBundle格式都经过了Webpack的二次处理,所以实际使用工具输出的JS Bundle会有不同。

2. 服务端部署JS Bundle

将JS Bundle部署在服务器,当接收到终端(Web端、iOS端或Android端)的JS Bundle请求,将JS Bundle下发给终端。客户端从服务器更新包后即可在下次启动执行新的版本,而无需重新下载 app,因为运行依赖的WeexSDK 已经存在于客户端了,除非新包依赖于新的 SDK。

3. WEEX SDK初始化

JS Framework 以及 Vue 和 Rax 的代码都是内置在了 Weex SDK 里的,随着 Weex SDK一起初始化。Weex SDK的初始化一般在App启动时就已经完成了,只会执行一次。

Weex SDK初始化主要包含以下操作:

  1. 初始化 JS 引擎,准备好 JS 执行环境,向其中注册一些变量和接口,如 WXEnvironment、callNative。
  2. 执行 JS Framework 的代码。
  3. 注册原生组件和原生模块。

针对第二步,执行 JS Framework 的代码的过程又可以分成如下几个步骤:

  1. 注册上层 DSL 框架,如 Vue 和 Rax。这个过程只是告诉 JS Framework 有哪些 DSL 可用,适配它们提供的接口,如init、createInstance,但是不会执行前端框架里的逻辑。
  2. 初始化环境变量,并且会将原生对象的原型链冻结,此时也会注册内置的 JS Service,如 BroadcastChannel。
  3. 如果 DSL 框架里实现了 init 接口,会在此时调用。
  4. 向全局环境中注入可供客户端调用的接口,如callJS、createInstance、registerComponents,调用这些接口会同时触发 DSL 中相应的接口。

下面详细介绍一下JS Framework。

JS Framework 的功能

Weex 是一个既支持多个前端框架又能跨平台渲染的框架,JS Framework 介于前端框架和原生渲染引擎之间,处于承上启下的位置,也是跨框架跨平台的关键。无论你使用的是 Vue 还是 Rax,无论是渲染在 Android 还是 iOS,JS Framework 的代码都会运行到(如果是在浏览器和 WebView 里运行,则不依赖 JS Framework)。

1. 适配前端框架

前端框架在 Weex 和浏览器中的执行过程不一样,这个应该不难理解。如何让一个前端框架运行在 Weex 平台上,是 JS Framework 的一个关键功能。

以 Vue.js 为例,在浏览器上运行一个页面大概分这么几个步骤:首先要准备好页面容器,可以是浏览器或者是 WebView,容器里提供了标准的 Web API。然后给页面容器传入一个地址,通过这个地址最终获取到一个 HTML 文件,然后解析这个 HTML 文件,加载并执行其中的脚本。想要正确的渲染,应该首先加载执行 Vue.js 框架的代码,向浏览器环境中添加 Vue 这个变量,然后创建好挂载点的 DOM 元素,最后执行页面代码,从入口组件开始,层层渲染好再挂载到配置的挂载点上去。

在 Weex 里的执行过程也比较类似,不过 Weex 页面对应的是一个 js 文件,不是 HTML 文件,而且不需要自行引入 Vue.js 框架的代码,也不需要设置挂载点。过程大概是这样的:首先初始化好 Weex 容器,这个过程中会初始化 JS Framework,Vue.js 的代码也包含在了其中。然后给 Weex 容器传入页面地址,通过这个地址最终获取到一个 js 文件,客户端会调用 createInstance 来创建页面,也提供了刷新页面和销毁页面的接口。大致的渲染行为和浏览器一致,但是和浏览器的调用方式不一样,前端框架中至少要适配客户端打开页面、销毁页面(push、pop)的行为才可以在 Weex 中运行。

在 JS Framework 里提供了如上图所示的接口来实现前端框架的对接。图左侧的四个接口与页面功能有关,分别用于获取页面节点、监听客户端的任务、注册组件、注册模块,目前这些功能都已经转移到 JS Framework 内部,在前端框架里都是可选的,有特殊处理逻辑时才需要实现。图右侧的四个接口与页面的生命周期有关,分别会在页面初始化、创建、刷新、销毁时调用,其中只有 createInstance 是必须提供的,其他也都是可选的(在新的 Sandbox 方案中,createInstance 已经改成了 createInstanceContext)。

2. 构建渲染指令树

在浏览器上它们都使用一致的 DOM API 把 Virtual DOM 转换成真实的 HTMLElement。在 Weex 里的逻辑也是类似的,只是在最后一步生成真实元素的过程中,不使用原生 DOM API,而是使用 JS Framework 里定义的一套 Weex DOM API 将操作转化成渲染指令发给客户端。

JS Framework 提供的 Weex DOM API 和浏览器提供的 DOM API 功能基本一致,在 Vue 和 Rax 内部对这些接口都做了适配,针对 Weex 和浏览器平台调用不同的接口就可以实现跨平台渲染。

3. JS-Native 通信

在开发页面过程中,除了节点的渲染以外,还有原生模块的调用、事件绑定、回调等功能,这些功能都依赖于 js 和 native 之间的通信来实现。

首先,页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通信用的是 callNative 和 callJS 这两个底层接口(现在已经扩展到了很多个),它们默认都是异步的,在 JS Framework 和原生渲染器内部都基于这两个方法做了各种封装。

callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用,界面的节点(上文提到的渲染指令树)、模块调用的方法和参数都是通过这个接口发送给客户端的。为了减少调用接口时的开销,其实现在已经开了更多更直接的通信接口,其中有些接口还支持同步调用(支持返回值),它们在原理上都和 callNative 是一样的。

callJS 是由 JS Framework 实现的,并且也注入到了执行环境中,提供给客户端调用。事件的派发、模块的回调函数都是通过这个接口通知到 JS Framework,然后再将其传递给上层前端框架。

4. JS Service

Weex 是一个多页面的框架,每个页面的 js bundle 都在一个独立的环境里运行,不同的 Weex 页面对应到浏览器上就相当于不同的“标签页”,普通的 js 库没办法实现在多个页面之间实现状态共享,也很难实现跨页通信。

JS Framework 中实现了 JS Service 的功能,主要就是用来解决跨页面复用和状态共享的问题的,例如 BroadcastChannel 就是基于 JS Service 实现的,它可以在多个 Weex 页面之间通信

5. 准备环境接口

由于 Weex 运行环境和浏览器环境有很大差异,在 JS Framework 里还对一些环境变量做了封装,主要是为了解决解决原生环境里的兼容问题,底层使用渲染引擎提供的接口。主要的改动点是:

  • console: 原生提供了 nativeLog 接口,将其封装成前端熟悉的 console.xxx 并可以控制日志的输出级别。
  • timer: 原生环境里 timer 接口不全,名称和参数不一致。目前来看有了原生 C/C++ 实现的 timer 后,这一层可以移除。
  • freeze: 冻结当前环境里全局变量的原型链(如 Array.prototype)。

另外还有一些 ployfill:Promise 、Arary.from 、Object.assign 、Object.setPrototypeOf 等。

4. 执行JS Bundle

在初始化好 Weex SDK 之后,就可以开始渲染页面了。通常 Weex 的一个页面对应了一个 js bundle 文件,页面的渲染过程也是加载并执行 js bundle 的过程。

首先是调用原生渲染引擎里提供的接口来加载执行 js bundle,在 Android 上是 renderByUrl,在 iOS 上是 renderWithURL。在得到了 js bundle 的代码之后,会继续执行 SDK 里的原生 createInstance 方法,给当前页面生成一个唯一 id,并且把代码和一些配置项传递给 JS Framework 提供的 createInstance 方法。

在 JS Framework 接收到页面代码之后,会判断其中使用的 DSL 的类型(Vue 或者 Rax),然后找到相应的框架,执行 createInstanceContext 创建页面所需要的环境变量。

在旧的方案中,JS Framework 会调用 runInContex 函数在特定的环境中执行 js 代码,内部基于 new Function 实现。在新的 Sandbox 方案中,js bundle 的代码不再发给 JS Framework,也不再使用 new Function,而是由客户端直接执行 js 代码。

创建 weex 实例

当WEEX SDK获取到JS Bundle后,第一时间并不是立马渲染页面,而是先创建WEEX的实例。

每一个JS bundle对应一个实例,同时每一个实例都有一个instance id。

由于所有的js bundle都是放入到同一个JS执行引擎中执行,那么当js执行引擎通过WXBridge将相关渲染指令传出的时候,需要通过instance id才能知道该指定要传递给哪个weex实例。

在创建实例完成后,接下来才是真正将js bundle交给js执行引擎执行。

weex渲染流程

Weex 里页面的渲染过程和浏览器的渲染过程类似,整体可以分为【创建前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送渲染指令】->【绘制原生 UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在 JS Framework 中处理,最后一步是由原生渲染引擎实现的。 页面渲染的大致流程如下:

创建前端组件

Vue 框架在执行渲染前,会先根据开发时编写的模板创建相应的组件实例,可以称为 Vue Component,它包含了组件的内部数据、生命周期以及 render 函数等。如果给同一个模板传入多条数据,就会生成多个组件实例,渲染时会创建多个 Vue Component 的实例,每个组件实例的内部状态是不一样的。

构建 Virtual DOM

Vue Component 的渲染过程,可以简单理解为组件实例执行 render 函数生成 VNode 节点树的过程,也就是构建 Virtual DOM 的生成过程。

生成“真实” DOM

上面两个过程,在Weex 和浏览器里都是完全一样的,从生成真实 DOM 这一步开始,Weex 使用了不同的渲染方式。JS Framework 中提供了和 DOM 接口类似的 Weex DOM API,在 Vue 里会使用这些接口将 VNode 渲染生成适用于 Weex 平台的 Element 对象,和 DOM 很像,但并不是“真实”的 DOM。在 Vue 和 Rax 内部对这些接口都做了适配,针对 Weex 和浏览器平台调用不同的接口就可以实现跨平台渲染。

发送渲染指令

在 JS Framework 内部和客户端渲染引擎约定了一系列的指令接口,对应了一个元素的 DOM 操作,如 addElement removeElement updateAttrs updateStyle 等。JS Framework 使用这些接口将自己内部构建的 Element 节点树以渲染指令的形式发给客户端。

绘制原生 UI

客户端接收 JS Framework 发送的渲染指令,创建相应的原生组件,最终调用系统提供的接口绘制原生 UI。

同样的一份JSON 数据,在不同平台的渲染引擎中能够渲染成不同版本的 UI,这是 Weex 可以实现动态化的原因。

事件的响应过程

无论是在浏览器还是 Weex 里,事件都是由原生 UI 捕获的,然而事件处理函数都是写在前端里的,所以会有一个传递的过程。

如上图所示,如果在 Vue.js 里某个标签上绑定了事件,会在内部执行 addEventListener 给节点绑定事件,这个接口在 Weex 平台下调用的是 JS Framework 提供的 addEvent 方法向元素上添加事件,传递了事件类型和处理函数。JS Framework 不会立即向客户端发送添加事件的指令,而是把事件类型和处理函数记录下来,节点构建好以后再一起发给客户端,发送的节点中只包含了事件类型,不含事件处理函数。客户端在渲染节点时,如果发现节点上包含事件,就监听原生 UI 上的指定事件。

当原生 UI 监听到用户触发的事件以后,会派发 fireEvent 命令把节点的 ref、事件类型以及事件对象发给 JS Framework。JS Framework 根据 ref 和事件类型找到相应的事件处理函数,并且以事件对象 event 为参数执行事件处理函数。目前 Weex 里的事件模型相对比较简单,并不区分捕获阶段和冒泡阶段,而是只派发给触发了事件的节点,并不向上冒泡,类似 DOM 模型里 level 0 级别的事件。

上述过程里,事件只会绑定一次,但是很可能会触发多次,例如 touchmove 事件,在手指移动过程中,每秒可能会派发几十次,每次事件都对应了一次 fireEvent -> invokeHandler 的处理过程,很容易损伤性能,浏览器也是如此。针对这种情况,可以使用用 expression binding 来将事件处理函数转成表达式,在绑定事件时一起发给客户端,这样客户端在监听到原生事件以后可以直接解析并执行绑定的表达式,而不需要把事件再派发给前端。

以上就是weex的基本工作原理了,下面看下weex的应用。

weex的三种工作模式

1. 全页模式

目前支持单页使用或整个App使用weex开发(还不完善,需要开发Router和生命周期管理),这个可以类比React Native。

2. Native Component模式

把weex当作一个IOS/Android组件来使用,类比ImageView。但是局部动态化需求旺盛会导致频繁发版。

3. H5 Component模式

在H5中使用weex,类比WMC。在现有的H5页面上做微调,引入Native解决长列表内存暴增、滚动不流畅、动画/手势体验差等问题。

weex与H5 Hybird比较

从前,实现一个需求,需要三种程序员(iOS,android,前端)写三份代码,这就带来了很大的开发成本,所以业界一直在探索跨平台技术方案。从之前的Hybrid,到现在的Weex,React Native,这些方案的根本目的都是一套代码,多端运行。

H5 Hybrid方案的本质是利用客户端APP的内置浏览器功能(即webview),通过JSBridge实现客户端Native和前端JS的通信问题,然后开发H5页面内嵌于APP中,该方案提升了开发效率,也满足了跨端的需求,但是有一个问题就是,前端H5的性能和客户端的性能相差甚远。

而weex采取H5页面的开发方式,同时在终端的运行体验不输Native App。weex利用Native的能力去做了部分浏览器去做的工作。

weex的优势

参考: 详细介绍 Weex 的 JS Framework

相关 [weex 工作 原理] 推荐:

weex的工作原理

- -
18年的时候,公司有一段时间在推行weex技术栈,我们这边刚好有一个项目,于是有幸体验了一把weex开发. 今天想把之前整理&总结一些的关于weex内容输出成文,同时回顾一下weex的工作原理. btw,白天上班,晚上写文,保持每日一篇真的好难🤦. Weex是一套支持跨平台、动态更新的使用Javascript进行原生APP开发的解决方案.

【Weex】网易严选 App 感受 Weex 开发 · Issue #3 · zwwill/blog · GitHub

- -
自打出生的那一天起,Weex 就免不了被拿来同 React Native「一决高下」的命运. React Native 宣称「Learn Once, Write Anywhere」,而 Weex 宣称「Write Once, Run Everywhere」. 在我看来,并没有谁更好,只有谁更合适. 下面我将围绕 Weex 入门进行讲解.

Weex在达人店的一年实践

- - SegmentFault 最新的文章
Weex在达人店的一年实践. 本文来自尚妆移动端团队 路飞. 发表于尚妆github博客,欢迎订阅. 尚妆达人店接入weex也一年的时间了,在此期间,也陆陆续续出了一些文章:. 「Android」 详细全面的基于vue2.0Weex接入过程(Android视角). 「前端」weex页面传参. 「大前端」weex里native主动发送事件到JS的方案实现.

SSL工作原理

- - 互联网 - ITeye博客
SSL 是一个安全协议,它提供使用 TCP/IP 的通信应用程序间的隐私与完整性. 超文本传输协议 (HTTP)使用 SSL 来实现安全的通信. 在客户端与服务器间传输的数据是通过使用对称算法(如 DES 或 RC4)进行加密的. 公用密钥算法(通常为 RSA)是用来获得加密密钥交换和数字签名的,此算法使用服务器的SSL数字证书中的公用密钥.

CDN 工作原理

- - 互联网 - ITeye博客
1.CDN的实现基础是通过将DNS的域名设置为CNAME(就是指向另一个域名,但这个域名也是有A记录的(就是域名对应IP)). 2.CNAME的作用就是要求浏览器发送域名到CNAME这个域名的服务器去解析得到域名的IP,就是本地DNS不作IP解析,而由CNAME指定的服务器去解析. 3.CNAME指定的服务器去解析的IP地址是CDN的全局负载均衡设备IP.

基于weex的有赞无线开发框架

- - SegmentFault 最新的文章
出于对开发效率和动态化的要求,无线端的开发框架也一直在更新,从 Hybrid、结构化 Native View、React Native、Weex,再到现在正在大受关注的 Flutter. 什么样的框架才是适合自己的团队. 不仅要有技术追求,而且要考虑实际业务需要. 最近,有赞移动选择了 weex 作为无线开发框架,搭建了从开发、Debug、构建、发布、数据一个闭环的流程.

oracle工作原理摘录

- - Oracle - 数据库 - ITeye博客
引自:http://blog.sina.com.cn/s/blog_79921f110100xffb.html. ORACLE工作原理1-连接. 我们从一个用户请求开始讲,ORACLE的完整的工作机制是怎样的,首先一个用户进程发出一个连接请求,如果使用的是主机命名或者是本地服务命中的主机名使用的是机器名(非IP地址),那么这个请求都会通过DNS服务器或HOST文件的服务名解析然后传送到ORACLE监听进程,监听进程接收到用户请求后会采取两种方式来处理这个用户请求,下面我们分专用服务器和共享服务器分别采用这两种方式时的情况来讲:.

Memcache工作原理总结

- - Java - 编程语言 - ITeye博客
1.  分片原理. 咱们废话话不多说了,直接看Memcache的原理. 首先memcache解决的最大的一个问题就是内存多次读取的内存碎片问题. 内存碎片分为内存内部碎片和内存外部碎片. 一般是指在外部碎片中出现了不连续的细小内存片段,不能够被进程利用.

JVM工作原理介绍

- - 开源软件 - ITeye博客
一、          JVM 的生命周期. 1.       JVM实例对应了一个独立运行的java程序它是进程级别. 启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点.

阿里巴巴向 Apache基金会捐赠移动开发框架Weex

- - ITeye资讯频道
12月15日,阿里巴巴宣布将移动开源项目 Weex 捐赠给 Apache 基金会开始孵化,Weex 有望成为中国移动领域的首个Apache顶级项目,这意味着中国移动技术开始反哺世界. 据悉,这也是继JStorm、RocketMQ 之后,阿里向 Apache 捐赠的第三个项目. 2016 年 12 月 15 日,阿里巴巴宣布将移动开源项目 Weex 捐赠给 Apache 基金会开始孵化.