ReactNative(0.61)整合到现有App和避坑指南

2,483 阅读5分钟

整合ReactNative(0.61)到 iOS 和 Android 两个端

1. 简介

ReactNative近年来快速发展,各大公司也使用非常广泛,官方文档包含整合方式:

Integration with Existing Apps

然鹅,按照这篇文档整合并不能顺利整合成功,除非你实力强大,自带避坑能力。

最近我再写一个系列文章,里面包含整合ReactNative的内容:

《成为大前端》系列

这里单独拿出其中自己避坑过程,希望能帮到你。

2. 前提 (必读)

先假设的三端项目名为:

  • iOS: SampleIOS
  • Android: SampleAndroid
  • ReactNative: IntegrationRN

并且已经创建或者已有了 iOS 和 Android 项目,路径分别是:

你/的/路径/
    SampleIOS/
    SampleAnroid/

路径可以任意,但如果和文章这里的不同,你需要注意你配置的相对路径和本文章的区别

3. 创建ReactNative项目

这里创建一个用于整合的项目,不是用在开发上线的项目。按照官方整合文档说明创建即可,不同点如下:

  • 项目之间的相对路径不同
  • 使用 npm 而不是 yarn

创建项目文件夹

创建 IntegrationRN 与 iOS 和 Android 项目并列

SampleIOS/
SampleAnroid/
IntegrationRN/

创建 package.json 并安装依赖

IntegrationRN/
  package.json  +

内容为:

{
  "name": "integration-rn",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "react-native start"
  }
}

安装依赖

cd path/to/IntegrationRN
npm install react-native --save

完成之后,我们需要安装指定版本的 react,版本参考前一个命令的输出:

npm WARN react-native@0.61.5 requires a peer of react@16.9.0 but none is installed.

npm install react@16.9.0 --save

创建 index.js

IntegrationRN/
  package.json
  index.js      +

内容为

import React from 'react';
import {AppRegistry, StyleSheet, Text, View} from 'react-native';

class HelloWorld extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>Hello, World</Text>
      </View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  hello: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});

AppRegistry.registerComponent('IntegrationRN', () => HelloWorld);

测试完成情况

npm run start

4. iOS 整合

现有项目结构如下:

使用 pod 引入 RN 依赖

如果你的项目已经是用 pod 管理,可以跳过

安装 CocoaPods, 安装方法有很多,我们这里使用 gem

sudo gem install cocoapods

编写 Podfile

SampleIOS下创建Podfile文件,与SampleIOS.xcodeproj并列

如果按照官方文档提供的 Podfile 内容,由于它太老了,你将调入无尽大坑:

  1. 需要修复react-native相对路径
  2. 需要修复好几个依赖的路径
  3. 运行发现奔溃,需要添加更多依赖,但是是哪些呢,路径在哪呢
  4. 加完之后,前端开发完,发现还需要添加NativeComponent,???

还是使用我花精力搞定的 Podfile 吧

无 Bug 的 Podfile

如果你的项目路径和前面预设的有差异,注意调整react_native_path变量

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

