Blog | Phodal - A Growth Engineerhttp://www.phodal.com/blog/2018-01-22T12:12:37.108195+00:00Blog【架构拾集】:移动应用架构设计2018-01-22T12:11:29+00:002018-01-22T12:12:37.108195+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/architecture-design-series-design-a-mobile-application-architecture/> 如何 GET 架构设计的新技能
在这一个多月里,我工作在一个采用插件化的原生 Android 应用项目上。随着新技术的引入,及编写原生 Android 代码的技能不断提升,我开始思索如何去解锁移动应用新架构。对,我就是在说 Growth 5.0。
两星期前,我尝试使用了 Kotlin + React Native + Dore + WebView 搭建了一个简单的 Android 移动应用模板。为了尝试解决 Growth 3.0+ 出现的一系列问题:启动速度慢、架构复杂等等的问题。
PS:作为 Architecture 练习计划的一部分,我将采用规范一些的叙述方式来展开。
1. 业务架构
2. 技术远景
3. 方案对比
4. 架构设计方案
5. 持续集成设计
6. 测试策略
7. 架构实施
即下图:
![技术选型](http://architecture.phodal.com/images/tech-inception.jpg)
业务架构
---
> 技术是为了解决业务的问题而产生的。
脱离了业务,技术就没有了存在的前提。脱离了业务的架构不叫 “架构”,而叫刷流氓,又或者是画大饼。业务由于其本身拥有其特定的技术场景,往往是对技术决策影响最大的部分。
因此,开始之前让我们先了解一些业务,这里以 Growth 为例。
Growth 的价值定位是:**带你成为顶尖开发者**。
复杂一点的说明就是:_Growth_ **提供** _编程学习服务_ **使用** _Web开发路线_ **帮助** _新手 Web 程序员_ **解决** _Web 学习路径问题_。
让我们来看一下,更复杂一些的说明(电梯演讲):
\ | \
------------|-----------
对于 | 缺少 Web 体系经验的程序员
他们有 | 书籍、在线教程、论坛、技术问答、练习项目
我们的产品 | 编程学习软件 Growth
是一个 | 移动应用
它可以 | 涵盖Web开发的流程及技术栈,Web开发的学习路线、成长衡量等各方面。
但他不同于 | segmentfault、知乎
它的优势是 | 拥有完整的 Web 开发流程(知识图谱)、配套完整的电子书、提供练习及学习工具。
在原有的业务架构下,我们拥有 Growth、探索、社区、练习四个核心业务,以及用户中心的功能。
- Growth(首页),即带有详细介绍的 Web 应用的生命周期,能帮助开发者理解 Web 应用的构建流程。
- 探索,以辅助开发者了解 Web 应用方方面面的知识,如常用工具、练手项目、技能测验、读书路线等等。
- 练习,通过这些练习项目,来帮助开发者更好的掌握知识。
- 社区,一个简易的论坛。
- 用户中心,一些用户的收藏数据、应用相关的设置等等。
这就是业务上的主要架构,接下来让我们看看技术上的事务。
技术远景
---
> 远景,即想象中未来的远大景象。技术远景,即想象中未来的技术方面的远大景象。
在上一节中,我们介绍的是项目的业务远景。而作为一个技术人员,在一个项目里,我们也已经创建自己的技术远景。一来,我们可以创建出可持续演进的架构;二来,可以满足个人的技能需求。
以 Growth 为例,我的最基本的技术需求是:提升自身的能力。然后才是一个跨平台的技术设施——减少构建时间。
从 Growth 1.0、Growth 2.0 采用的 Ionic,到 Growth 3.0 采用的 React Native,它都优先采用新的技术来帮助自己成长,并使用了跨平台的移动应用开发框架。而这几个不同的版本里,也拥有其对应的不同技术问题
- Growth 1.0 主要是 Angular 1.x 的跳崖式升级,使之变成不可维护的系统。
- Growth 2.0 则是 Angular 2.x 那庞大的构建体积,带来了启动时间慢的问题。
- Growth 3.0 则是,React Native 生成的 ``index.android.bundle`` 文件有 3.1M,这个体积相当的大,以至于即使在高通的骁龙 835 处理器上,也需要 4~5 秒的打开时间。
而在 Growth 5.0 的设计构架里,考虑到 React Native 本身的不加密,其对于应用来说,存在一些安全的风险。我决定引入 Native 的计划,来从架构上说明,这个系统在某种程度上也是可以加密的。
因此,对于我而言,从技术上的期望就是:
- 使用新技术带来成长
- 让应用长期可维护
- 拥有跨平台的基础设施
- 插件化方案
方案对比
---
对于普通的应用来说,其需要**从不同的方案中选择一个合适的方案**。其选择的核心,取决于项目所依赖的关键点。如在 Growth 有两个关键点:代码复用程度、应用性能。
这个时候就需要罗列出不同系统的优缺点,并从中选择合适自己的一部分。
如下数据(**纯属个人使用体验总结,没有任何的数据基础**):
\ | 原生 | React Native | NativeScript | 混合应用
-----------|----------|--------------|---------------|------------
开发效率 | 2 | 4 | 3 | 5
跨平台程度 | 0 | 3 | 3 | 4
性能 | 5 | 4 | 4 | 2
成熟度 | 5 | 4 | 3 | 5
安全性 | 5 | 3 | 4 | 2
总计 | 17 | 18 | 17 | 18
PS:NativeScript 在安全性上比 React Native 好一点点的原因是,使用 NativeScript 的人相对少一点,所以技术成本就高一些。毕竟,macOS 和 Android 手机上也是有病毒的。
考虑到我打算结合不同的几个框架,所以这里就不需要选择了。
技术方案
---
在定下了基本的技术方案后,就差不多是时候进行架构设计了。
现今的很多应用里,也是采用多种技术栈结合的架构,如淘宝的 Android 原生 + Weex + WebView,或者支付宝(不确定有没有 Weex)。但是,可以肯定的是几乎每个大型应用,都会在应用里嵌入 WebView。WebView 毕竟是可以轻松地进行远程动态更新,也需要原生代码那样的后台更新策略。
在 Growth 3.0 里,我们选择了使用 React Native + WebView 的构建方式,其原因主要是 WebView 的生态圈比较成熟,有相当多的功能已经用 WebView 实现了。而在新版本的设计中,则系统变得稍微复杂一些:
![架构图 2](http://architecture.phodal.com/images/arche.jpg)
从设计上来说,它拥有更好的扩展性,毕竟在安全上也更容易操作。然而,从技术栈上来说,它变得更加复杂。
### Growth 技术方案
**原生部分**
系统在底层将采用原生的代码作为基础框架,而不再是 React Native 作为基础。再考虑到项目上正在实施的 Android 插件化方案,我打算在 Android 的 Native 部分使用 RePlugin 来引入一些更灵活的地特性。因此,从架构上来说,能满足个人的成长需求了。
毕竟原生 Android 有些架构还是相当有意思的:
![原生 Android 架构](http://architecture.phodal.com/images/android-architecture.jpg)
**React Native**
React Native 从代码上的变化比较大,架构设计上从代码上切分出几个不同的页面。它**可能可以**在某种程度上 Bundle 文件过大,带来的加载速度慢的问题。因而,在某种程度上,可能带来更快的启动速度。
**WebView**
总体上来说,WebView 变化不会太大。除了,可能从 React Native 的 WebView 迁移到原生部分的 WebView 之外。
持续集成设计
---
之前我们提到持续集成的时候,多数是指持续集成的实施。而今天我们谈到持续集成的时候,则是在讨论如何去设计。
### 代码策略
首先,就是代码策略,即代码管理策略。代码管理,指的就是决定采用哪种 git 工作流。会影响到代码管理的因素有:
- 上线功能。如某次发布要上线哪些功能,肯定会影响到正常的开发流程。
- 代码集成。当我们采用模块化、插件化来设计系统架构时,就需要将几个不同的的项目集成到一起。
- 代码合并。在有的项目里,人们会使用 PR 来提交代码,有的则是直接在 master 上提交,也有的采用 feature branch。
- 分支策略。什么时候,决定拉出新的分支?
- 修复 bug。当我们拉到一条新的分支时,我们要怎么去应对一个 bug 的出现呢?
对于 Growth 而言,则仍然是 master 分支,采用多个 GitHub 项目的集成方式。
### 工具箱
作为一个有经验的程序员,我们应该在设计的初期考虑到我们所需要的工具:
- 基础设施,诸如 React Native 需要的 Node.js、Android 及 Java 需要的构建工具 Gradle
- 文档工具,诸如架构决策记录工具 ADR,
- 开发工具,编写 Android 应用需要的 Android Studio、编写 React Native 的 Intellij IDEA
- 依赖库,这些工具是我们
- 持续集成,在持续集成上可以采用 Travis CI
- 应用发布,APP 仍然使用 GitHub 和 pgyer.com 来进行测试版发布。至于后台 API,是否从 GitHub、Coding 上迁出,仍然有待商榷。
这些也仍是我们在设计架构的过程中,需要考虑的一些因素。
测试策略
---
一般情况下,我们要会采用测试金字塔:
![测试金字塔](http://architecture.phodal.com/images/test-pyramid.png)
在这里,引用《全栈应用开发:精益实践》对于测试金字塔的分析:
> 从图中我们可以发现:单元测试应该要是最多的,也是最底层的。其次才是服务测试,最后才是 UI 测试。大量的单元测试可以保证我们的基础函数是正常、正确工作的。而服务测试则是一门很有学问的测试,不仅仅只在测试我们自己提供的服务,也会测试我们依赖第三方提供的服务。在测试第三方提供的服务时,这就会变成一件有意思的事了。除此还有对功能和 UI 的测试,写这些测试可以减轻测试人员的工作量——毕竟这些工作量转向了开发人员来完成。
而如果是架构混搭的应用来说,其的测试成本相当的大。因为要测试的部分是 3 + 1,即:
- 原生部分,采用原先代码的测试策略,如 JUnit
- React Native 部分,继续之前的 ``react-test-renderer`` 测试渲染、``jest`` 和 ``chai`` 测试业务逻辑
- WebView 部分,采用框架本身推荐的框架
- 组合部分,对于这部分来说,UI 上的测试会更加可靠,如在 Growth 3.0 中采用的 ``appium`` 就是一个不错的选择。
架构实施
---
最后,让我们来看看我在两个星期前,搭的一个架子,用于作技术验证功能。一共由三部分组件:
- 使用 Kotlin 编写的原生代码
- 使用 React Native 编写的 Fragment
- 使用 Ionic 编写的 WebView 应用
接下来看两个简单的代码示例:
### 创建 React Native 的 Fragement
如下是一个使用 React Native 编写的 Fragement 示例,它可以直接在原生的 Activity 上使用:
```
class ArcheReactFragment : ReactFragment() {
override val mainComponentName: String
get() = "RNArche"
private var mReactRootView: ReactRootView? = null
private var mReactInstanceManager: ReactInstanceManager? = null
@Nullable
override fun onCreateView(inflater: LayoutInflater?, group: ViewGroup?, savedInstanceState: Bundle?): ReactRootView? {
mReactRootView = ReactRootView(activity)
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(activity.application)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index")
.addPackage(MainReactPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build()
mReactRootView!!.startReactApplication(mReactInstanceManager, "RNArche", null)
return mReactRootView
}
}
```
除了将 React Native 切分成不同的几个子模块。对于一个 React Native 应用来说,它可以**注册多个 Component**
```
AppRegistry.registerComponent('RNArche', () => App);
AppRegistry.registerComponent('RNArche2', () => App2);
```
这样一来说,可以在一个 React Native 应用里被原生部分多次调用不同的组件。
### 简单的 WebView
对于那些不需要原生组件的组件来说,可以直接由原生应用来对 WebView 处理。从逻辑上来说,这样的性能会更好一些:
```
@SuppressLint("SetJavaScriptEnabled")
@Nullable
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater?.inflate(R.layout.fragment_webview, container, false)
mWebView = view?.findViewById(R.id.webview)
mWebView!!.loadUrl("file:///android_asset/www/index.html")
val webSettings = mWebView!!.settings
webSettings.javaScriptEnabled = true
mWebView!!.webViewClient = WebViewClient()
return view
}
```
对,就是这么简单。
结论
---
So,尝试去做这样的设计吧。
![架构设计](http://architecture.phodal.com/images/tech-inception.jpg)WebView <-> React Native <-> Native 相互调用2017-08-16T14:56:00+00:002017-08-16T15:15:39.062742+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-webview-react-native-native-code-call-chain/在《[我们是如何将 Cordova 应用嵌入到 React Native 中](https://www.phodal.com/blog/cordova-webview-embeded-react-native-case-study/)》 一文中,我们简单地介绍了『React Native 重写 Cordova 插件:复杂插件的调用』步骤:
1. WebView 调用 RN 方法,并监听 React Native 返回的相应事件
2. React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
3. 原生代码执行 React Native 调用的方法,并响应事件给 React Native
4. React Native 接收到原生代码的值,执行 injectJavaScript 注入代码到 WebView 里并执行
5. 注入的 JavaScript 执行代码,并发出相应的广播
6. WebView 调用的地方,接收到广播,执行相应的方法
上面的 4 和 5 可以是:
4.React Native 接收到原生代码的值,并返回给原生代码
5.接收到相应的值,并发出相应的广播
本文则详细讨论一下这个过程。
步骤1:WebView 调用 RN 方法,并监听 React Native 返回的相应事件
---
这里,我们和《[React Native + Cordova WebView 演进:Plugin 篇](https://www.phodal.com/blog/react-native-inside-cordova-webview-with-plugin/)》中一样,仍然以 DatePicker 为例。
首先,我们需要一个广播:当 React Native 返回值时,我们就发出一个广播,这样可以解耦合代码。下面的代码则监听相应的广播:
```
$rootScope.$on('Bridge.datePicker', function(event, data) {
// 更新时间
});
```
然后便是相应的 datePicker 的调用:
```
function datePicker(options) {
function handler(event) {
event.target.removeEventListener('message', handler);
var data = JSON.parse(event.data);
$rootScope.$broadcast('Bridge.datePicker', data);
}
window.document.addEventListener('message', handler);
window.postMessage(JSON.stringify({
action: 'DATE_PICKER',
payload: payload
}));
}
```
先监听从 React Native 发过来的内容,当接收到内容将数据以广播的形式发出。然后,再通过 PostMessage 告诉 React Naitve,我们需要在调用哪个 action,并传递相应的参数。
步骤2:React Native 接收到 WebView 的调用,调用原生代码,并监听原生代码返回的相应事件
---
在 WebView 的 onMessage 方法里,我们需要处理不同的 action:
```
onMessage = (evt, webView) => {
const event = JSON.parse(evt.nativeEvent.data);
const action = event.action;
const payload = event.payload
...
switch (action) {
case 'DATE_PICKER': {
return DatePickerHandler.showDatePicker(payload, webView);
}
}
...
}
```
然后根据传过来的 action 类型,调用相应的方法,如这里是 DatePickerHandler.showDatePicker,其 Android 部分代码如下所示:
```
const { action, year, month, day } = await DatePickerAndroid.open(options);
if (action !== DatePickerAndroid.dismissedAction) {
webView.postMessage(JSON.stringify({
type: 'DATE_PICKER',
success: true,
date,
}));
}
```
iOS 则有一些不同,iOS 没有非标签的组件,需要自己写。而且,由于 iOS 的 DatePicker 是异步的,因此我们需要通过事件的方式进行。如下是写完插件后的调用示例:
```
const RNNoTagDatepicker = NativeModules.RNNoTagDatepicker;
const DatePickerEvent = new NativeEventEmitter(NativeModules.RNNoTagDatepicker);
...
const showPicker = async (options) => {
RNNoTagDatepicker.show(options);
};
```
步骤3:原生代码执行 React Native 调用的方法,并响应事件给 React Native
---
如上,由于 iOS 的日期插件是异步的,并且它只能通过方法,而非组件的方式来唤醒 UI,故而需要 sendEventWithName 来返回值
```
RCT_EXPORT_METHOD(show:(NSDictionary *) options) {
dispatch_async(dispatch_get_main_queue(), ^{
}
}
#pragma mark - Actions
- (IBAction)doneAction:(id)sender {
dispatch_async(dispatch_get_main_queue(), ^{
NSTimeInterval seconds = [self.datePicker.date timeIntervalSince1970];
[self sendEventWithName:@"DATEPICKER_NATIVE_INVOKE" body: @{@"status": @"success", @"value": [NSString stringWithFormat:@"%f", seconds]}];
[self hide];
});
}
```
步骤4:React Native 接收到原生代码的值,并返回给原生代码
---
在这个例子里,由于在 WebView 以广播的方式解绑,因此可以直接返回值:
```
DatePickerEvent.addListener('DATEPICKER_NATIVE_INVOKE', (evt: Event) => {
...
webView.postMessage(JSON.stringify({
type: 'DATE_PICKER',
success: true,
date
}));
...
});
```
如果是要不断地发送数据,则需要在 RN 代码里执行:
```
let js = 'var event = new CustomEvent("' + action + '", {detail: ' + JSON.stringify(detail) + '});';
js += 'window.document.dispatchEvent(event);';
webView.injectJavaScript(js);
```
步骤5:接收到相应的值,并发出相应的广播
---
紧接着,就回到步骤一中的 handler:
```
function handler(event) {
event.target.removeEventListener('message', handler);
var data = JSON.parse(event.data);
$rootScope.$broadcast('Bridge.datePicker', data);
}
```
步骤6:6. WebView 调用的地方,接收到广播,执行相应的方法
---
最后,我们终于到了:
```
$rootScope.$on('Bridge.datePicker', function(event, data) {
// 更新时间
});
```
如此复杂的过程,也是。。。
我们是如何将 Cordova 应用嵌入到 React Native 中2017-08-16T14:33:11+00:002017-08-16T15:16:30.635545+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/cordova-webview-embeded-react-native-case-study/> 重写一个应用是一件简单的事,可是演进一个应用则是一件复杂的工作。
过去的一年多里,我在工作上的主要职责是:手机 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 示例](/static/media/uploads/tabbar-example.jpg)
这个时候,假设我们要去掉『探索』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](https://github.com/phodal/growth) 的时候,就发现 React Native 的 WebView 是有一些明显的坑的。即在开发环境和生产环境,我们需要处理好 WebView 的路径问题。生产环境时,Android 需要将路径放到 ``file:///android_asset/`` 目录下:
```
let source;
if (__DEV__) {
source = require(`./www/index.html`);
} else {
source = Platform.OS === 'ios' ? require(`./www/index.html`) : { uri: `file:///android_asset/www/index.html` };
}
```
实际上,那一点也适用于 iOS,在 iOS 打包的时候,我们也需要将 WebView 的代码放置到相应的 ``assets`` 目录下。因此,便需要编写打包脚本:
```
rm -rf ios/assets/src/components/ui/www
mkdir -p ios/assets/src/components/ui/www
cp -a src/components/ui/www ios/assets/src/components/ui/
```
而在那之前,还有 WebView 的跨域问题。**在 Android 版里的 WebView 可以支持 allowUniversalAccessFromFileURLs**。而 iOS 则不行,要支持的方式便是通过原生代码去获取,但是这样一来调用链太长。
除此,还需要了解的是 WebView 的各种生命周期。在不同的过程中,赋予不同的业务逻辑:
```
onNavigationStateChange={this.onNavigationStateChange}
onMessage={this.onMessage}
onLoad={this.onLoad}
onLoadStart={this.onLoadStart}
```
因此,就整体上来说,在这一部分只剩下一部分小问题了。
React Native 重写 Cordova 插件:常规插件调用
---
开始之前,让我们再说说一下调用链的问题。过去我们在 Cordova 是调用原生代码,便是 WebView <-> Cordova 原生插件(PS:感兴趣读者可以阅读:[Cordova插件 / 混合应用插件开发: hello,world解析](https://www.phodal.com/blog/create-cordova-plugin-demo-hello-world/)。而现在则变成了 WebView <-> React Native <-> 原生插件,整一个链条里直接多了一个节点。在那一篇《[React Native + Cordova WebView 演进:Plugin 篇](https://www.phodal.com/blog/react-native-inside-cordova-webview-with-plugin/)》里,我们介绍了这个过程:
由 WebView 执行 postMessage,并监听相应的事件:
```
window.postMessage(JSON.stringify({
command: 'DATE_PICKER',
payload: options
}));
window.document.addEventListener('message', function (e) {
var data = JSON.parse(e.data);
if(data.command && data.command === 'DATE_PICKER' && data.success) {
$rootScope.$broadcast('Bridge.datePicker', data)
}
});
```
再由 React Native 去调用原生组件,并返回相应的值:
```
const { command, year, month, day } = await DatePickerAndroid.open(options);
const date = new Date(year, month, day);
webView.postMessage(JSON.stringify({
command: 'DATE_PICKER',
success: true,
date,
}));
```
而在复杂的系统里,则需要一些更复杂的手段。
React Native 重写 Cordova 插件:复杂插件调用
---
在那篇《[Ionic 与 Cordova 插件编写:基于事件与广播的机制](https://www.phodal.com/blog/use-cordova-ionic-build-chat-app-sendjavascript-to-ui-thread/)》中,我介绍了一下项目里,所需要的一个由 Native 发出事件的例子。这时,需要在原生代码里,发出相应的事件:
```
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
String js = "var event = new CustomEvent('someAction'});window.document.dispatchEvent(event);";
webView.loadUrl("javascript:" + js);
}
});
```
在 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 编译](https://www.phodal.com/blog/react-native-repackage-cordova-plugin-notes/) 》及《[WebView <-> React Native <-> Native 相互调用](https://www.phodal.com/blog/cordova-webview-react-native-native-code-call-chain/)》)
上面的代码变成了 React Native 里的:
```
let js = 'var event = new CustomEvent("' + action + '", {detail: ' + JSON.stringify(detail) + '});';
js += 'window.document.dispatchEvent(event);';
webView.injectJavaScript(js);
```
这真是一个相当复杂的过程,特别是我们的调试的时候,需要:
- 使用 XCode/Android Studio 打断点,查看相应的日志
- 使用 React Native Remote Debug 打下相应的日志
- 使用 Safari/Chrome 查看 WebView 的日志
- 使用 Charles 抓包,查看调用情况
React Native 跳转 WebView
---
由于框架设计的原因,从 WebView 里跳转到 React Native,已经不是什么问题。
```
window.postMessage(JSON.stringify({
action: 'BLABLA'
}));
```
而从 React Native 返回到 WebView 也不算是什么问题。只需要按下返回的时候,发出相应的事件:
```
window.postMessage(JSON.stringify({
action: 'GO_BACK'
}));
```
然后在 React Native 里调用相应的代码即可:
```
BackHandler.handleRNBack = () => {
Actions.pop();
};
```
处理 Tabbar
---
在上节里,我们提到了 Tabbar 的问题,而由于第三方封装的 TabBar 都会绑定 View,所以只能自己去实现。以下是一个简单的 Tabbar 示例:
```
<view 0="" 1,="" 49}}="" :="" ?="" isloadingvisible="" paddingbottom:="" style="{{flex:">
<webview ...=""></webview>
</view>
<view>
<tabbar>
<tabbar.item ...="" =="" onpress="{()"> {
// 页面跳转
}}
title='首页'>
</tabbar.item>
</tabbar>
</view>
```
只需要在相应的 onPress 方法里,绑定对应的 WebView 的路由页面处理即可。
React Native + Cordova WebView 演进:Plugin 篇2017-05-24T04:30:42+00:002017-05-24T04:58:55.645916+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-inside-cordova-webview-with-plugin/最近,项目上正在打算使用 React Native 来重写/重构/演讲原来的应用。由于早先使用 Cordova + Ionic 的时候,项目的业务代码很长一段时间里,主要是由我一个编写的。与此同时,也不会分配充足的人力,用于重写现有的业务逻辑。
因此,作为一个咨询师,我提供了几个不同的重构方案,并建议客户使用 React Native + WebView 的形式来进行演进。即在 React Native 里使用 WebView 来嵌入原有的业务逻辑,新的业务逻辑则采用 React Native 进行。考虑到未来的一段时间内, 业务代码将会继续采用 WebView 编写,而技术代码则用 React Native 编写,这是比较理想的方案。
而作为演进的其中一个难点是:重写原有的 Cordova 插件。我们使用到了数量众多的 Cordova 插件,如 Toast、日期控件等等。这个时候,就需要借助于 React Native WebView 的通信来做这件事。
React Native WebView 通信
---
早期的 React Native WebView 并不能直接与 WebView 通信,而自 React Native 0.37 版本后,则提供了:
- onMessage
- postMessage
两个方法来与 WebView 进行交互,如下是一个简单的 DEMO:
```
...
class xxWebView extends React.Component {
webview: WebView
handleMessage = (evt: any) => {
// doSomething()
}
render() {
return (
<webview =="" ref="{webview"> this.webview = webview}
onMessage={this.handleMessage}
/>
)
}
}
```
在我们的 WebView 里,只需要执行下:
```
window.postMessage({ plugin: 'TOAST' })
```
就可以向插件发送信息。因此,对于完成我们的插件来说,只需要做到下面的步骤:
- 当需要调用原生插件的时候,在 WebView 里调用 window.postMessage 来传递,相应的**插件名 + 插件的参数**
- React Native 通过 onMessage 来处理对应的类型,并调用对应的插件
- 当需要返回结果时,通过 webview.postMessage 来传递参数,并带上相应的**插件名 + 返回结果**
- 在 WebView 端 ,如果想获取返回的结果,则需要 window.document.addEventListener 来监听 message 事件
- 最后,再根据返回的值来做相应的处理。
接着,让我们来看一个简单的日期控件的 DEMO。
Cordova WebView 调用 React Native 日期控件
---
### WebView
重写这段逻辑前,先让我们来看看原有的逻辑代码:
```
function onSuccess(date) {
// 更新时间
}
datePicker.show(options, onSuccess, null);
```
我们通过 options 来传递参数,而 onSuccess 则是成功的回调。不过,由于已经没有 Cordova 的机制,这里的 success 和 error 的回调就没有啥用了。
因此,在 WebView 上这段逻辑就变成了:
```
$rootScope.$on('Bridge.datePicker', function(event, data) {
// 更新时间
});
BridgeHelper.datePicker(options);
//BridgeHelper.js 中的相关代码
window.postMessage(JSON.stringify({
command: 'DATE_PICKER',
payload: options
}));
```
同时,我们有一个全局的监听函数,在这里面判断是否有对应的 command 类型。如果是我们需要的 DATE_PICKER,并且是成功地修改值,便会发出这样的一个广播,上面的代码就可以成功地更新时间。
```
window.document.addEventListener('message', function (e) {
var data = JSON.parse(e.data);
if(data.command && data.command === 'DATE_PICKER' && data.success) {
$rootScope.$broadcast('Bridge.datePicker', data)
}
});
```
这个原理与之前提到的 [Ionic 与 Cordova 插件编写:基于事件与广播的机制](https://www.phodal.com/blog/use-cordova-ionic-build-chat-app-sendjavascript-to-ui-thread/) 是相似的,通过全局事件来控制逻辑。
### React Native
在 React Native 端,则也是对相应的 handleMessage 进行处理,然后调用相应的组件来处理,如下是调用系统的控件:
```
DatePickerHandler.showDatePicker = (payload, webView) => {
const showPicker = async (options, webView) => {
try {
const { command, year, month, day } = await DatePickerAndroid.open(options);
if (command === DatePickerAndroid.dismissedcommand) {
//
} else {
const date = new Date(year, month, day);
webView.postMessage(JSON.stringify({
command: 'DATE_PICKER',
success: true,
date,
}));
}
} catch ({ code, message }) {
console.warn('Cannot open date picker', message);
}
};
showPicker(options, webView);
};
```
通过这样复杂的工作,我们就可以完成大部分的工作。</webview>