1. 背景

  1. 随着移动浪潮的兴起,以及公司发展需要,我们需要重视和掌握移动APP开发技术。基于我们公司可投入到APP开发中的前端工程师远远多于原生Ios&Android工程师,以及基于app开发的低成本,高效性,以及跨平台等特性,选择hybrid app 开发的模式是目前公司最合适的app开发模式。如何在hybird app 开发的模式下,很好的增强原生工程师和前端工程师的配合,既发挥原生开发的底层开发能力和性能优势又要发挥前端开发的快捷,跨平台特性,这就变得特别重要。

2.方案选型

  1. 基于需要低成本,高性能,良好的跨平台性以及前端工程师为主原生工程师为辅的背景,提出以下几种技术方案

2.1 cordova+vue/ionic(phonegap)+angular

  1. 实现原理

    基于JavaScript和webViews,应用程序创建HTML并将其显示在webview中,类似于内嵌了一个浏览器,渲染,事件处理都是webview宿主控制处理。JavaScript语言很难直接与本地代码通信,所以他会经历一个在JavaScript领域和native领域之间进行上下文切换的“bridge”

    1. ![屏幕快照 2019-04-15 上午9.25.56.png](https://cdn.nlark.com/yuque/0/2019/png/296359/1555291573405-f3546a16-f7d7-4c5f-a8ef-67db1b008b6e.png#height=240&id=ufqAc&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-04-15%20%E4%B8%8A%E5%8D%889.25.56.png&originHeight=480&originWidth=1002&originalType=binary&ratio=1&size=74537&status=done&style=none&width=500)
  2. 方案评估

    纯web的开发方式,只是最后通过cordova/phonegap移动应用打包平台打包成apk或者ipa,并且提供一些访问原生能力的接口(相机,声音,震动等)。
    优点:基本不需要原生开发能力,前端工程师就可以胜任开发工作,成本低。
    缺点:性能相对差一些,原生工程师参与度低。
    简单展示类APP可以采用此方案,不建议作为我们的主流方案。

2.2 react-native

  1. react-native 实现原理

    RN中JavaScript通过bridge调用native的OEM widgets,实现原生调用渲染。

    1. ![屏幕快照 2019-04-15 上午9.33.13.png](https://cdn.nlark.com/yuque/0/2019/png/296359/1555292011530-0d8fcc4b-1ca2-4826-b370-f89fbde4e5bf.png#height=227&id=B4ToO&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-04-15%20%E4%B8%8A%E5%8D%889.33.13.png&originHeight=474&originWidth=1064&originalType=binary&ratio=1&size=81557&status=done&style=none&width=509)
  2. react-native层次架构(以Android为例)

    1. ![屏幕快照 2019-04-12 上午10.38.27.png](https://cdn.nlark.com/yuque/0/2019/png/296359/1555036760891-b909ac3f-380c-4c99-be15-909e902f9fe6.png#height=337&id=R3LCX&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-04-12%20%E4%B8%8A%E5%8D%8810.38.27.png&originHeight=932&originWidth=1290&originalType=binary&ratio=1&size=121241&status=done&style=none&width=466)
  • Java层:该层主要提供了Android的UI渲染器UIManager(将react-native控件映射成Android Widget)以及一些其他的功能组件(例如:图片加载处理的Fresco、网络请求Okhttp)等,在java层均封装为Module,java层核心jar包是react-native.jar,封装了众多上层的interface,如Module,Registry,bridge等。

  • c++层:主要处理Java与JavaScript的通信以及执行JavaScript代码工作,该层封装了JavaScriptCore,执行对js的解析。基于React Native运行在JavaScriptCore中,Web开发者可以尽情使用ES6,ES7的新特性,如class、箭头操作符等,完全不存在浏览器兼容的情况。Bridge桥接了java , js 通信的核心接口。JSLoader主要是将来自assets目录的或本地file加载javascriptCore,再通过JSCExectutor解析js文件。

  • Js层:该层提供了各种供开发者使用的组件以及一些工具库。

    Component:Js层通js/jsx编写的Virtual Dom来构建Component或Module,Virtual DOM是DOM在内存中的 一种轻量级表达方式,可以通过不同的渲染引擎生成不同平台下的UI。component的使用在 React 里极为重要, 因为component的存在让计算 DOM diff 更高效。
    ReactReconciler : 用于管理顶层组件或子组件的挂载、卸载、重绘。

  1. Js与Java通信机制

react-native在js端和java端互相有个映射关系,通过两端的配置表来实现,java端和js端持有同一张表,通信时靠这张表的各个条目的对应进行的。react-native实现了组件让开发者调用,就是通过js的配置表调取原生控件,java调用js也是类似的情况。所以当我们使用复杂控件时,可以自己实现java代码,添加入配置表中,即可自定义心新的映射关系。
APP开发架构规划 - 图1

  • Js调用Java

如果消息队列中有等待Java 处理的逻辑,而且 Java 超过 5ms 都没有来取走,那么 JavaScript 就会主动调用 Java 的方法,在需要调用调Java模块方法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等待Java的事件触发,把MessageQueue中的{moduleID,methodID}返回给Java,再根据模块注册表找到相应模块处理。流程如下图:
屏幕快照 2019-04-12 下午2.59.05.png

  • Java调用js

Java通过注册表调用到CatalystInstance实例,透过ReactBridge的jni,调用到Onload.cpp中的callFunction,最后通过javascriptCore,调用BatchedBridge.js,根据参数{moduleID,methodID}require相应Js模块执行。流程如下图:

  1. ![屏幕快照 2019-04-12 下午3.33.23.png](https://cdn.nlark.com/yuque/0/2019/png/296359/1555054413544-1e671104-5c3a-4431-96b4-37de6fba645c.png#height=533&id=icWIg&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-04-12%20%E4%B8%8B%E5%8D%883.33.23.png&originHeight=958&originWidth=1028&originalType=binary&ratio=1&size=93880&status=done&style=none&width=572)
  1. 打包加载

最终,JS代码会被打包成一个 bundle 文件,自动添加到 App 的资源目录下。react native 的打包脚本目录为/node_modules/react-native/local-cli,打包最后会通过 metro 模块压缩 bundle 文件。而bundle文件只会打包js代码,自然不会包含图片等静态资源,所以打包后的静态资源,其实是被拷贝到对应的平台资源文件夹中。

  1. native+react-native+webview开发方案
  • 基于以上叙述,React Native是可以完美兼容使用Objective-C、Java或是Swift编写的组件。 如果你需要针对应用的某一部分特别优化,中途换用原生代码编写也很容易。 想要应用的一部分用原生,一部分用React Native也完全没问题。
  • 原生工程师:提供通讯录、系统、设备信息读取接口,以及自定义原生组件或者扩展已有的原生组件提供给前端工程师使用,一些动画,大量数据计算等对性能要求很高的页面,可以由原生工程师单独开发。
  • 前端工程师:负责react-native项目构建,js端组件编写,业务代码编写。
  • 可以更具需求在app中灵活的开发nativepage 和 react-native page,并且实现nativaPage和react-native page之间的交互以及数据通信。
  • 对于经常改动的apge,以及缩减APP体积,我们也可以在APP中灵活使用webview,将对应的page部署到服务器上,动态拉取。可动态更新这部分的page内容。

    1. 此方案经过这两周探索,技术上我们均可以实现。
  1. 方案评估

    优点:
    react-native APP启动后实际上是去调用渲染原生组件,性能上趋近于原生,生态圈完善,目前美团APP,京东APP等大量APP均采用这种方案开发,此方案前端与原生能很好地互相补充和融合。
    待优化:对前端工程师技能要求较高,三方库依赖比较多和繁琐,并且Windows开发环境和调试比较繁琐会带来一定的不便。

2.3 Flutter

  1. 为什么选择Flutter
  • 2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flutter 1.0 Release 版本,正式发布之前就很火热,目前势头很强劲,心怀天下。市面上咸鱼APP,马蜂窝APP,小米金融APP等均已经用了flutter框架。
  • 类似于RN APP框架都是是通过 JS 桥接的方式转换为原生的控件,所以受各个系统间的差异影响非常大,虽然可以开发一套代码,但对各个平台的适配却很繁琐。flutter跨平台开发,针对 Android 与 iOS 的风格设计了两套设计语言的控件实现(Material & Cupertino)。这样不但能够节约人力成本,而且在用户体验上更好的适配 App 运行的平台。
  • 重写了一套跨平台的 UI 框架,渲染引擎是依靠 Skia 图形库实现。Flutter 中的控件树直接由渲染引擎和高性能本地 ARM 代码直接绘制,不需要通过中间对象(Web 应用中的虚拟 DOM 和真实 DOM,原生 App 中的虚拟控件和平台控件)来绘制,使它有接近,某些方面甚至超越原生页面的性能,帮助我们提供更好的用户体验。
  • 同时支持 JIT 和 AOT 编译。JIT 编译方式使其在开发阶段支持热重载(HotReload),这样在开发时可以省去构建的过程,提高开发效率。而在 Release 运行阶段采用 AOT 的编译方式,使执行效率非常高,让 Release 版本发挥更好的性能。
  • Flutter 使用 Dart 语言开发
  1. Flutter 实现原理

    Flutter 实现了一套新的架构,既不使用dom webViews 也不使用OEM widgets,他提供自己的widgets。Flutter将widgets和渲染器从平台移动到应用程序中,从而使其可以自定义和扩展。平台对于flutter而言像是一个画布,在这个画布中,flutter提供的widgets可以绘画在平台画布中从而呈现在设备屏幕上,并可以访问事件和服务,flutter 采用的drat语言和本地平台iOS/Android代码之间提供一个借口,可以进行数据编码和解码,这种方式比通过JavaScript bridge 的方式快几个数量级。

    1. ![屏幕快照 2019-04-15 上午9.41.56.png](https://cdn.nlark.com/yuque/0/2019/png/296359/1555292549407-de71ed83-45a4-4925-a363-3ff94aaff9e1.png#height=238&id=xdChS&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-04-15%20%E4%B8%8A%E5%8D%889.41.56.png&originHeight=498&originWidth=1040&originalType=binary&ratio=1&size=77578&status=done&style=none&width=497)<br />ß
  2. Flutter 架构

    Flutter 架构也是采用的分层设计。从下到上依次为:Embedder(嵌入器)、Engine、Framework。
    . 屏幕快照 2019-04-14 下午10.13.30.png

  • Framework

    1. 1. Foundation 在最底层,主要定义底层工具类和方法,以提供给其他层使用。<br /> 2. Animation 是动画相关的类,可以基于此创建补间动画(Tween Animation)和物理原理动画(Physicsbased Animation),类似 Android ValueAnimator iOS Core Animation。<br /> 3. Painting 封装了 Flutter Engine 提供的绘制接口,例如绘制缩放图像、插值生成阴影、绘制盒模型边框等。<br /> 4. Gesture提供处理手势识别和交互的功能。<br /> 5. Rendering 是框架中的渲染库。控件的渲染主要包括三个阶段:布局(Layout)、绘制(Paint)、合成(Composite)。<br /> 6. Widget 控件层。所有控件的基类都是 WidgetWidget 的数据都是只读的, 不能改变。所以每次需要更新 页面时都需要重新创建一个新的控件树。每一个 Widget 会通过一个 RenderObjectElement 对应到一个渲染节点(RenderObject),可以简单理解为 Widget 中只存储了页面元素的信息,而真正负责布局、渲染的是 RenderObject。<br /> 7. 最后是Material & Cupertino,这是在 Widget 层之上框架为开发者提供的基于两套设计语言实现的UI 控件,可以帮助我们的 App 在不同平台上提供接近原生的用户体验。
  1. Flutter 和 iOS、Android 的交互

    使用平台通道(Platform Channels)在 Flutter 工程和宿主(Native 工程)之间传递消息,主要是通过 MethodChannel 进行方法的调用,如下图所示:

    1. ![屏幕快照 2019-04-14 下午11.24.58.png](https://cdn.nlark.com/yuque/0/2019/png/296359/1555255513150-f57b4280-ba20-4996-a1bf-2795dee5154f.png#height=364&id=fBc3C&name=%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202019-04-14%20%E4%B8%8B%E5%8D%8811.24.58.png&originHeight=790&originWidth=1022&originalType=binary&ratio=1&size=108654&status=done&style=none&width=471)

    以网络请求为例,我们在Dart中定义一个MethodChannel对象:

    1. import 'dart:async';
    2. import 'package:flutter/services.dart';
    3. const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network');
    4. Future<Map<String, dynamic>> post(String path, [Map<String, dynamic> form]) async {
    5. return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) {
    6. return new Map<String, dynamic>.from(result);
    7. }).catchError((_) => null);
    8. }

然后在Java端实现相同名称的MethodChannel:

  1. public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler {
  2. private static final String CHANNEL_NAME = "com.sankuai.waimai/network";
  3. @Override
  4. public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
  5. switch (methodCall.method) {
  6. case "post":
  7. RetrofitManager.performRequest(post((String) methodCall.argument("path"), (Map) methodCall.argument("body")),
  8. new DefaultSubscriber<Map>() {
  9. @Override
  10. public void onError(Throwable e) {
  11. result.error(e.getClass().getCanonicalName(), e.getMessage(), null);
  12. }
  13. @Override
  14. public void onNext(Map stringBaseResponse) {
  15. result.success(stringBaseResponse);
  16. }
  17. }, tag);
  18. break;
  19. default:
  20. result.notImplemented();
  21. break;
  22. }
  23. }
  24. }

在Flutter页面中注册后,调用post方法就可以调用对应的Java实现:

  1. loadData: (callback) async {
  2. Map<String, dynamic> data = await post("home/groups");
  3. if (data == null) {
  4. callback(false);
  5. return;
  6. }
  7. _data = AllCategoryResponse.fromJson(data);
  8. if (_data == null || _data.code != 0) {
  9. callback(false);
  10. return;
  11. }
  12. callback(true);
  13. }),
  1. 方案评估
  • Flutter 开发框架亦能很好地与Android和iOS互相兼容,实现互相通信,遇到设备底层问题可以灵活的通过Android/iOS 解决。实现了前端工程师与原生工程师融合
  • Flutter 开辟了一种全新的思路,从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多,性能优于react-native 等于原生。
  • Flutter 采用Dart语言进行开发,需要工程师花一些时间学习语言。
  • 目前公司已有一些工程师熟悉和实践过flutter的开发,并且学习热情很高。
  • 由于flutter技术比较新,没有那么多三方的模块,可能需要自己花一些时间去造轮子。
  • 总体来说,我们公司目前是有能力去用此方案开发APP的。

3. 写在最后

  1. 任何框架都会有自身的局限性和不足,不可能一个框架解决所有的问题,我们可以根据不同的项目类型,灵活的选择不同的框架,不同的框架中灵活的与Android/iOS融合,我们这两周的探索中已经解决了各框架与原生代码融合的问题,APP开发对前端工程师提出了更高的要求,最重要的是提高我们工程师的硬实力,从几个带动到几片工程师的学习和参与,这样下来我们之后应对APP开发的项目才能够更加得心应手。<br /> 为了之后更好的推动APP开发的建设,以及带动跟多的工程师参与学习APP开发的知识沉淀我司的技术积累,在前端工程师中选择有丰富APP经验的同事组成相应的小组已推广APP相关技术栈的学习实践。<br /> 基于之前的了解,此处推荐一些同事带头参与到相关技术栈APP开发的实践当中来。<br /> react-nativeTang,Ethan(之前在同程负责APP的开发,对react-native技术栈非常了解与熟悉)<br /> Zhong,Miya<br /> Zhang,leo<br /> Feng,Shawn<br /> flutterYan,pony<br /> Jiang,Wim<br /> Liu,Tiny<br /> Wang,janey<br /> Li,Finley<br /> iOS/AndroidWu,Henry Tao,Ryan