以下基于react-native 0.64.2
更新说明
日期 | 更新内容 |
---|
2021-08-12 | 补充RN调试及bundle加载的信息 | 2021-07-10 | - |
1. React Native集成到原生项目中
1.1. RN的工作原理
对于RN的工作原理,这里简单说明一下不做深入的探索,主要是为了更好地了解和使用RN。
1.2. 推荐集成方式
在原生项目中直接集成RN并不一定很好操作,更快捷和合理的方法是通过RN创建一个全新的项目,然后再将原项目移植到该项目下替换原来的android 文件夹中的原生项目。
使用此方法可以尽可能保证集成时不必要的麻烦。
1.3. 集成RN的开发环境
此部分操作以官网提供的方式集成即可,一般以最新版本的RN集成相应的环境。
官方中文文档参考,包含环境集成,混合集成
在MAC系统中,RN使用的npm包管理及其它环境,都需要安装/usr/local/bin 到路径下,由于MAC系统最新系统可能对系统路径有访问的权限限制,所以使用homebrew安装的话可能会出现一些无法安装的情况。如果原环境中已经有homebrew等环境,建议直接升级即可。
1.4. 原生与JS通讯交互
原生与JS交互的事件通讯
1.4.1. 原生->JS
原生与JS的通讯一般通过事件进行数据的传递。双方通过注册和使用相同名称的事件名称进行事件收发。
WritableMap params = Arguments.createMap();
params.putString("message",msg.obj.toString());
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
其中reactContext是RN的模块在package中创建并注册时,由RN框架提供的。每个RN的模块也都是需要该参数初始化的。以下是一个RN桥接模块的实现示例。
public class CustomToastPackage implements ReactPackage{
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ToastModule(reactContext));
return modules;
}
}
public class ToastModule extends ReactContextBaseJavaModule {
public ToastModule(ReactApplicationContext context){}
}
原生给JS通讯一般是以下几个流程:
- 定义通讯的事件名称
- 构建发送的事件数据(一般是RN框架提供的ReadableMap),数据以key-value的形式存在
- 调用RN模块的方法发送事件即可
注意RN要处理事件的话,必须在监听相应的事件。
1.4.2. JS->原生
JS与原生的通讯通过定义桥接的接口达到调用的目的,这部分的规则与android中JS通讯的基本规则一致,在JS中调用的模块方法名称与android本地定义的方法名称是必须一致的。
除此之外,相关的方法需要添加上@ReactMethod 的注解即可。注意交互通讯的方法必须是在注意到RN的模块中。
1.5. 打包React Native编译后的bundle文件
参考:将ReactNative项目打包生成jsbundle
- 首先需要在RN项目中创建输出的文件夹
out/ios 与 out/android 的文件夹,用于存储输出的打包数据 - 使用以下命令打包
- 命令不会默认创建文件夹,如果文件夹未被创建成功则报错
react-native bundle --entry-file index.js --bundle-output ./out/ios/index.ios.bundle --platform ios --assets-dest ./out/ios --dev false
react-native bundle --platform android --dev false --entry-file index.js --bundle-output ./out/android/index.android.bundle --assets-dest ./out/android
打包后的文件一般会生成两部分内容:index.android.bundle 的JS bundle文件,还有相关的资源文件。将两者一同放置到手机存储路径中读取即可正常加载相关的 bundle 与资源文件了。注意打包后的drawable资源文件夹也需要与bundle文件放到同一目录下,以便bundle中加载使用。
2. ReactNative模块在第三方项目中集成使用
与RN相关的模块如果打成了AAR给到第三方的应用或者项目去使用时,如果也要求需要有RN的开发运行环境,那对第三方应用开发来是很大的负担,也大大增加了开发的工作量与项目周期,毕竟众所周知开发环境的搭建一个走不好就是会很难处理。
相关的资料在官网暂时未找到,通过尝试与验证以下方案可以解决:RN模块以aar(依赖库)的方式在基本项目中使用的方案
2.1. RN运行的本地依赖
2.1.1. 提供node_module依赖
由于目前无法查找到运行RN需要的依赖库的远程仓库,所以只能使用RN项目的node_module模块提供的本地依赖文件。官方也没有提供明确的远程依赖方式。
-
将当前RN开发项目中的node_module文件夹打包压缩,提供给到第三方项目方,第三方项目方通过解压使用该文件夹中的本地依赖库解决对RN运行环境的依赖。分别需要在项目根目录下添加依赖库地址,项目中需要添加使用的依赖库 -
根目录中的build.gradle添加解压后的node_module文件夹的路径作为依赖库仓库 allprojects{
repositories{
maven { url("node_module文件夹路径/react-native/android") }
maven { url("node_module文件夹路径/jsc-android/dist") }
}
}
-
在使用RN模块的项目中添加相关依赖 dependencies{
implementation "com.facebook.react:react-native:+"
implementation "org.webkit:android-jsc:+"
}
-
依赖注意事项
-
react-native 和JS引擎库的版本号建议都使用+ 来表示,表示获取最新的版本号。因为这里的版本依赖是通过本地进行依赖的,所以使用+ 可以确保与本地的node modules里的版本对应而不会出现版本冲突的问题 -
关于JS解析引擎,根据新建的RN项目的配置信息,应该是使用了JSC 有引擎。JS引擎实际上要不是使用了hermes ,要不是使用JSC ,是不需要同时存在这两个引擎的。 RN原始项目的引用声明如下:根据声明实际上是应该使用了JSC 引擎
project.ext.react = [
enableHermes: false,
]
def jscFlavor = 'org.webkit:android-jsc:+'
def enableHermes = project.ext.react.get ("enableHermes", false);
dependencies {
if (enableHermes) {
def hermesPath = "../../node_modules/ hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
2.1.2. 提供aar依赖
待确认
2.2. RN模块集成与调试的处理
RN模块直接集成实际上是很简单的,大部分时候都没有太大的问题。但是当RN模块已经有相关的业务JS代码,JS功能实现也会引入一些模块的使用,如果只是单纯的JS模块代码,那么只需要解决好JS库的引用即可。如果引用第三方的RN模块,除了JS代码还会有与之关联的Native代码作为功能桥接,这里就比较容易有问题了。
以下全部指在混合项目中调试或者是使用RN项目的情况,即存在RN源码、native源码的项目直接调试
对于RN模块,所有的依赖声明在pakage.json 中声明,这里可以理解为是android中的gradle文件。以下是一个json文件示例:
{
"name":"模块名称",
"version":"模块版本号",
"scripts":{
"android":"react-native run-android"
},
"dependencies":{
"@react-hook/async":"^3.1.1"
}
}
以上只列出一些关心的字段,其它的暂时不关心所以不列出来(也可能是暂时还没有用到不知道他们的重要性,以后有再补)。
字段 | 作用 |
---|
name | 模块的名称,这个主要是参考,在实际与native项目中使用一般是关心index.js 中的模块名称 | scripts | 脚本命令,和yarn 命令配合使用,如yarn android 就是运行react-native run-android ,这个命令可以运行起android项目和JS服务器,如果自己有需要可以定义相关命令在这里。 | dependencies | RN模块中依赖的库,注意这里的库实际上就是在根目录的node_module 的文件夹下的,其中部分项目可能是包含native源码 |
当RN模块的代码获取到后,本地可能是没有node_module 的文件夹的(或者有但是依赖库不全),这时需要同步依赖库。重点:同步前先删除掉yarn.lock的文件,该文件会锁住当前的依赖库版本导致同步失败。
yarn
如果同步失败请检查:
- 是否有删除掉
yarn.lock 文件再同步 - 是否有科学上网,有很多的库都需要科学上网才能正常同步
- 如果是在终端运行命令,注意终端是否已经设置了代理,mac终端的代理命令为
export http_proxy=http://127.0.0.1:1087;export https_proxy=https://127.0.0.1:1087; 。
2.2.2. 更新Native代码的依赖
有的RN模块依赖的库有相应的Native代码实现,同步到node_module中后,可能是整个源码项目而不是aar 或jar 的依赖库,所以需要添加Native代码的依赖。
在需要使用RN模块的module中的build.gradle 中,添加上以下的依赖
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
以上的gradle依赖是使用node_module 中的gradle文件生成相应的Native项目的依赖。如果编译失败或运行失败请根据错误信息并检查以下操作:
- 上述的命令中使用的
node_module 路径是相对地址,请确认地址是正确的能访问到相应的node_module 文件夹 - 如果运行失败发现缺少某些库或依赖时,请注意确认有没有执行上一步更新RN模块的依赖操作
对于报错的库,可以通过RN中package.json 文件的依赖来查看node_module 中是否有该库的存在。如json文件中依赖库为@react-hook/async ,则表示了在node_module/@react-hook/async 文件夹下有这个库。 - 所有使用了native源码的库,在添加了这个gradle的引用后,都会在项目中以
project 的形式存在
2.2.3. 动态调试
2.2.3.1. 远程调试
对于JS服务器不在本机,需要连接到其它机子上的服务器时,可以通过DevTools 修改连接的地址,默认的地址是localhost:8081 ,连接到本机的JS服务器。
- 通过摇晃手机调起调试弹窗,修改host地址
- 直接通过代码指定调试JS地址,这个在无法调起调试弹窗时非常有用
SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
mPreferences.edit().putString("debug_http_host", "192.168.2.105:8081").commit();
2.2.3.2. 本地调试
当需要使用JS服务器进行动态调试时,默认情况下在RN项目下运行yarn android 的命令即可。在运行成功后,后续修改了Native的代码直接重新运行应用就可以正常调试。
如果单独运行起JS服务react-native start ,再单独运行Native应用,可能会出现无法调试的情况(如无法连接到JS服务器),请注意这种情况下就只能通过yarn android 。
2.2.4. 加载bundle
不管是源码调试还是运行打包后的JS代码,都是先将JS代码打包成bundle文件,然后再读取运行的。只是一个从JS服务器远程获取,一个是从本地文件加载。
2.2.4.1. 配置加载本地bundle文件
如果需要加载的本地已打包好的bundle文件,需要指定一些配置参数
val builder = ReactInstanceManager.builder()
builder.apply {
setApplication(application)
setBundleAssetName("index.android.bundle")
setJSMainModulePath("index")
setCurrentActivity(hostActivity)
addPackages(yourpackages)
setInitialLifecycleState(LifecycleState.RESUMED)
setJSBundleFile(bundlePath)
}
val rnManager=builder.build()
rnRootView.startReactApplication(rnManager, moduleName, null)
字段 | 默认值 | 说明 |
---|
bundleAssetName | index.android.bundle | 本地bundle文件名称,一般不修改 | jsMainModulePath | index | RN模块中的index.js文件,是程序的入口,一般也不修改 | jsBundleFile | - | bundle文件所在的文件绝对路径,如/sdcard/rn/index.android.bundle | moduleName | - | 根据实际业务设置启动的模块名称,一般是index.js文件中AppRegistry.registerComponent() 注册的模块名称 |
2.2.4.2. 配置加载远程服务器文件/或aasets
对于远程文件,实际上就是加载assets中的bundle文件,连接到JS服务器时,就会去服务器下载打包后的bundle文件加载。如果不连接JS服务器的情况下,则会直接尝试加载assets文件夹中的bundle文件。
val builder = ReactInstanceManager.builder()
builder.apply {
setApplication(application)
setBundleAssetName("index.android.bundle")
setJSMainModulePath("index")
setCurrentActivity(hostActivity)
addPackages(yourpackages)
setInitialLifecycleState(LifecycleState.RESUMED)
}
val rnManager=builder.build()
rnRootView.startReactApplication(rnManager, moduleName, null)
加载远程文件时,相关字段意义如下(仅针对通常情况下)
字段 | 默认值 | 说明 |
---|
bundleAssetName | index.android.bundle | 固定为此值,ios应该为index.ios.bundle | jsMainModulePath | index | 固定值,对应的就是RN模块中的index.js文件,是程序的入口 | jsBundleFile | null | 不需要设置 | moduleName | - | 根据实际业务设置启动的模块名称,一般是index.js文件中AppRegistry.registerComponent() 注册的模块名称 |
2.3. 其它注意事项
- 如果有需要使用到RN库的地方,才添加相关的依赖,否则不需要
- 根据RN官网的资料,在原生项目中集成RN时,
application 是需要实现ReactApplication 的接口。但是实际上,通过上面的方式集成RN模块后,并不需要实现ReactApplication 的接口就可以正常加载和使用RN的 bundle 了,不用修改应用的 Application - 如果提示JS代码alert弹窗时未找到寄宿的activity,请注意是否reactRootView是否挂载在
FragmentActivity 中,因为RN官方的DialogModule 模块是通过Fragment实现弹窗的
|