重写一个应用是一件简单的事,可是演进一个应用则是一件复杂的工作。
过去的一年多里,我在工作上的主要职责是:手机 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/
目录下:
let source;
实际上,那一点也适用于 iOS,在 iOS 打包的时候,我们也需要将 WebView 的代码放置到相应的 assets
目录下。因此,便需要编写打包脚本:
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 的情况下,过程则变成这样的:
- WebView 调用方法,并监听 React Native 返回的相应事件
- React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
- 原生代码执行 React Native 调用的方法,并响应事件给 React Native
- React Native 接收到原生代码的值,执行 injectJavaScript 注入代码到 WebView 里并执行
- 注入的 JavaScript 执行代码,并发出相应的广播
- 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 的路由页面处理即可。