---
title: React Native+Webview应用架构趟坑记
tags: react-native webview architecture
---

用这个架构的目的是，不仅要让应用可以具备支持Android和iOS的能力，还想保留它有朝一日可以被以小代价扩展到Web端的可能性。当然，留可能性是要付钱的，做架构就是一个平衡可能性和成本的事情嘛。

另外，这个RN架子还留着日后扩展成为Super App的可能性。因此，在RN+Webview的架构下，我们把所有原生相关的功能都放在了RN（比如认证、拍照、扫描枪、推送通知、定位，等等），而把登录之后的大部分应用功能都放在了（可复用的）Web。

实践下来，发现相比一端的架构模式，这种把单个应用拆分成两个组件的方式，天然地有以下的问题：

## Navigation/Gesture

用户在通过原生后退键或手势去进行后退导航时，需要把原生的导航命令传递给位于Web的SPA执行。

当SPA上的后退操作不仅仅是简单的`navigate(-1)`时，就需要调用SPA实际的`onBack()`代码了。

## AuthN/AuthZ

项目上的认证与登录采用的是接近于OAuth的定制方案。用户在未登录时，需要先在RN一侧进行登录，由RN与认证服务器沟通。登录成功后，RN侧拿到`access_token`（AT）和`refresh_token`（RT），并把它们**单向同步**给运行在Webview中的SPA。截止目前，一切都很好。

但是到AT的刷新过程，问题就来了。通常来说，（用RT）刷新AT有两种方式：一是在API调用发现AT 401过期时，自动用RT刷新一遍AT，并继续API调用；二是在AT即将过期前，设定timer自动刷新。这两种方法对于用户来说都是无感知的。当然也可以两种办法结合使用。但是这两种方案，在RN+Webview两端都持有AT/RT的时候，就会带来额外的同步困扰：

* 如果使用方案一（调用API时刷新），那么同样的刷新逻辑必须在两个组件侧都重复一遍，并且提前刷新的一方需要同步给另外一方。此外还可能有额外的竞争条件问题。如果RN同时还作为Super App有接入多个应用的可能性，那么每个子应用都必须实现一套刷新逻辑+一套双向的token同步事件监听。这样就不能很好地将认证这个切面剥离到Super App上了——当然，涉及认证的部分可能有其他专门的方案，但是同样的问题仍然是存在的。
* 如果使用方案二（RN提前刷新），好处是刷新逻辑也由RN这个架子统一接管了，并且同步逻辑是单向的。但这会带来另一个问题：定时器（timer）在RN上本质上是不可靠的。跟Web不同，用于计时的定时器运行在独立的JS进程中，这个进程可能由于应用被放到后台、系统的省电策略等等原因，而被OS降速、挂起甚至重启。如果要利用timer来保证RN总是能获得最新的AT，则必须采用比`setInterval()`更可靠的机制来保证实时性。

出于架构上对单向同步的考虑，我们最终选择了方案二，也真的遇到了定时器不可靠的问题。最终，我们采用的定时保障机制是`setInterval()`+监听应用的前后台状态切换，手动保存定时器的执行时间，在发生前后台切换时触发检查，并在确认定时器未按时触发时手动触发一遍。这种机制无疑仍然是不完美的，因为即便应用未切换到后台、一直放在前台一段时间，定时器都仍然可能出现不准的现象，但是在AT的刷新间隔是10分钟，这种不准被控制在了一个可接受的范围内。

## Modal

当RN和Web两边都有弹窗时，怎么确保弹窗间的层叠关系——特别是Webview里的弹窗需要覆盖外头的弹窗时，也是挑战。

## Design System

同样的App，应用的设计系统和UI组件应该是相同的。但是比较遗憾，目前没找到比较好用的两边都能用的组件库。现在两边的组件样式统一是手动保证的，而不是用了统一的组件库。

## Webview通信

由于出现了两个组件（RN和Webview），它们之间不可避免需要发生通信。Webview用的是`react-native-webview`这个库，它的信息是通过`webview.postMessage`(RN->Webview)和`window.ReactNativeWebview.postMessage`(Webview->RN)来传输的，消息传输的上限是有限的（有说1M的，有说10M的）。这在一般情况都是够用的了，但是当你需要传输一些复杂数据——比如图像——时，就需要额外的设计了。

我们的场景是，有一个相册功能。用户可以选择相册并浏览，可以增删改图片。拍照和图片编辑功能，出于性能和原生体验考虑，是放在RN的，那么在拍照完，图片就需要从RN回传到Webview；在选择图片编辑时，原图就需要从Webview回传到RN。在有多张大图片的时候，两边通信都有可能触发Webview的最大传输限制。这就又需要额外的处理。

处理的办法有：在可能的时候尽量传缩略图；压缩图片大小；只传图片引用而将内容通过临时文件或者CDN加载；为Webview的信息传输手动加一层chunking；修改架构，把相册功能或者所有API调用完全挪到RN侧以避免通过Webview进行大数据量的传输。

## 调试

如果都在同一端（RN），大部分的React代码应该都在秒级就可以得到验证——有hot reload的加持。但是当你需要调试一些联动的功能（比如拍照）时，每次修改就都是分钟级的验证时间了：先把React代码`npm build`成一个bundle，再把这个bundle拷贝到Android assets目录底下，最后再重新打debug包部署到设备上。虽然这个过程有命令行和Gradle缓存节省时间，但对于我这2019款的老Mac来说，仍然需要一分钟以上的时间。还是比较打断心流的。

## 测试

联动的功能咋做测试？
