美柚其实在具有热更新功能的混合应用技术运用中算比较晚了,经过一系列的调研准备,于今年的3月份,开始在美柚APP上使用RN框架(去年9月,有在杭州电商里面使用了几个RN的页面,但只是粗略应用)。因为大家都没有什么经验,基本都是摸着石头过河进行尝试。
这篇文章就是用来记录基于美柚APP的RN框架从0到1的一个开发和设计的过程。

台前幕后

痛点:

在做这个事情前,我们先要明确我们要做这个事情的原因和目的,我们为什么要这么做。

  1. 开发维护成本太高:要开发维护两套代码,而且原生在开发用户界面上的成本非常高,我之前写android页面你写个样式的编写维护好几个xml文件,不同的列表数据还得写不同类型的适配器。开发维护成本比web的页面开发高很多。
  2. 繁复的版本管理:新老版本都要做兼容,就会出现一系列的版本。
  3. 频繁的变更需求:在ios下,新版本发布需要通过app store的提交审核,会有个时间的延迟。不具备热更新的功能。
  4. 性能体验的保证:在H5下的性能和体验得不到保证。
  5. App size越来越大:目前美柚App已经快97MB,柚宝宝已经107MB。

定位

要清楚RN的定位,RN是一种解决方法,他是为了解决以上那些痛点而存在的。也就是在对比Native 和 H5, 要清楚RN 的优势在哪里。我们也要清楚认识到,在移动端App上,native 和 H5都是有其存在的价值的,RN可以替代他们,但是目前是不可能完全取代的。

横向对比

知道为什么做、知道哪些痛点和要做这件事件的目的。现在就要知道怎么去做,用什么方式和方法。在市面上,流行的框架有 ionic nativescript weex reactnative四种比较主流的框架。这四个框架都是可以解决上面提到的那些痛点。

对于这些技术,我们提前做了下调研。

Ionic

Ionic is the beautiful, free and open source mobile SDK for developing native and progressive web apps with ease.

就算你不懂任何移动端技术,也是可以hold的住的框架。在之前,我有落地过一个INOIC 1的项目,开发体验非常好,上手速度也非常快。INONIC1是基于ng1, IONIC2 是基于ng2。纯前端的开发模式,对前端开发人员的门槛很低,学习成本也很低。他是基于webview进行开发的APP,使用cordova API调用原生的功能。而且极光JPush之类的第三方插件都对IONIC支持,生态链也非常成熟。

但是唯一的缺陷,也是致命的缺陷就是性能和体验的问题。目前出的IONIC2听说性能有很大改进,但是肯定没办法跟原生的相比。IONIC比较适合于一下小型的APP,和能力有限并且要快速迭代的初创公司。

网上对ionic的评价:

1
2
3
4
5
6
7
8
优势:
ios 和 android 基本上可以共用代码,纯web思维,开发速度快,简单方便,一次编码,到处运行,如果熟悉web开发,则开发难度较低。
文档很全,系统级支持封装较好,所有UI组件都是有html模拟,可以统一使用。
可实现在线更新 允许加载动态加载web js
前端的程序员就可以独立开发
文档多,开发者多,视频教程多 容易学习 遇到问题容易解决 技术成熟
劣势:
占用内存高一些(不过手机内存都大了不影响),不适合做游戏类型app, web技术无法解决一切问题,对于比较耗性能的地方无法利用native的思维实现优势互补,如高体验的交互,动画等。

Nativescript

cross-platform mobile applications

出现的很早,比RN还出现的更早一点。和RN与weex一样,都是最终转为native的解决方案,出自于一个保加利亞的軟件公司公司Telerik,比RN还出现的略早点,版本已经到了3.1。也是native、也是有各种组件、支持各种css style、也可以chrome调试开发,也支持集成进native的混合开发。很无解它的短板在什么地方,为什么用的人不多,网络上的相关资料也很少。看到的为数不多的文章写有偏向于整体的解决方案,网上更多的评论是NS更注重于用户体验功能之类的,而对于UI渲染这块是比较弱,不置可否。
大部分来自ionic的开发者,因为可以不用去关注native的相关技术。原生的开发应用性能得到保障。但是相关文档比较少,大部分都得通过官网学习,NS的中文网站都还没翻译全。关于NS的文章也是很少,基本都得看外文文献NS vs RN

其实我对NS没有过多实践,所有知道都是从官网或者其他的人写的文章这样的第二手资料,没什么资格去说这个事情,上面的评论大家看看就好。

传送门NativeScript 10K 文档

WEEX:

Write once, Run anywhere!

weex 14k

