Rax 天生就是一个跨容器的解决方案,这让我们不必单独了解 iOS 和 安卓背后做了什么,让我们的开发变得很省心。但真实情况是我们的业务往往同时跑在 web 和 native 两端,web 我们轻车熟路是前端天生的优势,native 对很多人来说就会有些陌生,native 上出现的各种问题用 web 的路子去解释往往会解释不通。据 2017 年底粗略统计 Rax 的用户 70% 是前端同学,本文面向前端,简单介绍一下 Rax 背后的 native 端在做的一些事情。本文吸取了大量前人的经验,对于过深的 native 概念进行了剔除,以求前端同学更好理解,如果想更深入了解的同学可以参考下面附件内容。
Rax 是一套基于 React 写法的 Weex 上层 DSL,很自然的我们就会拿来和 RN 进行对比。实际上 Rax 的设计上也是在尽量靠拢 RN。下面我们先来了解一下。
RN: Learn once, write anywhere
Rax 的使用方式和 RN 类似,RN 基于开源 JavaScript 库 React.js 来开发 iOS 和 Android 原生 App,在 JavaScript 中用 React 抽象操作系统原生的 UI 组件,代替 DOM 元素来渲染,比如以 View 取代 div,以 Image 替代 img 等(Rax 此处步调一致)。
一次学习,便可以编写 iOS 和 Android 两端的代码,这是 RN 的理念。与 RN 不同,Rax 在靠拢 RN 规范的同时,还做了另外一件事情就是跨端,让一次学习变成了一次编写。
作为背景了解可以了解 ReactNative 官网,RN 的学习资料比较容易获取这里不再展开。
Weex: write once run anywhere
Rax 在 Native 端的背后实现则是 Weex,Weex 是一款轻量级的移动端跨平台动态性技术解决方案
(图引自 ppt《weex-前端》by 子宽/饮源)
流程如下
Server 端主要职责是 weex DSL 到 JS 的转换,以及 JS 的部署下发
Client 端则负责客户端的绘制部分,具体如下:
(iOS 角度解析,弱化了部分纯客户端的概念)
SDK 初始化时比业务先注入 JS framework,如下
Weex runtime 初始化
name
,WXEnvironment
等全局的 APINative module
,component
等,给 JSContext 扩展 callback,全部转换成数字 id,每一个 module,component 等都有其对应的 id。framework.js
代码业务 bundle 初始化脉络研究
createInstance
方法,接收的参数是@[instance, temp, options ?: @{}];window api
注入创建 UI 界面的计算是在 JavaScript 这边的,创建和销毁阶段都是由 Native 主动调起 JavaScript 的方法。这里先介绍一下 JSCore,JavaScriptCore 是封装了 JavaScript 和 Objective-C 桥接的 Objective-C API,可以做到 JavaScript 调用 Objective-C,或者 Objective-C 调用 JavaScript。
createInstance 最后 sendTasks 实际上是调用 Weex 仓库 runtime/config.js 的 sendTasks ,而这个方法传递给 callNative。callNative,是Native注册的block,这是JS framework 与 Native 连接脉络的最后一步。
Weex 在 JS 端有一层 virtual-DOM 的设计,这一层设计一方面使得 Weex 能够通过 JS 控制 native 的视图层,另外也提供了一个相对中立的规范,供上层 JS 框架调用。
(图片引自《Weex 中的 virtual-DOM 介绍》 by 勾股)
我们在 Weex 中所能感受到的各种视觉效果和交互效果,实际上都是通过这样的 virtual-DOM 结构进行分解和执行的。virtual-DOM 的设计很大程度上借鉴了 HTML DOM 的设计,不论从 API 还是 class,但做了一定的简化和取舍,主要包括以下几点:
布局
Flexbox 是 Weex 中默认且唯一的布局模型,不需要手动为元素添加 display: flex; 属性。传统 web 布局花样繁多,不过 Weex 上我们布局的方式有些局限,比如我们无法使用 float 布局,对于 absolute 布局最好我们也需要谨慎使用(长列表性能考虑)。(布局样式参考这里
样式
Weex 中样式使用限制包括
Weex 对于 CSS 的支持相比 Web 弱了很多,很多酷炫的 CSS 必杀技在 Weex 上是不太好施展拳脚的,所以在我们写样式之前最好能先读一读 Weex 的 通用样式 和 文本样式
单位
Weex 中做元素的布局时需要先了解一下 Rax 在 Weex 上的单位,Weex 天生支持 wx 和 px
霸道的 list cell
除了 Web 和 Weex 的差异,在 iOS 和 Android 上同样存在细微的差异。Android list 中的 cell,无法展示内部超出的元素。如下图,我们如果要实现 图 1 的效果,如果直接用绝对定位飘出去,可能在安卓下会被 cell 截断得到图 2 的效果
Web 页面天生是可以滚动的,Native 中却不是。于是 Native 中提供了滚动容器来解决。Rax 中的滚动容器有如下几个(对于列表的详细分析这里也不展开,只说 list native 原理部分)
可回收的长列表 RecyclerView
Rax 的 RecyclerView 是一个高性能的可回收长列表,它的内部实现是 Weex 的 list 标签。Weex Android 的 List 的原生实现是 Android RecyclerView 组件,在 iOS 上则使用的是原生的 UITableView。它又一个重要特性就是可以回收非可视区域的 cell,并进行复用。
Android RecyclerView
在 Android 中,RecyclerView 提供了复用机制来减少内存开销、提升滑动效率,Weex 中 List 也暴露出相应的 API 支持 Cell 复用:设置相同 scopeValue 的 Cell 支持 ViewHolder 复用,这里的 ViewHolder 服用是只重复的数据类型复用,cell 内如果拥有相同的 children 结构,则该类型的 cell 可以复用,滑出可视区域的 cell 会被回收,在内部实现中不通 RecyclerView 的相同结构 cell 也有复用的策略。
iOS UITableView
UITableView 是一个以行数据概念实现的列表,每一行数据都是一个 UITableViewCell。
为了性能上更优,利用有限的结构动态切换其内容来尽可能减少资源占用,以达到 cell 复用。
UITableView内部有一个缓存池,初始化时使用 initWithStyle:(UITableViewCellStyle) reuseIdentifier:(NSString *) 方法指定一个可重用标识,就可以将这个 cell 放到缓存池。然后在使用时使用指定的标识去缓存池中取得对应的 cell 然后修改 cell 内容即可。以达到滚动时创建的 cell 地址是初始化时已经创建的。(详细不展开,原理在这里)
Web 中我们不指定图片的宽高页面会自动撑开,Weex 中却不会,内部实现的不同导致我们渲染图片的时候必须传入宽高(图片默认高度为 0)。虽然我们在图片的 onLoad 事件中可以拿到宽高信息,但此时再设置宽高会产生页面的抖动。在做瀑布流布局时图片的宽高就更加必要了。
另外 Weex 中相比 Web 图片额外做了加载的优化,我们不用考虑 Weex 上的图片懒加载。
不能设置背景图,只能使用图片插入到文档中
针对 gif 图片的显示依赖客户端的图片库,手淘环境可以在 attribute 和 style 上设置 quality='original' 解决,这个属性主要是让客户端的图片库不去优化该图片(避免一些 cdn 优化策略在某些 Weex 图片上不适用)
Weex 支持的事件类型有限,支持的事件类型见 通用事件,且不区分事件的捕获阶段和冒泡阶段,相当于 DOM 0 级事件。Appear 事件,Page 事件都是和传统 Web 开发思路有所不同的。
Weex 为我们提供了许多内建模块:
animation、WebSocket、picker、meta、clipboard、dom、modal、navigator、storage、stream、webview、globalEvent 等等。
这部分是区别于 Web 的一些功能,直接调用移动端设备能力,Rax 中使用内建模块方式如下:
let dom = require('@weex-module/dom');
dom.getComponentRect('viewport', (e) => {
console.log(e.result, e.size);
});
在 Rax 项目的实际开发过程中有很多和传统 Web 开发是不同的。我们更多的了解一些背后的机制,会对我们排查问题有所帮助。文章有遗漏之处或者不准确的地方欢迎指出。
← 淘宝技术部 2018 实习生内部推荐启动啦 实现一个 JavaScriptCore 的 debugger —— iOS 篇 →题图:https://unsplash.com/photos/i-P1lmY_e1w By @Sergey Pesterev