我们是如何将 Cordova 应用嵌入到 React Native 中

2018-01-29 14:03:05 浏览数 (1)

重写一个应用是一件简单的事,可是演进一个应用则是一件复杂的工作。

过去的一年多里,我在工作上的主要职责是:手机 APP 开发。日常主要是编写基于 Ionic 和 Angular 的混合应用,并想方设法地帮助客户将之与 React Native 相结合。在完成了嵌入 WebView 后,重写插件等一系列工作后,便想记录一下这个过程中遇到的坑。

平滑地演进

如我在开头所说,在有足够人力和物力的情况下,最好的方式就是在重写应用。

一来,应用在其生命周期里,经过了不同的开发人员、不同的业务变更,必然有大量的遗留代码。尽管,我已经尽量去保证 90% 左右的单元测试覆盖率,但是仍然没有 100% 的把握(甚至 90% 都没有),来保证了解每一行代码。

二来,演进过程中,必然会遇到很多技术上的挑战,有相当多的部分是别人没有遇到过的。在这期间里,我遇到了一系列的技术问题,找到一些行业内有经验的开发者,却也发现都没有遇到相似的案件。多数的问题,诸如 iOS 上的知识,只能了解一下大概,细节下来都得自己去解决。

再让我们回到 Cordova 嵌入 React Native 应用的这个话题里。在这个项目的一半时间里,业务功能都是由我一个人编写的。再加上剩下的一半时间,有两个人同时在编写应用。那么总的项目所需要的人年就是 1.5,即一个人写 1.5 年才能写完应用。而在采用 React Native 的时候,离上线就有几个月,没有三四个人,是不可能完成重写的。因此,在方案上只有结合原有 Cordova 的 WebView 方式。

而结合的方式则有两种:

  • React Native 与 Cordova 是两个不同的视图,使用时从 Cordova 跳转 React Native,再由 React Native 转回 Cordova。
  • React Native 嵌入原有的 Cordova 的 WebView

简单的介绍一下这两种方案。

React Native 与 Cordova 结合的两种方案

React Native 结合 Cordova

这种方案的主要优点在于:集成很方便,只需要集成两个 Activity 就好了,就几天的工作量。而其缺点主要有两部分:界面跳转的时候,会存在一定的等待时间,加载 React Native 导致的。从技术上来说,这个可以在后期解决,算不上是一个问题。还有一个缺点是,入口代码无法使用新的技术编写。假设下图是一个 Tabbar 的截图,它是用 WebView 编写的:

Tarbar 示例

这个时候,假设我们要去掉『探索』Tab 的内容,而改成一个新的页面。那么,我们仍然只能在旧的 WebView 上编写,或者跳转到相应的 React Native 页面上。前者导致了不好的开发体验,后者则会导致不好的用户体验。

除了此,还可以做的一件事,嵌入 Cordova 的 WebView。

React Native 嵌入 Cordova WebView

在 React Native 中嵌入 Cordova WebView 并不是一件容易的事,对于我们而言,工作量大概是一两个月。因此,其显著的缺点是:开发周期长,插件带来的风险不可控。其优点是,我们的演进变得很轻松,我们可以获得一个类似于『微信小程序』的框架。

因为 WebView 是运行在 React Native 框架之下,我们可以随意地在页面上嵌入 Native 的元素。这一点与 Cordova WebView 和 React Native 之间相互跳转,有着明显的差异。如:

  • 想添加新的 Tab,只需要自己做一个 Tabbar,然后便能做一个新的 Native 页面。
  • 原先我们用 Cordova 调用摄像头时,界面超难定制,而使用 React Native 则便得很轻松
  • 当我们在 WebView 里,可以轻松地调用任何原生组件,在体验上也不比原生应用差

因此,主要工作就变成了:重写 Cordova 的插件。实际上,大部分的 Cordova 插件重写起来,都相当的简单——因为都有相应的 React Native 插件,只需要做一些相应的数据传递即可。

接着,让我们来看看这个过程中,我们遇到的一些坑。

React Native 处理 WebView