原理:Weex 表面上是一个客户端技术,但实际上它串联起了从本地开发环境到云端部署和分发的整个链路。开发者首先可以在本地像撰写 web 页面一样撰写一个 app 的页面,然后编译成一段 JavaScript 代码,形成 Weex 的一个 JS bundle;在云端,开发者可以把生成的 JS bundle 部署上去,然后通过网络请求或预下发的方式传递到用户的移动应用客户端;在移动应用客户端里,WeexSDK 会准备好一个 JavaScript 引擎,并且在用户打开一个 Weex 页面时执行相应的 JS bundle,并在执行过程中产生各种命令发送到 native 端进行的界面渲染或数据存储、网络通信、调用设备功能、用户交互响应等移动应用的场景实践;同时,如果用户没有安装移动应用,他仍然可以在浏览器里打开一个相同的 web 页面,这个页面是使用相同的页面源代码,通过浏览器里的 JavaScript 引擎运行起来的。

其实最开始选型的第一选是weex,选择他原因有三吧

  1. 基于vue,我们刚做完一个vue的项目,开发体验很不错
  2. 可以三端统一,不只是 IOS和 Android,还有 H5也是可以,由于它的代码是基于VUE的,在IOS和android只是集成了sdk进行转换。
  3. 对国产的支持吧

从选用开发,到弃用,归结起来的原因有以下几点吧:

  1. 生态链欠成熟;pull request 没有响应,论坛的提问也是过两天自己回答。
  2. 文档不全;刚好也碰到weex官方支持vue,在官方文档也在升级支持vue,然后我们刚好是使用的最新支持vue开发的版本。官放说明文档很不全,得新老切换这看。
  3. 团队对阿里开源的畏惧;好像都吃过阿里开源项目的亏,阿里KPI项目的尿性也是众人皆知了。又值阿里将weex贡献给apache时间节点,心里怕怕的。
  4. 能力不够;怕自己hold不住;其实最关键还是我们能力的问题,我们没有那么多时间、精力、人力耗在这个上面。不希望这个月绩效为0分;
  5. 页面效果不理想;我们拿贴士的页面入手,但是出来的效果,在IOS上显示差异很大。

react native

优点 目的

  1. 基于 React 前端的技术栈,组件化开发思想
  2. 相对于 native,开发速度快,成本低
  3. 相对于 H5,性能更优异,体验更好
  4. 生态比较成熟,社区活跃,市场成熟,开源组件丰富充足

RN在市场中的使用情况
(图片中数据来源于携程2016.10统计的android市场的数据)

Learn once,Write anywhere!

RN 50K
做不到 Write once, Run anywhere! 对于一些特定的组件,还是要写两套代码。

落地狂奔

快速迭代

因为从weex半路转到RN,所以基本是从零开始。对很多东西都疑惑:

  1. 怎么将RN集成进美柚App
  2. RN 和native 怎么进行交互传值
  3. 页面跳转;native 怎么跳 RN页面,RN页面怎么跳RN 页面。
  4. 如何开发编译完的bundl文件给Native。

ReactNative是基于react,而我只是对react有理论知识上的了解,还没有落地过任何项目。但是经过一个月的小步快跑。完成了八个页面的改造,18个组件的抽象,还有和native协定了交互的规范。

人员配置

不管是RN 或者weex 都不是真的像他们的宣传语一样,学一次或者写一次就能在任何地方使用,也不可能只由一个程序员完成。你得会客户端的语言,不管你是写独立的RN APP或者是像我们集成native中的APP。对于大部分公司的程序员,基本是不可能写ios、android和前端三端代码,至少现在还没遇到过。比较多的是,android客服端的同学学习react或者vue, ios 客户端的同学学习前端的语言。js是个脚本语言,相对门槛还是比较小的。而前端同学如果之前没有基础积累,再去学习客户端,不管是android 或者ios,难度都是非常大的。

所以这需要一个团队来共同完成,以下是我们的人员配置:

其实在开发过程中,会碰到很多分歧,当谁也无法说服谁的时候,就得找江大了,江大做定夺。

在开发联调过程中,如果是涉及到协议、规范、结构之类的通用部分,我们会一起协商。但是对于可行性研究,前端不可能同时和IOS、android两端同时进行,我们会选择一端进行,如果通过了才会再在另一端开发。我选择的是IOS,xcode运行内存占比只有几百MB,ASD内存占用就达到2G左右。在mac上启动xcode或者模拟器也会很快。如果是开发,你用Android 真机或者ios 真机就都差不多了,因为不用设计底层开发造成的模拟器频繁代码更新。

成果展示

Android的数据统计

来自宏弘的统计数据

从以上数据来看,RN的整体速度比WebView快了1倍,启动速度会比WebView稍微慢点,原因在于RN的启动需要解析bundle文件,需要耗时。Eatable页面,WebView比RN要快,这块主要是启动时间慢导致,可能原因是bundle文件过大导致解析慢。

IOS效果对比

RN底下访问静态页面基本是秒开秒关

