整合ReactNative(0.61)到 iOS 和 Android 两个端
1. 简介
ReactNative近年来快速发展,各大公司也使用非常广泛,官方文档包含整合方式:
然鹅,按照这篇文档整合并不能顺利整合成功,除非你实力强大,自带避坑能力。
最近我再写一个系列文章,里面包含整合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 内容,由于它太老了,你将调入无尽大坑:
- 需要修复react-native相对路径
- 需要修复好几个依赖的路径
- 运行发现奔溃,需要添加更多依赖,但是是哪些呢,路径在哪呢
- 加完之后,前端开发完,发现还需要添加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
了
结语
整合流程其实很简单,但是其中的坑想要避开,查阅了大量的资料,希望这篇能减少你的整合时间。
最后
整合完后,最重要的问题是提供离线包
机制,在我的系列文章里会陆续更新出来,欢迎关注我。