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)Android (Kotlin) 集成 React Native 和 Ionic:Kotlin + React Native + Dore + Ionic 3.x2018-01-03T14:45:21+00:002018-01-03T14:49:08.913859+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/build-hybrid-android-application-kotlin-react-natvie-dore-ionic-3/为了总结去年的移动开发经验,我使用 Kotlin + React Native + Dore + Ionic 3.x 做了一个简单的模板项目:Arche。
项目地址:[https://github.com/phodal/arche](https://github.com/phodal/arche)
Arche 是一个 Android 移动应用模板——使用原生(Kotlin) Android 集成 React Native,以及 Ionic Web 框架、基于 React Native 的混合应用框架 Dore。
其技术栈是:
- 原生 Android(Kotlin)
- React Native
- Ionic + Angular
- Dore Framework
创建原生 Android 项目
---
首先使用 Android Studio 创建一个带导航栏的项目
生成的 MainActivity 如下:
```kotlin
class MainActivity : AppCompatActivity() {
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
message.setText(R.string.title_home)
return@OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
message.setText(R.string.title_dashboard)
return@OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
message.setText(R.string.title_notifications)
return@OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
}
```
在这里,我使用的是 Fragment 来切换页面,逻辑如下:
```
private fun switchFragment(lastIndex: Int, index: Int) {
val transaction = supportFragmentManager.beginTransaction()
transaction.hide(fragments!![lastIndex])
if (!fragments!![index].isAdded) {
transaction.add(R.id.container, fragments!![index])
}
transaction.show(fragments!![index]).commitAllowingStateLoss()
}
private fun initFragments() {
homeFragment = HomeFragment()
archeReactFragment = ArcheReactFragment()
archeWebViewFragment = ArcheWebViewFragment()
fragments = arrayOf(homeFragment!!, archeReactFragment!!, archeWebViewFragment!!)
lastShowFragment = 0
supportFragmentManager
.beginTransaction()
.add(R.id.container, homeFragment)
.show(homeFragment)
.commit()
}
```
对应的,``mOnNavigationItemSelectedListener`` 就变成了:
```
private val mOnNavigationItemSelectedListener = OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
if (lastShowFragment != 0) {
switchFragment(lastShowFragment, 0)
lastShowFragment = 0
}
return@OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
if (lastShowFragment != 1) {
switchFragment(lastShowFragment, 1)
lastShowFragment = 1;
}
return@OnNavigationItemSelectedListener true
}
R.id.navigation_webview -> {
if (lastShowFragment != 2) {
switchFragment(lastShowFragment, 2)
lastShowFragment = 2
}
return@OnNavigationItemSelectedListener true
}
}
false
}
```
其布局是:
```
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.constraintlayout android:layout_height="match_parent" android:layout_width="match_parent" tools:context="arche.phodal.com.arche.MainActivity" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools">
<framelayout android:adjustviewbounds="true" android:id="@+id/container" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical">
</framelayout>
<android.support.design.widget.bottomnavigationview android:background="?android:attr/windowBackground" android:id="@+id/navigation" android:layout_height="wrap_content" android:layout_marginend="0dp" android:layout_marginstart="0dp" android:layout_width="0dp" app:layout_constraintbottom_tobottomof="parent" app:layout_constraintleft_toleftof="parent" app:layout_constraintright_torightof="parent" app:menu="@menu/navigation"></android.support.design.widget.bottomnavigationview>
</android.support.constraint.constraintlayout>
```
React Native 部份
---
### 创建 React Native 应用
```
npm install -g create-react-native-app
create-react-native-app RNArche
cd RNArche
npm start
```
### 生成项目
Inject
```
npm run inject
```
### 添加权限
添加 Overlay 的权限处理:
```
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + packageName))
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE)
}
}
}
@SuppressLint("ShowToast")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "Lost Permissions", Toast.LENGTH_SHORT)
}
}
}
}
```
以及 AndroidManifest.xml 中的:
```
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"></uses-permission>
```
### 创建 ReactApplication
我的是 ArcheApplication
```
class ArcheApplication : Application(), ReactApplication {
private val mReactNativeHost = object : ReactNativeHost(this) {
override fun getUseDeveloperSupport(): Boolean {
return true
}
public override fun getPackages(): List<reactpackage> {
return Arrays.asList<reactpackage>(
MainReactPackage()
)
}
}
override fun getReactNativeHost(): ReactNativeHost {
return mReactNativeHost
}
}
```
### 创建基础的 Fragment
```
abstract class ReactFragment : Fragment() {
private var mReactRootView: ReactRootView? = null
private var mReactInstanceManager: ReactInstanceManager? = null
abstract val mainComponentName: String
override fun onAttach(context: Context?) {
super.onAttach(context)
mReactRootView = ReactRootView(context)
mReactInstanceManager = (activity.application as ArcheApplication)
.reactNativeHost
.reactInstanceManager
}
override fun onCreateView(inflater: LayoutInflater?, group: ViewGroup?, savedInstanceState: Bundle?): ReactRootView? {
super.onCreate(savedInstanceState)
return mReactRootView
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mReactRootView!!.startReactApplication(
mReactInstanceManager,
mainComponentName,
null
)
}
}
```
### 更新 MainActivity
然后在我们的 MainActivity 中实现对应的逻辑:
```
class MainActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler {
...
private var mReactInstanceManager: ReactInstanceManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
mReactInstanceManager = (application as ArcheApplication).reactNativeHost.reactInstanceManager
initFragments()
if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + packageName))
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE)
}
}
}
override fun onPause() {
super.onPause()
if (mReactInstanceManager != null) {
mReactInstanceManager!!.onHostPause(this)
}
}
override fun onResume() {
super.onResume()
if (mReactInstanceManager != null) {
mReactInstanceManager!!.onHostResume(this, this)
}
}
override fun invokeDefaultOnBackPressed() {
super.onBackPressed()
}
}
```
### 创建我们的 Fragment
```
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
}
}
```
这里的 ``"RNArche"`` 需要和 React Native 中注册的 Component 保持一致:
```
AppRegistry.registerComponent('RNArche', () => App);
```
在这里,也可以注册多个 Component
```
AppRegistry.registerComponent('RNArche', () => App);
AppRegistry.registerComponent('RNArche2', () => App2);
```
当然,也可以创建一个自己的 Activity 来做对应的事情。
WebView
---
好了,现在,让我们来创建我们的 WebView 应用:
```
npm install -g cordova ionic
ionic start archeWebview sidemenu
```
然后下面的命令,来生成最后的 www 文件:
```
ionic build --prod
```
将其复制到 ``assets`` 目录下。
对应的 Android 代码如下:
```
@SuppressLint("SetJavaScriptEnabled")
@Nullable
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater?.inflate(R.layout.fragment_webview, container, false)
avi = view?.findViewById(R.id.avi)
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
}
```
然后,我也顺手添加了一个 Loading 效果:
```
private fun setLoadingProgress() {
mWebView!!.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
avi!!.show()
super.onPageStarted(view, url, favicon)
}
override fun onPageFinished(view: WebView?, url: String?) {
avi!!.hide()
super.onPageFinished(view, url)
}
}
}
```
对应的 XML 如下:
```
<?xml version="1.0" encoding="utf-8"?>
<linearlayout android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<linearlayout android:id="@+id/information_detail" android:layout_gravity="center_horizontal" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical">
<com.wang.avi.avloadingindicatorview android:id="@+id/avi" android:layout_gravity="center_horizontal" android:layout_height="match_parent" android:layout_width="match_parent" android:visibility="visible" app:indicatorcolor="#000" app:indicatorname="BallClipRotateMultipleIndicator" style="@style/AVLoadingIndicatorView"></com.wang.avi.avloadingindicatorview>
<webview android:id="@+id/webview" android:layout_height="match_parent" android:layout_width="match_parent" android:scrollbars="none"></webview>
</linearlayout>
</linearlayout>
```
Q & A
---
### 问题libgnustl_shared.so" is 32-bit instead of 64-bit
在 build.gradle 中添加 ndk 配置:
```
android {
compileSdkVersion 26
defaultConfig {
applicationId "arche.phodal.com.arche"
minSdkVersion 16
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary= true
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
```
### Bundle files
```
cp ./RNArche/android/app/build/intermediates/assets/release/index.android.bundle ./app/src/main/assets/index.android.bundle
```
问题:
```
java.lang.ClassCastException: android.app.Application cannot be cast to arche.phodal.com.arche.ArcheApplication
```
在 AndroidManifest.xml 添加对应的 application
```
<application android:allowbackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:name=".ArcheApplication" android:roundicon="@mipmap/ic_launcher_round" android:supportsrtl="true" android:theme="@style/AppTheme">
```
</application></reactpackage></reactpackage>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 插件笔记:插件编写与第三方 SDK 编译2017-08-16T11:22:05+00:002017-08-16T11:22:01.873253+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-repackage-cordova-plugin-notes/
最近刚完成项目上的几个 React Native 插件的编写,便想记录这其中遇到的坑——主要是封装第三方 SDK 的问题。记录的过程中,顺便也就写一下相关的插件编写了。因此,本文分成三部分:
- React Native 插件封装
- Native 主动向 React Native 发送数据
- React Native 编译问题
React Native 插件封装
---
先让我们从 React Native 端的 JS 封装代码看起。假设我们的 SDK 名字是 SDK,那么对应在 React Native 上的 sdk.js 代码如下所示:
```
import { NativeModules } from 'react-native';
module.exports = NativeModules.SDK;
```
实际上,在这里,我们应该列出所有的方法。但是,由于时间限制原因,就没有在这一一列出了。
### React Native 插件 Android 示例
对应的,我们在 Android 端,只需要在方法开发之前添加 ``@ReactMethod`` 注解,再调用 promise 方法即可:
```
@ReactMethod
public void isLogined(Promise promise) {
try {
promise.resolve(SDK.app().is_logined());
} catch (Exception e) {
promise.reject(e);
}
}
```
**React Native Android 插件带参数示例**
```
@ReactMethod
public void login(int userid, String signature, Promise promise) {
}
```
而在 iOS 上则稍微麻烦一些。
### React Native 插件 iOS 示例
RN 对于 iOS 和 Android 的封装方式,到底还是因为两个系统的不同,而有所差异。如下是头文件 ``sdk.h`` 的代码:
```
#import "RCTBridgeModule.h"
@interface SDK : NSObject <rctbridgemodule>
@end
```
相应的代码文件 ``sdk.m`` 则如下所示:
```
@implementation SDK
RCT_EXPORT_MODULE();
```
```objc
RCT_EXPORT_METHOD(isLogined:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
BOOL isLogin = [[SDKHelper sharedHelper] isLogin];
resolve(@{@"isLogined": @(isLogin)});
}
```
从代码上来看,iOS 的 promise 使用方式与 Android 还是有些差异的。
**React Native iOS 插件带参数示例**
```
RCT_EXPORT_METHOD(login:(int64_t)userID
password:(NSString *) password
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[[SDKHelper sharedHelper] login:userID password:password];
resolve(@"true");
}
```
Native 主动向 React Native 发送数据
---
在我们的需求里,还会遇到向 React Native 发送数据的功能。这个时候需要用到 EventEmitter,用于监听来自原生代码的示例。如下是两个不同的平台监听的代码:
```
webviewOnLoad = () => {
if(Platform.OS === 'android') {
DeviceEventEmitter.addListener('SDK_EVENT', this.handleNativeInvoke);
} else {
var RCTSDK = new NativeEventEmitter(NativeModules.SDK);
RCTSDK.addListener('SDK_EVENT', this.handleNativeInvoke)
}
}
```
这个时候就需要继承 RCTEventEmitter,其头文件如下所示:
```
#import <react rctbridgemodule.h="">
#import <react rcteventemitter.h="">
@interface SDK : RCTEventEmitter <rctbridgemodule>
@end
```
代码文件则如下:
```
#import <react rctbridge.h="">
@implementation SDK
@synthesize bridge = _bridge;
- (NSArray<nsstring *=""> *) supportedEvents
{
return @[@"SDK_EVENT"];
}
RCT_EXPORT_MODULE();
- (void)pushEvent:(NSString *)formatString eventName:(NSString *)eventName JSONObject:(id)JSONObject {
NSData* JSONObjectData = [NSJSONSerialization dataWithJSONObject:[JSONObject copy]
options:NSJSONWritingPrettyPrinted
error:nil];
NSString* JSONObjectString = [[NSString alloc] initWithData:JSONObjectData
encoding:NSUTF8StringEncoding];
NSString* javascript = [NSString stringWithFormat:formatString, eventName, JSONObjectString];
dispatch_async(dispatch_get_main_queue(), ^{
[self sendEventWithName:@"SDK_EVENT" body: @{@"injectedJS": javascript}];
});
}
@end
```
我们需要使用如下的代码,以此来声明事件的类型:
```
- (NSArray<nsstring *=""> *) supportedEvents
{
return @[@"SDK_EVENT"];
}
```
随后使用下面的代码,来向 React Native 发送事件:
```
[self sendEventWithName:@"SDK_EVENT" body: @{@"injectedJS": javascript}];
```
React Native 编译问题
---
在这个过程中,还遇到编译的问题。
以下是编加编译的示例:
```
//:configuration = Debug
HEADER_SEARCH_PATHS = $(SRCROOT)/** $(SRCROOT)/node_modules/react-native/React $(SRCROOT)/../../../node_modules/react-native/React/** $(SRCROOT)/../react-native/React/**
//:configuration = Release
HEADER_SEARCH_PATHS = $(SRCROOT)/** $(SRCROOT)/node_modules/react-native/React $(SRCROOT)/../../../node_modules/react-native/React/** $(SRCROOT)/../react-native/React/**
//:completeSettings = some
HEADER_SEARCH_PATHS
```
还有:
```
//:configuration = Debug
FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/../node_modules/react-native-xxx/ios/RNxxx/**
//:configuration = Release
FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/../node_modules/react-native-xxx/ios/RNZxxx/**
//:completeSettings = some
FRAMEWORK_SEARCH_PATHS
```
除此,还需要在**项目**里手动添加库:
Link Binary with Libraries
添加: ``CrashReporter.framework, CoreTelephony.framework``
以及相应的编译选项:
```
//:configuration = Debug
OTHER_LDFLAGS = -ObjC -lsqlite3 -lz -liconv -lstdc++ -lc++ -lstdc++.6
//:configuration = Release
OTHER_LDFLAGS = -ObjC -lsqlite3 -lz -liconv -lstdc++ -lc++ -lstdc++.6
//:completeSettings = some
OTHER_LDFLAGS
```
</nsstring></nsstring></react></rctbridgemodule></react></react></rctbridgemodule>使用 React Native 重写大型 Ionic 应用后,我们想分享一下这八个经验2017-07-26T13:31:46+00:002017-07-26T13:28:37.306844+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-case-study/本文的内容是关于 React Native 重写的经验分享,基于 React Native 重写 Ionic 应用[Growth](https://github.com/phodal/growth-v2) 过程中遇到的一些坑。
Growth 是一款专注于Web开发者成长的应用。其 1.0 和 2.0 主要使用 Ionic 实现,Ionic 1.x 的主要问题是 Angular 1.x 已经落后了。而 Ionic 2.x 则在启动的性能上不是让人满意——其实在开源方面,我是中 HDD(热闹驱动开发)的一员。
在重写的过程中,我们错误估计了其开发效率与 Ionic 2.x 是接近的,我们以为会差上个 0.2 倍左右的差距——**上手新的框架的学习成本**。但是实际上这个差距可能是在 0.5~1.0 倍之间,毕竟要填的坑太多了,以至于在中途的时候让人想放弃。
最后,我们花了两三个月的时间才重写完这个应用。在 APP 发布的这几天里,顺便写了篇文章分享一下经验:
- 你遇到的问题,别人基本到遇到过
- 版本间差异太大,导致下游配套
- 新的组件坑更多
- 大部分时间,你都是在重写 UI
- 最麻烦的地方,其实是搭建环境
- 真机才能反映问题
- 尽早尝试 Release 0.0.1
- 记得记录崩溃问题
幸运的是,作为一个开源应用,你可以看到这些坑是如何解决的。
你遇到的问题,别人基本到遇到过
---
你遇到的问题,别人基本到遇到过,要么就是你的**关键词不对**。
这一点实际上与 React Native 无关,只是在编写应用的过程中,遇到一些奇怪的问题。而尽管我第一时间使用了 Google 来搜索,但是并不能第一时间找到合适的答案。因为在这个领域里,我算是半年新手,总会错失一些关键词。
而遗憾的是,Google 不一定能第一时间帮你解决问题,有些问题在官方的 issues 里,但是没有被索引。因此,如果 Google 不到结果,请找官方的 issues,或者源码。
如果只是一般的应用,那么你遇到的问题,大部分人也都遇到过。除非,你是在写一些原生的组件,遇到一些莫名其妙地问题。
版本间差异太大,导致下游配套
---
开始编写 Growth 的时候,使用的 React Native 的版本是 0.42。在Growth 3.0 里面,使用了一些长的列表,如 awesome 列表,导致性能上不是很理解。在看到 React Native 0.43+ 之后,便升级到了 React Native 0.44。
整个升级过程中,看上去很容易:
1. 修改 package.json 中 react-native 的版本从 ^0.42.0 为 ^0.44.3
2. 修改 package.json 中的 react、react-dom 等组件版本从 15.4.2 变为 16.0.0-alpha.6
然而新版本里的类型检测 prop-types,已经变成了一个独立的组件,这就意味着我需要修改所有相关的代码。要不就会遇到这样的问题:
```
Warning: View.propTypes has been deprecated and will be removed in a future version of ReactNative. Use ViewPropTypes instead.
```
而,这意味着所有的第三方组件都需要修改。这并不是一件容易的事,这会导致遇到一系列的问题,如我的持续集成会在 Travis CI 出现问题。
幸运的是,我使用的原生组件比较少,因此也没有遇到一些组件不能支持新版本的问题。而对于那些库来说,他们可能是这样子的 README:
```
if on react-native < 0.40: npm i react-native-xx@0.4
if on 0.42 >= react-native >= 0.40 npm i react-native-xx@0.6
if on 46 >= react-native >= 0.44 npm i react-native-xx@1.0
```
除了此,对于我来说,那些使用 enzyme 写的测试也出现了问题,因为 enzyme 的开发者不想支持 alpha 版本的软件。
新的组件坑更多,如文档更新不及时
---
当我们更新了我们的 RN 版本,我们可能会遇到文档更新不及时的问题。
如,在 Android 版里的 WebView 可以支持 ``allowUniversalAccessFromFileURLs``,即如果我的 WebView 是通过 file 拿到的,有了这个参数就可以发起不是跨域的请求——可以解决 WebView 的跨域问题。但是在使用的时候,文档并没有更新到这方面的内容。那么,这个坑就只能自己去看源码填。好在你可以用 Command + B 查看到相关的问题。
与些同时,如果你看到的文档是中文翻译,那么出于某种原因,这个坑可能会更深。
因此,作为一个 HDD(热闹驱动开发) 的程序员,如果你喜欢跳坑里,那么你应该具备深入源码的能力。否则,就不要轻易地使用最新框架,坑你不止一点点。
大部分时间,你都是在重写 UI
---
如果你已经习惯了 React,那么总体来说,没有多大地区别。并且,我们可以使用直接使用大量的 Node.js 的库,如 moment。
由于,这是我第二次尝试 React Native,总的来说体验比第一次好多了。先简单地作为一个小结,对于大部分人来说,他们只是在写业务功能。故而:大部分时间,你都是在重写 UI。
重写的过程中,我预期会遇到一些原生的组件问题,然而一个都没有——列表性能问题另算,即使采用了。想来这个生态已经是成熟了,
在这个过程中,尽管会遇到一些 iOS 打包的问题,Android 资源的问题。而如果你和我一样,也写过 Cordova 的插件,那么其实问题并不会太多,只是仍然会有一点。
注:在 Ionic 时代,可以用 Ionic resources 来生成 icon 和 splash 以适配不同的机型。在 RN 中,可以采用 generator-rn-toolbox 来做类似的事。
最麻烦的地方,其实是搭建环境
---
这一点和 Web 应用开发是类似的,在搭建 Web 应用开发环境的时候,我们需要:
- 设计构建系统
- 搭建持续集成
- 完成自动化发版
- 等等
因而,有了上面的 UI 结论后,你也熟悉了 React。对于你来说接触 RN 最大的障碍是:搭建一个开发环境。你需要 Android SDK,以及 iOS 的环境。
准备完之后, 你用官方的脚本创建了一个 hello, world,发现跑不起来,啊哈哈哈~。
最有意思的事,当前版本的 RN 不支持 Android 的包名创建。如果你开始没有取好包名,就开始填坑了,那么你就需要 react-native-rename 这样的工具来重命名包。然后继续你填坑~~
除此,作为一个追求质量的程序员,我们还需要:编写测试。除了,使用 Facebook 的 Jest 进行单元测试,React Native Test Renderer 进行界面测试,还有集成测试工具 Appium。
这些没有哪一个都会很顺利的~~。
有时候真机才能反映问题
---
模拟器虽然很快,但是它并不能暴露出一些问题,容易出现一些不一致。相关经历,如下所示。
尽早尝试 Release 0.0.1
---
我的意思是,你要早点出一个 React Native 的 MVP 版本。
由于,我日常用的手机是 Android 系统,而 React Native 的 Web 资源问题,实际上在 Android 和 iOS 上都会出现的。Android 手机上需要将资源放在:``file:///android_asset/``,但是对于 iOS 也需要放在 ``assets`` 目录下,否则 Release 的时候,会找不到相应的资源。
这是我在要发布新版本的时候,遇到的问题。如果是在真实开发过程中,那么这一点可能会影响你的 KPI,如果有的话;又或者会导致你加班。
记得记录崩溃问题
---
我在 release Growth 3.0 的早期版本 2.9.9 的时候,漏掉了一个对 Null 值的判断,结果造成了大量的闪退问题(三十几个用户)。
好在我采用的 Google Analytics 能收集崩溃信息,如下:
```
RCTFatalException: Unhandled JS Exception: null is not an object (evaluating 'h.default.getWebView().postMessage') Trace: <redacted> growth growth <re
```
因此,在下一版本里便快速修复了。除此,在一些未知的机型也会出现问题,尽管是个问题,但是由于数量较少就没有在意了。
对于写原生代码来说,这几乎是必备的手段。对于 WebView 来说,并不会存在太大的崩溃问题,除非使用了原生组件。</redacted>React Native WebView PostMessage 问题:Setting onMessage on a WebView overrides2017-07-16T02:20:13+00:002017-07-16T02:26:16.946276+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-webview-post-message-failure/继上一篇《[React Native WebView onMessage 收不到 window.postMessage 消息](https://www.phodal.com/blog/react-native-onmessage-couldnt-read-postmessage-issue/)》之后,我遇到一个奇怪的 WebView 问题:
> Setting onMessage on a WebView overrides existing values of window.postMessage,
but a previous value was defined.
这次就机智了,第一时间找 React Native 官网的 issues,果然就有了:[onMessage failing when there are JS warnings and errors on the page](https://github.com/facebook/react-native/issues/10865)
由于里面给的方案是这样的:
```javascript
(function() {
var originalPostMessage = window.postMessage;
var patchedPostMessage = function(message, targetOrigin, transfer) {
originalPostMessage(message, targetOrigin, transfer);
};
patchedPostMessage.toString = function() {
return String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
};
window.postMessage = patchedPostMessage;
})();
```
实际上,就是在上一篇中,我们复写了 ``window.postMessage`` 方法导致的。不过,由于上面的代码及作者给出的,都不能满足 AirBnb ESLint 的风格,因此便记录一下修改后的代码
```javascript
const patchPostMessageFunction = () => {
const originalPostMessage = window.postMessage;
const patchedPostMessage = (message, targetOrigin, transfer) => {
originalPostMessage(message, targetOrigin, transfer);
};
patchedPostMessage.toString = () => String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
window.postMessage = patchedPostMessage;
};
const patchPostMessageJsCode = `(${String(patchPostMessageFunction)})();`;
```
React Native + Visual Studio Code / Monaco Editor 制作手机编辑器2017-06-04T14:38:25+00:002017-06-04T15:04:12.085697+00:00Phodal Huanghttp://www.phodal.com/blog/author/root/http://www.phodal.com/blog/react-native-visual-studo-code-monaco-editor-build-mobile-editor/因为想为 Growth 3.0 (GitHub:[https://github.com/phodal/growth-ng](https://github.com/phodal/growth-ng))里添加一个练习功能,便想要这样的话,就需要一个代码编辑器——是的,我又要定制一个编辑器了。能想到最简单的方案就是:基于 WebView。而,我想要大致的功能就是:
- 可以用 React Native 传递代码到 WebView 里
- 点击 React Native 上的按钮,就可以运行 WebView 里的代码
便想到了 VS Code 背后的 Monaco Editor,其架构师为大名鼎鼎的「GoF 设计模式」作者之一 Erich Gamma。
因此,我要做的无非就是:
- 集成 Monaco Editor
- 制定一个简单的代码运行机制、消息规范
- 处理一些特殊的函数,如 console.log
React Native 集成 Monaco Editor
---
这一步倒是比较简单:从官网下载一个 Demo 即可,然后按示例,配置一下就可以了:
```javascript
require.config({paths: {'vs': './vs'}});
require(['vs/editor/editor.main'], function() {
var editor = monaco.editor.create(document.getElementById('container'), {
value: [
'function x() {',
'\tconsole.log("Hello world!");',
'}'
].join('\n'),
language: 'javascript'
});
});
```
而,为了支持中文,则还需要加上几句代码:
```javascript
require.config({
'vs/nls' : {
availableLanguages: {
'*': 'zh-cn'
}
}
});
```
然后在我们的 RN 代码中,引入这个 WebView 即可:
```javascript
render = () => {
let source;
if (__DEV__) {
source = require('./GrEditor/index.html');
} else {
source = Platform.OS === 'ios' ? require('./GrEditor/index.html') : { uri: 'file:///android_asset/GrEditor/index.html' };
}
return (
<webview =="" ref="{(webview)"> {
this.webview = webview;
EditorWebViewServices.setWebView(webview);
}}
startInLoadingState
source={source}
onMessage={this.handleMessage}
automaticallyAdjustContentInsets={false}
style={[AppStyles.container, styles.container, { height: this.state.visibleHeight }]}
/>
);
}
```
然后,我们就需要制定一个简单的代码运行机制。
RN 触发 Monaco Editor 事件
---
当我们在原生界面上点击运行时,我们只需要 postMessage 过去即可:
```javascript
static runCode() {
EditorWebViewServices.getWebView().postMessage(JSON.stringify({
action: 'runCode',
code: {},
}));
}
```
然后在 WebView 里处理对应的 Action:
```javascript
document.addEventListener('message', function (event) {
var data = JSON.parse(event.data);
if(data.action === 'runCode') {
eval(window.editor.getValue());
}
});
```
这里的 ``window.editor.getValue()`` 可以获取到 Monaco Editor 中的代码,为了方便我就暂时直接用 eval 函数了,啊哈哈哈~~。
这样一来,代码勉强算是可以工作了。
React Native 与 Monaco Editor 处理 console.log
---
在测试的过程中,我发现了 ``console.log`` 是没有地方输出的,想自己定制一个 Console.log 界面,发现还挺麻烦的。于是,就想到了用 Toast 来显示——于是,我便在 WebView 里覆盖住了系统的 console.log,改成了直接 postMessage:
```javascript
window.console.log = function(options) {
var result = {
action: 'console',
data: options,
};
window.postMessage(JSON.stringify(result));
};
```
而在 React Native 的组件里,则也是对数据进行对应的处理:
```javascript
handleMessage = (event: Object) => {
const message = JSON.parse(event.nativeEvent.data);
if (message.action === 'console') {
if (isObject(message.data)) {
Toast.show(JSON.stringify(message.data));
} else {
Toast.show(message.data.toString());
}
}
};
```
可这样,还是有个问题,我测试了一个 for 循环,结果发现只显示最后一个值。不过,我相信没有太多的用户会用它来写简单的代码。
React Native Monaco Editor 屏幕旋转
---
同理的,后来在处理屏幕也是相似的,在 RN 里监听屏幕旋转:
```javascript
orientationDidChange = () => {
EditorWebViewServices.getWebView().postMessage(JSON.stringify({
action: 'resize',
}));
};
```
然后在 WebView 里处理:
```javascript
document.addEventListener('message', function (event) {
var data = JSON.parse(event.data);
if(data.action === 'runCode') {
eval(window.editor.getValue());
}
if(data.action === 'resize') {
window.editor.layout();
}
});
```
周末就这么过去了,有人要来一起填坑吗?
</webview>