React Native 官方支持 iOS 和 Android,但并没有覆盖 Web,有些人试图做出补救,如兼容 React Native API 的 ReactWeb,也有人用在 React Native 上再封装一层的形式来兼容 Web。ReacMix 采取的就是后面的这种做法。ReactMix 作者在 QCon 北京 2016 上对这个框架进行了分享,本文由演讲总结整理而成。
嘉宾介绍
薛端阳,目前就职于上海携程,机票事业部无线研发团队高级技术经理,曾先后就职于焦点 / 淘宝 / 盛大 /IBM/ 苏宁 / 腾讯,在腾讯期间,主要负责腾讯电商旗下门户网站易迅网前端架构,以及微信购物 H5 移动端,开源过前端 UI 框架 KitJs,前端无限容量存储方案 localStore,前端多线程模板渲染引擎 MutiTpl,基于.Net 非 Node 环境下的前后端首屏直出框架 Fplus,以及最新基于 React Native 底层方案的 ReactMix,跨平台业务 JS 前台解决方案等。
ReactMix 是在 React Native 和 ReactJS 的基础上,全新架构一层 Framework 和自动化翻译工具,通过相应的翻译机制和扩展模式,将现有的浏览器中可执行的 HTML 页面、JS 代码和 CSS 样式,同步翻译成为 React Native 可以执行的代码,从而获得在 App 上直接运行的能力,同时具备原生的 App 体验的效果。
其 Github 地址为: https://github.com/xueduany/react-mix
ReactMix 的诞生
ReactMix,看名字就知道与 React 相关,它可以从前端的思路上帮助开发者把现有的 H5 代码平滑地转换成 ReactNative 代码。
React Native 目前存在着几方面的问题:第一,React Native 目前只支持内联的样式,不支持我们常用的 CSS className 继承和复用;第二,React Native 采用了类似 ReactJS 的语法风格,要求有组件封装,在组件 A 和组件 B 之间内部互相通信会比较麻烦;第三,对于已有的项目,如果改写成 React Native,就会有重构成本。这些问题都促进了 ReactMix 的诞生。
那么携程要开发 ReactMix 呢?因为携程 90% 的代码是历史代码,我们希望能够把这部分代码很平滑的地变成 React Native 代码。同时,我们希望能够使用 H5 的特性,提高现有的代码性能,包括渲染性能和执行性能。其实,最主要的原因是节省成本。综合这些原因,我们开发了 ReactMix,用来帮助我们很平滑地过渡到 React Native,解决一套代码完成 H5、Android、iOS 通用的问题。据说微软也做了一个插件来支持 UWP 平台,理论上 ReactMix 在这个插件下也是可以执行的。所以,可以说 ReactMix 可以通吃移动端了。
ReactMix 第一印象
首先看一下 ReactMix 的代码,如下图所示。
(点击放大图像)
从图中我们可以得出比较直观的特点:第一,它的写法类似JS;第二,采用了标记的写法,去重构我们的页面。而且,与阿里巴巴的Weex 相比最大的不同是:Weex 是单个class,而ReactMix 不只是单个class。这样一来,ReactMix 就类似H5——H5 里肯定有class name 合并,两个class name 同时存在的时候才可以,这可以帮助程序员重构页面。所以,第三个特点是它很像H5。
ReactMix 如何支持 CSS
ReactMix 可以直接使用原始版本 CSS,那么它是怎么用的呢?React Native 在加载模块的时候是通过关键字进行加载的:找到文件的路径,使用 require(),在运行的时动态解析 require 进行加载。ReactMix 把这两者结合在一起就成了第一个 API,使用时可以给它起一个别名,编译器会自动进行翻译。它可以被动态地加载到浏览器运行环境中。但是在 ReactMix 里面只需要写一个 require,再加一个字符串,就可以引用这个文件。它的加载方式基于静态语法分析,巧妙利用了 React Native 的 require 关键字是静态编译加载的特性。
ReactMix 支持 CSS 属性简写。React Native 只是 CSS 文件的类别写法,很多属性必须明确其含义,不支持常用 CSS 的简写。有些属性 React Native 帮你做了简写,有些没做,所以需要有一个工具来做静态语法分析。ReactMix 提供了一个工具,可以动态监测 CSS 文件的变化,进行动态翻译,翻译成和该 CSS 文件在 React Native 中对应的 JS 文件。将这个翻译后的 JS 文件给 React Native 用,而开发者在开发的时候看到的是一个 CSS 文件,也就是原始的文件。
除了简写,ReactMix 还需要支持 media query。那么 ReactMix 如何支持 media query 呢?media query 中的属性都是一个动态属性,在执行上下文环境时候动态计算,得到当前页面布局的结果。以 media query 举例,要判断屏幕是大屏还是小屏。需要在页面开始渲染的时候有一个环境上下文,根据变量来去执行 CSS 里面的具体内容。ReactMix 相当于对每个组件的方法进行重构,插入对于现有已支持的 CSS 文件动态解析,并根据这个动态解析内容,结合上下文环境,做到了对于 media query 选择器做进一步的支持。ReactMix 有一个动态语法分析,动态解析用户所做的定义,在运行的时分别定义这些元素,做类似等价的实现换算。也就是运行时动态编译。
上文提到的大部分内容是 CSS 文件加载以及 CSS 文件动态编译,其实是基于两种特性,一个特性是静态翻译,二是运行时翻译。那么还需要支持别的东西吗?
我们经常会遇到 CSS 的基本单位、度量单位不统一的问题。例如,iPhone 5 和 iPhone 6 相比,iPhone 6 的屏幕要比 iPhone 5 要大。假如按照 iPhone 5 做尺寸布局,比如边距定义为 5,那么 iPhone5 上就会觉得小,而 iPhone 6 上字体就会显得小。在 iOS 开发中,这被称为适配工作。一般情况下 iOS 开发不需要做适配,内容随着屏幕分辨率变大而变大,根据度量单位的方式来统一度量。有了这个度量单位,就可以去实现在三个端,或者未来四个端的整体布局方式,达到完全一样的效果。
经过静态翻译和动态翻译,以及 CSS 文件支持,再加上统一度量单位,最后渲染数出来的结果和在浏览器上看到的结果是一模一样的。
前面提到,像 React Native 只支持内联的 CSS。假如要实现这样一个例子:class A 和 class B,最终节点渲染需要往下查父节点,判断最终节点里有哪些 class 属性。具体来说,有个节点,它有 classname 属性,在 CSS 文件里定义可能就是 class A,要满足父节点包含 class B,父父节点包含 class C。对于 ReactMix 来说,如果直接按照 class A、B、C 的方式不能做继承关系,只能做组合关系,即以 ABC 三个样式组合在一起,可以看到样式组合。这个时候要以最终生效的节点,即 class C,做个标记。比如做成向上的小箭头,后面跟着它的值,组合成一个 k-v map,然后定义在 classB 之前有哪些限制条件。然后再对这个节点做向上查找,看副节点是否满足包含 class 的且是最后元素。如果包含取出来节点包含该 className,就认为它满足条件,直到查找到根节点。这是在写 H5 时经常被用到的例子。
如果需要去解析经 CSS 文件里面的单位要怎么做呢?首先在运行时候拿到页面显示的 API,算出页面的尺寸大小,然后做计算,生成按照当前屏幕大小动态显示的值,最后把这个值放到上文提到的方法中,得到节点的属性。
如何支持 HTML
解决了支持 CSS 的问题之后,要解决 HTML 的问题。对于 HTML 来说,刚好可以对应到 div 和 span、image 可以实现对应的例子。但你需要去做什么事呢?在浏览器里面,image 可以动态获知图片大小,根据图片大小给它定义一定大小的容器,而在 React Native 里面不先定义大小是没办法显示图片的。使用 ReactMix 后,可以不需要知道图片大小,它会根据图片内容动态图片信息,自动复制图片大小,把这个方法包装成同步的方法。
HTML 节点的常用 API 允许直接写自定义事件,同时得到事件的传递参数,和 H5 的标准事件是一样的。那么这是怎么实现的呢?对于 onclick 和 ontouch 来说,首先统一做事件委托,定义好委托模型,然后在这些委托模型里面填充注册文件。还需要利用冒泡机制,做上提操作,把很多事件统一放在一个大容器上,在处理事件时再判断该触发事件,而不是以就近的方式去处理。
ReactMix 给每个元素去创建事件委托,在每个委托内,都抽象了对于标准事件的模拟,实现了类似 domevent 的对象,可以手动触发它的冒泡机制。这里还有一个比较讨厌的点,去找一个具体的元素话会有这样一个问题:只能局限在 component 里面,如果把它分割过小就需要对 component 子元素进行修改,基于 React Native 来写比较麻烦。需要对子元素做一个属性,定义一个标签,找到 component A 的节点。ReactMix 可以基于 sizzle 选择器找到这个节点,支持三种选择器,可以通过整体的继承关系去找到用户想要的节点,返回的就是 element 对象。
ReactMix 是怎么找到这个元素的呢?其实找到元素的方法有很多,在每个节点动态计数,如果用户丢添加 NodeId 那么它不会添加,如果用户没添加那么它会动态添加 ID。有了 ID,它会注册一个数组,把这个 ID 添加上去,可以根据数据节点找这个元素,也可以在里面找。如果根据 classname 来找也有对应的数据。刚才已经提到,ReactMix 封装了对象,可以判断关系是否正确,根据这个关系可以找到用户动态需要节点。
ReactMix 创建了事件委托,可以提供标准的 HTML 的事件,可以支持标准的 CSS 文件和属性,扩展了元素的实现。这些步骤全部做完之后,就可以把 React Native 做得类似于 H5 开发。剩下的问题就比较简单了,就是 API 同步问题。React Native 里面有一些 API 是异步的,但也有一些是同步的。在 React Native 里面,有一个异步 API,那么对于浏览器来说,同样一段代码,比如用 H5 写的同步代码,来变成 React Native 代码,那么需要 async+await 实现 codestyle 的同步。这需要增加一些成本,它是新增的语法,理解完之后还需要识别哪些 API 是同步的,哪些是异步的。这里需要加关键字,把异步代码转成同步代码,ReactMix 把异步的 API 放在关键字列表里面,动态编译的时动态添加异步关键字,这对于开发人员完全透明。
关于第三方自定义控件
ReactMix 基于 ES7 的语法,所以需要通过 babel 等工具做代码降级。前文说的都是 ReactMix 基于加强方式做代码,比如大家都使用的 React Native,或者有一个项目引入了第三方插件,那么怎么实现这些框架之间的互相兼容呢。这就像两只孙悟空,都是猴子,都是 72 变,它们的功能完全一样的,只要不存在命名冲突,理论上就不会冲突。也就是说,它是语法插件,给用户的是无侵入式的体验。用户可以在后台使用 React Native,也可以使用其他类似的实现。如果想基于 H5 方式,比如把现有 H5 代码,以及 JS+CSS 代码很平滑转到别的平台上,那么不用改任何代码就可以把它很平滑地转移过去。包括 require 也一样,这个东西也可以动态根据不同平台来定义不同的实现。
上线代码会不会太大
前面这些问题都解决完之后,大家会思考:ReactMix 提供了很多语法糖支持,转了很多代码,那么上线代码会不会太大?这也是大家很关心的问题。
为了解决这个问题,我们会把上线代码做一个拆分,拆分为两个部分。第一部分是用户要经常使用的一部分,包括 H5 本身的代码,包括语法糖的代码,它的大小是 200K 左右,加在一起差不多 300K 左右大小,大部分并行操作是静态编译实行的。对于真正的业务来说,它的包肯定更小,一般情况下是 300K 左右。比如要实现具体机票预定流程和航班动态查询,加上 CSS 和 JS 文件,都加在一起差不多是这个大小。页面的平均渲染时间差不多是 iOS 200ms,Android 400ms,平均比 hybrid 加载速度快 30%,我们希望能够使用 ReactNative 帮助项目性能进行整体加速。ReactMix 项目已经开源了,github 地址是 https://github.com/xueduany/react-mix ,里面包含目前已经完成的语法糖静态编译工具和运行时的编译工具,大家可以关注一下。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论