H5有时候点击了要一段时间才有响应,页面加载速度也会慢一点。

在长数据列表页,或者picker选择器等上,原生体验秒杀H5

流程图

在框架搭建设计中,得先确定整体的一个流程。当你对整流程有了清晰的认知,对你开发是有很大帮助的。

开发发布流程:

  1. 编译打包
    • 通过RN提供的命令,先生成bundle文件
    • 再将编译后的文件进行拆包,根据基础和业务进行拆包,各个依赖的资源文件进行拆封。
    • 压缩;混淆压缩代码。减小代码量
    • 打包:并根据不同环境不同版本进行分别打成zip包
  2. 上传CDN

    将文件托管于CDN上,更好的请求加载速度

  3. 服务端配置

    将上传CDN的地址,配置在服务器上。由相关的权限控制人员在后台服务上配置启停相关RN版本包。
    权限管控:开发 和 生产隔离,开发环境上使用的版本由相对应的人员在服务端配置。权限隔离的好处,有助于问题排查,就是责任隔离。

  4. 解析RN

    目前的RN的存在,是作为一个替代H5的方案,也就是只有在使用小工具这块中使用。native不管 H5或者 RN都是通过协议去调用唤醒的,所以会先进行一个判断,当使用RN协议并且RN页面存在,就打开RN页面。否则会跳转到H5页面。

渲染原理

RN的编译过程,会将我们页面的React.createElement 来构建应用 UI,最终在render方法返回的都是元组件(view、image、picker等)。

当我们的页面要在客户端显示,会先在页面的contrller层(android底下是activity类),要new一个View对象。对应的是IOS的RCTRootView.m,android的ReactRootView.java。如果你没学过IOS 和android不要紧,可以理解为是个dom节点。一个页面可以由一个VIEW构成,也可以由多个VIEW构成。所以RN可以是一个页面,也可以是页面中的一小块。比如我们的广告项目在首页的插入就是用RN实现的。在新建view对象时,我们会调用一个引擎类,ios的RCTBridge.m 和android 的ReactInstanceManage.java类,去创建一个jscontext,去加载运行bundle文件。

交互

在初始化的过程中会传递参数

RN 和 Native之间的交互,都是通过Native端提供的MeiyouRNBridge。

通信协议/网络请求

协议的使用 或者网络请求,都是通过RN协议桥 MeiyouRNBridge来实现。

例如:MeiyouRNBridge.showToast(‘请求成功’);MeiyouRNBridge.setTitle(‘测试title’);

对与网络请求和协议的页面跳转,都在底层进行了相关封装。

比如下面的RN页面的跳转,你只需要在意模块的路径,名称,页面标题等相关参数,不用在意页面跳转的类型和这些传输的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 跳转RN 页面
* @param {string} source 模块路径
* @param {string} module 模块名称
* @param {string} title 页面标题
* @param {object} param 参数
* @param {*} info 信息
*/
const goRNPage = (source, moduleName, title, param, info) => {
let params = {};
params.source = source;
params.moduleName = moduleName;
params.title = title;
params.params = param;
return runAction('reactnative', params, info);
}

具体可以参考地址:
http://git.meiyou.im/iOS/iOS/wikis/work/RN.md

网络请求

使用native提供的封装的请求方法,而不使用RN提供的方法fetch,native会去请求方法做一些处理。比如拼接系统参数,比如根据所处的环境,比如测试、开发,自动切换不同的地址。

异常处理

异常有很多种,网络异常,请求异常,解析异常等等,这些都属于逻辑层面的,是我们开发人员关心的。在业务层面只展示一种结果,小柚子找不到了。

所以相关就很好处理了。

  • 一种写异常组件:Exception.js
  • 一种是调用native提供的组件:MeiyouRNBridge.setLoadingState(3);

第一种是可以自己控制范围的,更灵活,例如你有个搜索框和列表,当你按条件搜索时候,出现超时等网络异常,你只要在展示部分显示异常,而不用全页面。
第二种是更方便,但通用性差,只要调用提供的桥方法,就能全局覆盖。

白屏处理

在native调用RN页面的时候,如果没下载会下载,还会有解析和渲染的过程,所以会出现个loading动画。在早之前,native 在启动加载RN页面后,会隐藏加载动画,杭州的电商那边就是这么做的。但是这其中有个问题,页面的渲染中包含些异步操作,会有一小段时间,也就是在加载动作结束后,会有一小段白屏或者闪屏出现,这个体验就不好了。
为了解决体验问题,只能牺牲开发一些原则。native提供了关闭加载动画的方法,由我们自行控制结束动画。

工程化

高效 可维护性。对于环境进行配置,
一键命令式的完成编译,解析,拆包,拆分资源文件,压缩混淆,打包,清楚缓存等工作。