target 'SampleIOS' do
    react_native_path = "../IntegrationRN/node_modules/react-native"
    pod 'FBLazyVector', :path => "#{react_native_path}/Libraries/FBLazyVector"
    pod 'FBReactNativeSpec', :path => "#{react_native_path}/Libraries/FBReactNativeSpec"
    pod 'RCTRequired', :path => "#{react_native_path}/Libraries/RCTRequired"
    pod 'RCTTypeSafety', :path => "#{react_native_path}/Libraries/TypeSafety"
    pod 'React', :path => "#{react_native_path}"
    pod 'React-Core', :path => "#{react_native_path}"
    pod 'React-Core/DevSupport', :path => "#{react_native_path}"
    pod 'React-CoreModules', :path => "#{react_native_path}/React/CoreModules"
    pod 'React-RCTActionSheet', :path => "#{react_native_path}/Libraries/ActionSheetIOS"
    pod 'React-RCTAnimation', :path => "#{react_native_path}/Libraries/NativeAnimation"
    pod 'React-RCTBlob', :path => "#{react_native_path}/Libraries/Blob"
    pod 'React-RCTImage', :path => "#{react_native_path}/Libraries/Image"
    pod 'React-RCTLinking', :path => "#{react_native_path}/Libraries/LinkingIOS"
    pod 'React-RCTNetwork', :path => "#{react_native_path}/Libraries/Network"
    pod 'React-RCTSettings', :path => "#{react_native_path}/Libraries/Settings"
    pod 'React-RCTText', :path => "#{react_native_path}/Libraries/Text"
    pod 'React-RCTVibration', :path => "#{react_native_path}/Libraries/Vibration"
    pod 'React-Core/RCTWebSocket', :path => "#{react_native_path}"

    pod 'React-cxxreact', :path => "#{react_native_path}/ReactCommon/cxxreact"
    pod 'React-jsi', :path => "#{react_native_path}/ReactCommon/jsi"
    pod 'React-jsiexecutor', :path => "#{react_native_path}/ReactCommon/jsiexecutor"
    pod 'React-jsinspector', :path => "#{react_native_path}/ReactCommon/jsinspector"
    pod 'ReactCommon/jscallinvoker', :path => "#{react_native_path}/ReactCommon"
    pod 'ReactCommon/turbomodule/core', :path => "#{react_native_path}/ReactCommon"

    pod 'Yoga', :path => "#{react_native_path}/ReactCommon/yoga"

    pod 'DoubleConversion', :podspec => "#{react_native_path}/third-party-podspecs/DoubleConversion.podspec"
    pod 'glog', :podspec => "#{react_native_path}/third-party-podspecs/glog.podspec"
    pod 'Folly', :podspec => "#{react_native_path}/third-party-podspecs/Folly.podspec"
end

然后运行 pod install 搞定 ReactNative 依赖安装

代码整合

关掉 Xcode,使用 SampleIOS.xcworkspace 打开项目

接下是轻松的代码整合,修改 ViewController 的代码:

import UIKit
import React

class ViewController: UIViewController {

    override func loadView() {
        let jsCodeURL = URL(string: "http://localhost:8081/index.bundle?platform=ios")!
        let rootView = RCTRootView(
            bundleURL: jsCodeURL,
            moduleName: "IntegrationRN",
            initialProperties: [:],
            launchOptions: nil
        )
        self.view = rootView
    }
    
}

注意:

  • 如果你是在真机上运行,把localhost改为你的react开发服务器的ip
  • moduleName 和我们前面 js 代码 AppRegistry.registerComponent 的一致

最后,info.plist 配置允许加载 http

<key>NSAppTransportSecurity</key>
<dict>
	<key>NSAllowsArbitraryLoads</key>
	<true/>
</dict>

整合完毕,运行之后是 ReactNative 的开发界面,显示的是Hello, World

5. Android 整合

配置 gradle

implementation "com.facebook.react:react-native:+"

配置 maven

allprojects {
    repositories {
        // 指向我们的RN android源码
        maven { url "$rootDir/../IntegrationRN/node_modules/react-native/android" }
        ...
    }
}

代码整合

创建 Activity ,代码如下,这里我直接使用 MainActivity:

class MainActivity : Activity(), DefaultHardwareBackBtnHandler {

    private var mReactRootView: ReactRootView? = null
    private var mReactInstanceManager: ReactInstanceManager? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mReactRootView = ReactRootView(this)
        mReactInstanceManager = ReactInstanceManager.builder()
            .setApplication(application)
            .setCurrentActivity(this)
            .setBundleAssetName("index.android.bundle")
            .setJSMainModulePath("index")
            .addPackage(MainReactPackage())
            .setUseDeveloperSupport(BuildConfig.DEBUG)
            .setInitialLifecycleState(LifecycleState.RESUMED)
            .build()
        // The string here (e.g. "MyReactNativeApp") has to match
        // the string in AppRegistry.registerComponent() in index.js
        mReactRootView!!.startReactApplication(mReactInstanceManager, "IntegrationRN", null)