在我使用 RN 开发 Growth 3.0 的时候,就发现 React Native 的 WebView 是有一些明显的坑的。即在开发环境和生产环境,我们需要处理好 WebView 的路径问题。生产环境时,Android 需要将路径放到 file:///android_asset/ 目录下:

代码语言:javascript复制
let source;

实际上,那一点也适用于 iOS,在 iOS 打包的时候,我们也需要将 WebView 的代码放置到相应的 assets 目录下。因此,便需要编写打包脚本:

代码语言:javascript复制
rm -rf ios/assets/src/components/ui/www

而在那之前,还有 WebView 的跨域问题。在 Android 版里的 WebView 可以支持 allowUniversalAccessFromFileURLs。而 iOS 则不行,要支持的方式便是通过原生代码去获取,但是这样一来调用链太长。

除此,还需要了解的是 WebView 的各种生命周期。在不同的过程中,赋予不同的业务逻辑:

代码语言:javascript复制
onNavigationStateChange={this.onNavigationStateChange}

因此,就整体上来说,在这一部分只剩下一部分小问题了。

React Native 重写 Cordova 插件:常规插件调用

开始之前,让我们再说说一下调用链的问题。过去我们在 Cordova 是调用原生代码,便是 WebView <-> Cordova 原生插件(PS:感兴趣读者可以阅读:Cordova插件 / 混合应用插件开发: hello,world解析》里,我们介绍了这个过程:

由 WebView 执行 postMessage,并监听相应的事件:

代码语言:javascript复制
window.postMessage(JSON.stringify({

再由 React Native 去调用原生组件,并返回相应的值:

代码语言:javascript复制
const { command, year, month, day } = await DatePickerAndroid.open(options);

而在复杂的系统里,则需要一些更复杂的手段。

React Native 重写 Cordova 插件:复杂插件调用

在那篇《Ionic 与 Cordova 插件编写:基于事件与广播的机制》中,我介绍了一下项目里,所需要的一个由 Native 发出事件的例子。这时,需要在原生代码里,发出相应的事件:

代码语言:javascript复制
cordova.getActivity().runOnUiThread(new Runnable() {

在 Cordova里,只是 WebView 监听原生代码发出的事件。而在结合 React Native 的情况下,过程则变成这样的:

  1. WebView 调用方法,并监听 React Native 返回的相应事件
  2. React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
  3. 原生代码执行 React Native 调用的方法,并响应事件给 React Native
  4. React Native 接收到原生代码的值,执行 injectJavaScript 注入代码到 WebView 里并执行
  5. 注入的 JavaScript 执行代码,并发出相应的广播
  6. WebView 调用的地方,接收到广播,执行相应的方法

(PS:详细的代码说明见:React Native 重新封装 Cordova 插件笔记:插件编写与第三方 SDK 编译 》及《WebView <-> React Native <-> Native 相互调用》)

上面的代码变成了 React Native 里的:

代码语言:javascript复制
let js = 'var event = new CustomEvent("'   action   '", {detail: '   JSON.stringify(detail)   '});';

这真是一个相当复杂的过程,特别是我们的调试的时候,需要:

  • 使用 XCode/Android Studio 打断点,查看相应的日志
  • 使用 React Native Remote Debug 打下相应的日志
  • 使用 Safari/Chrome 查看 WebView 的日志
  • 使用 Charles 抓包,查看调用情况

React Native 跳转 WebView

由于框架设计的原因,从 WebView 里跳转到 React Native,已经不是什么问题。

代码语言:javascript复制
window.postMessage(JSON.stringify({

而从 React Native 返回到 WebView 也不算是什么问题。只需要按下返回的时候,发出相应的事件:

代码语言:javascript复制
window.postMessage(JSON.stringify({

然后在 React Native 里调用相应的代码即可:

代码语言:javascript复制
BackHandler.handleRNBack = () => {

处理 Tabbar

在上节里,我们提到了 Tabbar 的问题,而由于第三方封装的 TabBar 都会绑定 View,所以只能自己去实现。以下是一个简单的 Tabbar 示例:

代码语言:javascript复制
<View style={{flex: isLoadingVisible ? 0 : 1, paddingBottom: 49}}>

只需要在相应的 onPress 方法里,绑定对应的 WebView 的路由页面处理即可。

0 人点赞