是否有必要使用云打包,我们只要合并代码进master分支,就会自动触发。

开发

集成矢量小图标Fontawsome

对于Android 和 IOS,都提供了相对应的集成了美柚基本组件库的开发demo, 可以提供给前端开发,调试,基本可以还原美柚经期App的开发场景。

android

android 的开发demo如下图,

开启调试模式功能

IOS

IOS 的开发demo如下图

代码调试

在模拟器或者真心打开上面的开发demo, android在进入页面后点击标题,ios在电脑模拟器上可以使用快捷键command + d,真机使用手机摇一摇,调出debug页面。有开启调试模式、热更新等等功能,如下图所示


然后再在浏览器输入 http://localhost:8081/debugger-ui

可以打断点跟踪,打印日志和平常web开发调试一样,如下图所示:

只是在页面跳转的时候,使用断点监听会红屏警告,只要点击dismiss就可以跳过去,但是会很影响调试。建议测试页面跳转之类的,在跳转到相关页面再打开调试模式。

永无止境

优化


1、如图所示,JS init+Require,这块时间也就是JSBundle的执行时间,这块耗时是最长的
2、bundle文件随业务代码的增加是越来越大,如果每次更新,那耗费的流量会很大。怎么去缩减bundle文件的大小。

拆包

拆包的原理:解析

拆包策略: 按照RN的基础部分 和 业务部分。
我们拿一个空的RN包进行打包就能看到,一个bundle包的大小是1.7MB, 打包以后的zip包是400kb左右。而RN这个部分的基础包,只有更新RN版本的时候才会进行更新,而RN版本的更新是不向上兼容的,我们在从0.41升级到0.43的时候,就花了很多时间精力。不仅是前端,android 和ios也是一样的。所以这个部分没有特别的需求或者问题,不会变动。而经过拆包后。
基础包208kb, 业务包237kb。解析前index.bundle.js是889kb, 解析后的base.bundle是779kb, 业务index.bundle 是284kb。

怎么使用:

一种是在客户端进行物理合并,将bundle文件拼接在一起,讲资源文件放在统一在同一个文件夹下。但是这样做,除了减少网络开销,不会有其他优化的好处,意义不大。所以放弃了。

另一种是让基础部分和业务部分在同一个作用域中运行。

数据缓存

对网络请求的方法增加了本地缓存的功能。减少网络请求,加快了网页的响应速度。

对网络请求的方法进行封装改造,只要配置个属性标志是否需要缓存。如果这个接口是需要缓存的,我会优先读取本地的存储数据,再异步去请求数据,刷新本地的存储数据。这对很多变更性不大的页面会非常大的作用。类似能不能吃的index页面。极大的提高了打开RN页面的时间。

图片优化

图片在页面中的比重占比非常大,图片的加载速度直接影响页面的加载渲染效果,所以对于图片的优化也是很重的。

针对图片的优化,目前从图片的尺寸 和 动态转换webp两方面进行优化。

关于webp:WebP 格式是 Google 于2010年发布的一种支持有损压缩和无损压缩的图片文件格式,派生自图像编码格式 VP8。

webp的优势:无损压缩后的 WebP 比 PNG 文件少了 45% 的文件大小,即使 PNG 文件经过其他压缩工具压缩后,WebP 还是可以减少 28% 的文件大小。此外,与 JPEG 相比,在质量相同的情况下,WebP 格式图像的体积要比 JPEG 格式图像小 40%,而 WebP 在压缩方面比 JPEG 格式更优越。

美柚客户端已经支持webp格式的图片显示,前端针对webp的这种图片格式进行了优化,针对美柚前端大多数图片都是来自于七牛,做了转码动态的支持。

并设置最大长宽来进一步限制图片大小,对图片裁剪为最大显示的两倍的尺寸。保证清晰度的前提下,优化了图片的大小。

使用说明:使用封装的MImge组件,可配置needWebp 和设置size大小。

1
<MImage style={styles.themeIcon} size={[68,68]} needWebp={true} source={{uri: item.icon}}></MImage>

效果:

待优化

  1. 版本控制

    如果开发人员在CDN上上传了新版本1.2,在服务器上配置的新地址。客户端的更新机制是,对比版本异同进行更新,原先版本在客户端有缓存,但是针对未及时打开app更新的情况下,如果原地址文件被覆盖更改,就会出问题。

  2. 性能优化:可优化空间,具体等上线以后的数据统计报告
  3. 自动化流程
  4. 拆包:commonjs的加载模式,所有的加载运行到内存中,AMD的异步按需加载方式
  5. 文档不全;开发规范,操作指南之类的。
  6. CDN文件被破坏风险
  7. 安全性优化,对 bundle file MD5,进行 RSA 验证
  8. 框架抽离;可以复用多个项目,单独维护。

(感谢江大,盛华,宏弘的技术支持)