        setContentView(mReactRootView)
    }

    override fun invokeDefaultOnBackPressed() {
        super.onBackPressed()
    }

    override fun onPause() {
        super.onPause()
        mReactInstanceManager?.onHostPause(this)
    }

    override fun onResume() {
        super.onResume()
        mReactInstanceManager?.onHostResume(this, this)
    }

    override fun onDestroy() {
        super.onDestroy()
        mReactInstanceManager?.onHostDestroy(this)
        mReactRootView?.unmountReactApplication()
    }

    override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
        if (keyCode == KEYCODE_MENU && mReactInstanceManager != null) {
            mReactInstanceManager!!.showDevOptionsDialog()
            return true
        }
        return super.onKeyUp(keyCode, event)
    }

    override fun onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager!!.onBackPressed()
        } else {
            super.onBackPressed()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        mReactInstanceManager?.onActivityResult(this, requestCode, resultCode, data)
    }

}

SoLoader.init 问题

运行将遇到第一个问题:

java.lang.RuntimeException: SoLoader.init() not yet called

在 Application.onCreate 中添加:

SoLoader.init(this, false)

libhermes.so 问题

运行将遇到第二个问题,也是官方完全没有说明的问题

E/AndroidRuntime: FATAL EXCEPTION: create_react_context
    Process: com.example.sampleandroid, PID: 9677
    java.lang.UnsatisfiedLinkError: couldn't find DSO to load: libhermes.so
        at com.facebook.soloader.SoLoader.doLoadLibraryBySoName(SoLoader.java:738)
        at com.facebook.soloader.SoLoader.loadLibraryBySoName(SoLoader.java:591)
        at com.facebook.soloader.SoLoader.loadLibrary(SoLoader.java:529)
        at com.facebook.soloader.SoLoader.loadLibrary(SoLoader.java:484)
        at com.facebook.hermes.reactexecutor.HermesExecutor.<clinit>(HermesExecutor.java:20)
        at com.facebook.hermes.reactexecutor.HermesExecutorFactory.create(HermesExecutorFactory.java:27)
        at com.facebook.react.ReactInstanceManager$5.run(ReactInstanceManager.java:952)
        at java.lang.Thread.run(Thread.java:929)

关键信息

couldn't find DSO to load: libhermes.so

ReactNative 从某个版本之后开始使用 hermes 替代JavaScriptCore作为 JS 引擎

解决办法,添加 hermes 的依赖包:

implementation "com.facebook.react:react-native:+"
def hermesPath = "$rootDir/../IntegrationRN/node_modules/hermes-engine/android/"
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")

到这里,再在模拟器上运行,会正常加载,并显示Hello, World

真机运行问题

真机运行会出现以下问题:

问题的关键是 ReactNative 的 DevSupport 模块使用了 localhost 加载的 js 代码,但 React开发服务器 是在你的电脑上。

官方文档也给出了解决方式:

adb devices
adb reverse tcp:8081 tcp:8081

但是这种方式太坑了,不同的开发机每次都要运行这段

通过查看 DevSupport 模块的源码,我们得到一个近似完美的解决方式:

// 改变 debug_http_host
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
preferences.edit().putString("debug_http_host", WebManager.getWebDevServer()).apply()

最后如果你使用了androidx,有可能遇到:

Failed resolution of: Landroidx/swiperefreshlayout/widget/SwipeRefreshLayout;

继续添加这个依赖即可

implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'

这次运行,终于Hello, World

结语

整合流程其实很简单,但是其中的坑想要避开,查阅了大量的资料,希望这篇能减少你的整合时间。

文章源码:github.com/zzmingo/Sam…

最后

整合完后,最重要的问题是提供离线包机制,在我的系列文章里会陆续更新出来,欢迎关注我。