作者简介
郑勇,携程高级技术经理,目前主要负责CRNWEB框架的开发工作,以及在携程内部的使用推广和性能优化。
前言
React-Native自从2015年推出,就一直火到了现在,一度在技术圈言必RN,激发一波广泛的思潮。携程基础业务研发团队迅速跟进,在React-Native基础之上,开发出了CRN这一适合携程业务高速发展的、抹平了iOS和Android端组件开发差异的、做了大量性能提升的框架。然而无论是CRN还是React-Native本身都无法解决移动板块中的一大版图——WEB平台。
而现实是:存在大量的业务需求需要三端的支持,单独再开发一套H5成本高昂,后期的维护成本也很高,需求同步难,用户体验不一致等问题都会非常明显,而携程基础业务前端框架团队一直都在致力于解决iOS和Android之后,将BU业务代码无缝接入WEB平台的技术方案,于是CRN-WEB(简称CW)应运而生。
一、CRNWEB是什么?
CRN-WEB的使命就是在CRN和React-Native的基础之上,构建一个三端打通的平台,能够实现BU的一套业务逻辑代码,能够根据平台情况运行在三端之上,并带来用户体验上的一致性(和React-Native保持一致)和优越性(使用Virtual DOM,PWA等技术提升性能)。
1、设计共性
对于CRN-WEB这样一个框架,我们在设计之初就可以提取一些软件设计方面共性的问题:
1)易用性,CW框架必须简单易用,大幅度降低开发成本、运维成本和学习成本,将是这个框架的核心价值,如何做到呢?
2)一致性,和现有技术框架的集成问题,即如何将CRN-WEB与CRN和React-Native进行友好的集成,各自发挥各自的功能,如何保证各平台间的一致性?
3)稳定性,React-Native版本迭代迅速,版本间差异较大,既然三端打通,共用BU源码,那么BU的React-Native项目或者CRN项目在接入CW框架后,必须能够稳定运行在WEB平台上,如何保证项目稳定运行?
4)兼容性,WEB平台是非多(浏览器厂商多,版本多,私有规范多,差异多...),兼容性问题一直是WEB项目开发头疼的事情,如何处理好兼容性问题?无疑是非常棘手的。
5)扩展性,包括React-Native本身都还在不断的变动,增加新功能,再加上公司级别的功能性需求,业务级别的功能需求,将令如何保持框架扩展性变得非常麻烦。
2、我们的设计思想
That‘s a big business,的确,这些问题很难处理,但是经过深入的思考,我们提出了这样的设计思想。
React-Native为解决iOS和Android两端兼容提供了解决方案,它是如何做到的呢?当然RN团队经过了大量的工作和思考,最终他们提供了一套规范,即React-Native,与其说它是一个框架不如说它是一套规范,对,我就是这么认为的。
如果CRNWEB的设计也基于React-Native的规范,把React-Native抽象成一个逻辑层,为不同的平台提供相同的Component和API输出和相同的APP主要运行流程,然后在规范之下各个平台各自实现,即iOS Implement,Android Implement,WEB Implement,那么从设计上来看是比较完美的。
对于业务方而言如Flight项目,Hotel项目等等,无需关心底层的技术实现,使用React-Native这一套开发技术体系基本上就足矣。
3、设计优势
这样设计同时还可以解决好几个问题:
比如易用性,我们采用了React-Native的规范,那么我们就可以使用开发人员熟悉的技术,熟悉的规范,熟悉的知识,熟悉的流程,无需额外学习太多其它规范和技术栈。
否则BU的学习成本,接入成本太高,起不到降低开发成本的作用,当然为了解决易用性,还有很多其它方面的工作,比如提供一整套的开发流程,开发工具,发布工具,技术支持等等。
再比如一致性问题,和React-Native,CRN使用相同的规范,这样的设计保持了天然的一致性。
二、CRNWEB是如何工作的呢?
我们依然从程序设计的传统,Hello wolrd开始。
熟悉React-Native的同学可能一眼就能够看出来,这完全就是React-Native的原代码,你说的对,它不仅是一份RN的源代码,也是一份CRN-WEB的源代码。它虽然是一个最简单的Hello World,但是它几乎包含了React-Native的Component和API,以及主要的运行流程。
1、主题结构分成三个部分:
1)头部的依赖部分,使用ES6的语法import,导入依赖的程序包React和React-Native;
2)中间部分实现模块主要逻辑;
3)尾部使用ES6语法export导出模块输出;
在CRN-WEB中也是这样,毫无差异。
2、那么CRNWEB是如何让和React-Native相同的源代码运行在Web平台的呢?
要实现这种能力,那么它必须满足两个最重要的必要条件。第一点,我们要实现在Web平台上面,跟React-Native上面具有相同功能的Component和API,比如这里的View和Text,这个就是我们后面要讲到的组件系统。
第二点,我们要有一种机制使得我们的React-Native原代码能够在Web上面运行起来,调用我们WEB平台上的Component和API,使得我们对代码拥有足够的控制能力。这个就是我们后面要讲到的打包系统。
三、运行分析
HelloWorld代码编写完成,配置好环境,执行CRNWEB命令,查看编译后运行效果和运行结果。
1、入口组件
CRNWEB仍然使用了AppRegistry作为程序入口注册组件,当然这里的AppRegistry已经被换成了AppRegistry.web这个WEB版本的Implementation,AppRegistry实现了registerComponent作为程序入口,承担App初始化工作,例如:
1)运行环境初始化,例如识别是h5还是hybrid;
2)注入默认的全局性样式,例如抹平浏览器差异的样式;
3)全局性请求参数的解构和传递;
4)初始化全局性组件的容器等等;
2、同步组件的异步转换
HelloWorld组件就是一个标准的React-Native组件,在CRNWEB中为了提高性能,将HelloWorld组件转化为异步组件HelloWorld(__CRNWEBFUNCTION__),从而实现页面级别的按需加载,仅在需要的页面运行时进行加载。
这在WEB环境下是非常重要的一项优化,这是专门针对WEB环境下脆弱的网络环境而作出的改进,特别是在页面众多,组件数量大,组件体量大的较大型WEB项目中,性能提升非常显著,这在BU的实践中得到了的认可。
3、具体的业务逻辑页面的编译转化
而原来的HelloWorld业务逻辑被打包到了模块号为97的package中,并处理好了它的依赖,如下:
我们可以看到原来的ES6语法书写的代码被编译成了ES5语法的代码,因为ES5在WEB环境下有着更广泛和友好的支持,兼容性更好。而HelloWold中引入的View,Text,StyleSheet等等组件,也全部变成了WEB版本的具体实现,这里使用了一招瞒天过海。
4、组件系统
而View,Text等等众多的React-Native原生组件对应的WEB版本的具体实现,就构成了CRNWEB的组件系统,篇幅有限不做展开。
5、样式处理
而HelloWorld里引入的StyleSheet就是样式处理系统中的入口文件。
CRNWEB的样式处理系统我们主要提供四种方式:
首先是APPRegistry,我们需要注入一些默认的全局样式,这个前面已经有所提到。后面三种其实都是对于组件样式的处理。
第二种是对组件的默认样式,可能有一些组件,历史的组件,我们也给它提供了这种能力。
第三种是一种预处理,组件样式的一个预处理,基本上都要用到StyleSheet.Create,这个和React-Native保持一致。
第四种我们对样式的一个实时处理系统。
样式处理系统的任务就是处理样式的问题,包括但不限于:
1)平台间样式的差异性,比如Border,在React-Native下,它是分散的每一个属性值进行一个独立的编写,而在Web上面它的Border是一个混合制,所以这就是平台之间的差异,CRNWEB框架就会开一个任务去对它进行修复。
2)浏览器间的差异,比如有的浏览器支持FlexBox,有的不支持,而且即使是支持FlexBox,支持的程度,版本也不一样,这些都是需要具体处理的修复任务。
3)一些共性上的问题,如单位处理,颜色处理等等。
4)一些差异性样式问题,如前缀处理,视口问题。
5)Web不支持的样式,如BoxShadow的实现等。
6、事件处理
CRNWEB中还有非常重要的一大块逻辑就是事件处理,我们专门提供了一个事件处理系统来进行处理。
我们使用了PanResponder,它提供一个对触摸响应系统的Responder的可预测的包装,和React-Native保持一致的事件处理流程,所以在事件的处理流程和兼容性方面和React-Native保持了高度一致性。
四、打包系统概述
最后CRNWEB的一大重头戏就是打包系统。
1、打包任务
CRNWEB打包系统的任务非常多,从流程上看,大概分为以下几个阶段:
1)Prepare阶段,需要对它进行入口检查,版本检查,环境检查以及第三方的依赖检查等等,各种的预先准备条件都在在这个阶段进行处理。
2)进入到Webpack的打包构建流程,我们编写了很多Webpack的插件,对它打包进行各种处理和优化。
3)构建过程当中会去调到Babel,利用Babel对原代码进行编译,对语法进行处理,对于代码的同步、异步转换这样一些比较核心的内容,都是通过编写的Babel插件对原代码进行处理实现的。
4)进入到Create阶段,因为有的Bu需要生成JAVA工程,有的需要.Net的工程,还有的只需要一个Static静态工程,在这个阶段需要对它进行一个工程的一个创建。
5)对这个工程进行启动,我们提供了开发版和生产版,他们的侧重有所不同。像开发版的话,它的主要诉求是打包编译的速度要快,这样才可以提高效率。而对于生产版它最核心的需求就是,要使你的size最小化,使你的运行效果最好。我们使用了很多优化手段对它进行了处理。
6)最后涉及到的一块,需要对它的size,它的依赖进行各种各样的优化。我们实践下来发现,BU代码量是非常非常多,业务也是非常非常复杂。怎么办呢?我们对业务工程进行size的分析,依赖分析等等各种更深入到代码层面的分析和处理,从而寻找到最佳实践的解决方案。
2、一些关键优化点
随着业务蓬勃发展,页面越来越多,组件越来越大,无论对于Native还是对于Web来说,这都是无法回避的挑战,精简打包size成为重要工作,对于size这一块我们做了很多优化处理,包括但不限于:
1)对于React,进行了优化和增减,以及一些切入式的处理,只保留需要的部分。
2)使用了tree shaking技术,排除掉了很多死代码。
3)对组件进行高级别抽象,增加重用度。
4)减少组件层级,简单而有效的方案,既减少size又提升性能。
5)运用各种cache技术,提升用户体验。
另外我们使用了一些工具,能很好的将项目中的模块依赖关系呈现出来,比如说Log这个模块被哪些页面引用,首页这个页面引用了哪些具体的模块(如:FStyleSheet,Log,utils,LinearGradient.web等等),模块有多大多少个,都可以非常清晰的展示出来。这样就非常方便对代码进行优化和处理,并使数据可视化了。
我们现在项目有多大,它的主要代码组成结构是什么样的,它的每一个模块,每一个依赖,每一个组件size占比多少,都可以进行精确的数据分析。比如说最大的模块,你为什么最大,你包含了哪些业务逻辑,是不是必须的。这些数据都可以进行再一步的思考。
CRNWEB目前已经支持到了React-Native的最新版本0.54版本,React升级到16.2版本,已经有众多页面升级上线。
最后看看实际项目运行效果对比: