React-native集成到原生项目

Posted 疯狂小芋头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React-native集成到原生项目相关的知识,希望对你有一定的参考价值。

文章目录


以下基于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为RN注册创建模块时由package提供的
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) 
        //来自RN的框架提供的
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new ToastModule(reactContext));
        return modules;
    


public class ToastModule extends ReactContextBaseJavaModule 
    //每个RN模块都是需要使用该 reactContext 来提供初始化的
    public ToastModule(ReactApplicationContext context)

原生给JS通讯一般是以下几个流程:

  1. 定义通讯的事件名称
  2. 构建发送的事件数据(一般是RN框架提供的ReadableMap),数据以key-value的形式存在
  3. 调用RN模块的方法发送事件即可

注意RN要处理事件的话,必须在监听相应的事件

1.4.2. JS->原生

JS与原生的通讯通过定义桥接的接口达到调用的目的,这部分的规则与android中JS通讯的基本规则一致,在JS中调用的模块方法名称与android本地定义的方法名称是必须一致的。

除此之外,相关的方法需要添加上@ReactMethod的注解即可。注意交互通讯的方法必须是在注意到RN的模块中

1.5. 打包React Native编译后的bundle文件

参考:将ReactNative项目打包生成jsbundle

  1. 首先需要在RN项目中创建输出的文件夹 out/iosout/android 的文件夹,用于存储输出的打包数据
  2. 使用以下命令打包
  3. 命令不会默认创建文件夹,如果文件夹未被创建成功则报错
//ios
react-native bundle --entry-file index.js --bundle-output ./out/ios/index.ios.bundle --platform ios --assets-dest ./out/ios --dev false

//android
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模块提供的本地依赖文件。官方也没有提供明确的远程依赖方式。

  1. 将当前RN开发项目中的node_module文件夹打包压缩,提供给到第三方项目方,第三方项目方通过解压使用该文件夹中的本地依赖库解决对RN运行环境的依赖。分别需要在项目根目录下添加依赖库地址,项目中需要添加使用的依赖库

  2. 根目录中的build.gradle添加解压后的node_module文件夹的路径作为依赖库仓库

    allprojects
        repositories
                // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
                maven  url("node_module文件夹路径/react-native/android") 
                // Android JSC is installed from npm
                maven  url("node_module文件夹路径/jsc-android/dist") 
        
    
    
  3. 在使用RN模块的项目中添加相关依赖

    dependencies
        //RN的依赖库
        implementation "com.facebook.react:react-native:+"
        //JSC的js解析引擎
        implementation "org.webkit:android-jsc:+"
    
    
  4. 依赖注意事项

  • react-native和JS引擎库的版本号建议都使用+来表示,表示获取最新的版本号。因为这里的版本依赖是通过本地进行依赖的,所以使用+可以确保与本地的node modules里的版本对应而不会出现版本冲突的问题

  • 关于JS解析引擎,根据新建的RN项目的配置信息,应该是使用了JSC有引擎。JS引擎实际上要不是使用了hermes,要不是使用JSC,是不需要同时存在这两个引擎的。
    RN原始项目的引用声明如下:根据声明实际上是应该使用了JSC引擎

    //RN原项目是这样声明依赖的
    project.ext.react = [
        enableHermes: false,  // clean and rebuild if changing
    ]
    //JSC引擎依赖版本
    def jscFlavor = 'org.webkit:android-jsc:+'
    def enableHermes = project.ext.react.get    ("enableHermes", false);
    
    dependencies 
        //这里根据实际的配置选择是否使用hermes,根据配置默认是false的,但是实际上最终是true,hermes的so库会被打包到应用中
        //JSC与hermes是二选一的
        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源码的项目直接调试

2.2.1. 更新RN模块的依赖

对于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服务器,如果自己有需要可以定义相关命令在这里。
dependenciesRN模块中依赖的库,注意这里的库实际上就是在根目录的node_module的文件夹下的,其中部分项目可能是包含native源码

当RN模块的代码获取到后,本地可能是没有node_module的文件夹的(或者有但是依赖库不全),这时需要同步依赖库。重点:同步前先删除掉yarn.lock的文件,该文件会锁住当前的依赖库版本导致同步失败。

//同步依赖库命令
yarn

如果同步失败请检查:

  1. 是否有删除掉yarn.lock文件再同步
  2. 是否有科学上网,有很多的库都需要科学上网才能正常同步
  3. 如果是在终端运行命令,注意终端是否已经设置了代理,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中后,可能是整个源码项目而不是aarjar的依赖库,所以需要添加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项目的依赖。如果编译失败或运行失败请根据错误信息并检查以下操作:

  1. 上述的命令中使用的node_module路径是相对地址,请确认地址是正确的能访问到相应的node_module文件夹
  2. 如果运行失败发现缺少某些库或依赖时,请注意确认有没有执行上一步更新RN模块的依赖操作
    对于报错的库,可以通过RN中package.json文件的依赖来查看node_module中是否有该库的存在。如json文件中依赖库为@react-hook/async,则表示了在node_module/@react-hook/async文件夹下有这个库。
  3. 所有使用了native源码的库,在添加了这个gradle的引用后,都会在项目中以project的形式存在

2.2.3. 动态调试

2.2.3.1. 远程调试

对于JS服务器不在本机,需要连接到其它机子上的服务器时,可以通过DevTools修改连接的地址,默认的地址是localhost:8081,连接到本机的JS服务器。

  1. 通过摇晃手机调起调试弹窗,修改host地址
  2. 直接通过代码指定调试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)
字段默认值说明
bundleAssetNameindex.android.bundle本地bundle文件名称,一般不修改
jsMainModulePathindexRN模块中的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)
    //不需要设置jsBundlePath,或者设置为null
    //setJSBundleFile(bundlePath)

val rnManager=builder.build()
//启动页面,指定模块名
rnRootView.startReactApplication(rnManager, moduleName, null)

加载远程文件时,相关字段意义如下(仅针对通常情况下)

字段默认值说明
bundleAssetNameindex.android.bundle固定为此值,ios应该为index.ios.bundle
jsMainModulePathindex固定值,对应的就是RN模块中的index.js文件,是程序的入口
jsBundleFilenull不需要设置
moduleName-根据实际业务设置启动的模块名称,一般是index.js文件中AppRegistry.registerComponent()注册的模块名称

2.3. 其它注意事项

  1. 如果有需要使用到RN库的地方,才添加相关的依赖,否则不需要
  2. 根据RN官网的资料,在原生项目中集成RN时,application是需要实现ReactApplication的接口。但是实际上,通过上面的方式集成RN模块后,并不需要实现ReactApplication的接口就可以正常加载和使用RN的 bundle 了,不用修改应用的 Application
  3. 如果提示JS代码alert弹窗时未找到寄宿的activity,请注意是否reactRootView是否挂载在FragmentActivity中,因为RN官方的DialogModule模块是通过Fragment实现弹窗的

以上是关于React-native集成到原生项目的主要内容,如果未能解决你的问题,请参考以下文章

React-native集成到原生项目

react-native入门,编写静态页面,集成原生项目

与本机应用程序集成时,React-native 应用程序在每次导航时都会重新启动

react-native混合原生开发

原生iOS项目导入ReactNative,各种问题

在现有项目 React-Native 中添加现有的原生项目 Android