您的位置:首页 > 房产 > 家装 > React Native新架构系列-自定义Turbo Native Module扩展API

React Native新架构系列-自定义Turbo Native Module扩展API

2025/1/10 22:06:42 来源:https://blog.csdn.net/bingjianIT/article/details/140969605  浏览:    关键词:React Native新架构系列-自定义Turbo Native Module扩展API

今天我们介绍在React Native新架构中如何自定义Turbo Native Module扩展API。本系列基于React Native 0.73.4版本,从一名Android开发者的视角进行介绍。本系列介绍的内容默认读者对React Native有一定的了解,对基础的开发内容不再赘述。

在老的架构中,自定义API的能力被称为Native Modules。在新架构中,自定义API的能力被称为Turbo Native Modules,本文的主要内容参考自React Native开源代码中关于Turbo Native Modules的Android和JS部分的介绍,并结合一个实际的例子,会在其中穿插一下自己遇到的问题和理解。

1.为什么需要Turbo Native Module

其实,React Native已经给我们提供了常见的API,例如打开链接、网络请求、设备振动等API,所有的API我们可以在官方文档中查询到。但有一些场景需要我们有一些诉求,比如从相册选择图片,获取定位或者是进行复杂的图片处理等,这时候我们就需要利用Turbo Module将平台的(Android、iOS、HarmonyOS)能力桥接到JS侧,供我们在JS侧调用。一般情况下,我们使用React Native编写JS代码都是运行在Andorid和iOS双平台上,所以我们两端都需要对接响应的能力。但本文只会介绍Android和JS侧的实现,不包含iOS侧的实现。

2.如何开发一个Turbo Native Module

下面我们以一个具体的例子,详细介绍如何实现一个Turbo Native Module来自定义API提供给JS侧使用。

2.1.定义API

这里我们定义实现2个API用于以同步和异步的方式测量一段文本的宽度,定义如下:

1.API名称:

  • measureTextAsync

  • measureTextSync

2.参数定义:

对象object,类型TextInfo。

参数名称参数类型参数说明
textstring需要测量的文本内容
fontSizenumber字体大小
fontFamilystring字体名称

3.返回值

对象object,类型MeasureResult。

返回值名称返回值类型返回值说明
widthnumber文本宽度

2.2.创建模块文件夹

由于我们希望我们的模块可以独立分发给任意的React Native项目使用,所以我们需要单独创建一个模块文件夹编写相关的代码,这个模块文件夹中包含JS、Android、iOS代码实现(当然,我们本次对于iOS代码逻辑不进行实现)。

我们创建一个模块文件夹RTNTextHelper,其中RTN代表React Native。在其中创建js、android、ios 3个目录分别代表对应的代码实现,最终目录结构如下:

RTNTextHelper
├── android
├── ios
└── js

2.3.使用TypeScript定义API接口代码

接下来我们首先根据我们之前的API定义来定义JS侧接口代码,这部分代码是最终需要我们在React Native业务代码中使用的对外接口代码,放在js目录下,然后才有后面Andorid、iOS分别进行具体实现。

新架构要求我们必须使用TypeScript或者Flow进行定义,这是为了接口定义中出入参类型是明确的,方便后面Codegen依据此进行模版代码生成。同时还有2个要求。

  1. 该文件必须命名为Native<MODULE_NAME> ,使用 Flow 时扩展名为.js.jsx ,使用 TypeScript 时扩展名为.ts.tsx 。 Codegen 将仅查找与此模式匹配的文件。
  2. 该文件必须导出TurboModuleRegistrySpec对象。

我们这里根据我们前面的API定义进行相关代码编写如下,具体可以查看代码中的注释:

// 文件名:NativeRTNTextHelper.ts
import { TurboModule, TurboModuleRegistry } from "react-native";export type TextInfo = {text: stringfontSize: numberfontFamily?: string
}export type MeasureResult = {width: number
}export interface Spec extends TurboModule {// 异步方法measureTextAsync(textInfo: TextInfo): Promise<MeasureResult>;// 同步方法measureTextSync(textInfo: TextInfo): MeasureResult;
}export default TurboModuleRegistry.get<Spec>("RTNTextHelper") as Spec | null;

需要注意以下几点:

  1. 模块定义比如是一个名称为Spec的interface
  2. 必须继承自TurboModule
  3. TurboModuleRegistry中get需要填写的模块名为我们之前定义的模块名(但需要注意不是文件名,不包含Native前缀),这行代码执行的时候,会加载我们的模块

2.4.添加模块配置

2.4.1.添加package.json

因为我们最终分发的模块是一个包含了Android、iOS、JS代码的npm包,然后用户通过yarn install进行安装,所以我们需要给我们的模块编写一个package.json文件(注意,package.json文件放在模块根目录下,不是js目录下),文件的具体内容如下:

{"name": "rtn-texthelper","version": "0.0.1","description": "Measure text with Turbo Native Modules","react-native": "js/index","source": "js/index","files": ["js","android","ios","rtn-calculator.podspec","!android/build","!ios/build","!**/__tests__","!**/__fixtures__","!**/__mocks__"],"keywords": ["react-native","ios","android"],"repository": "https://github.com/linkecoding/RTNTextHelper","author": "Sidon","license": "MIT","bugs": {"url": "https://github.com/linkecoding/RTNTextHelper/issues"},"homepage": "https://github.com/linkecoding/RTNTextHelper#readme","devDependencies": {},"peerDependencies": {"react": "*","react-native": "*"},"codegenConfig": {"name": "RTNTextHelperSpec","type": "modules","jsSrcsDir": "js","android": {"javaPackageName": "com.sample.rtntexthelper"}}
}

这里有几项配置需要说明:

  • name这个是最终发布的npm包的名称
  • react-native和source指向npm包中js代码入口对应的文件
  • peerDependencies中不指定具体的版本号,依赖具体业务工程中的版本号进行确定
  • codegenConfig这个是最关键的配置
    • name与最终生成的Android/iOS模板代码的名称有关,这里一般为模块名称添加Spec后缀
    • jsSrcsDir是codegen寻找模块定义的API代码(NativeRTNTextHelper.ts文件)时查找的目录名称,我们这里是js目录
    • android.javaPackageName是定义最终生成的Android模板代码的包名称

2.4.2.添加build.gradle配置

除了配置npm包模块外,我们需要配置android文件夹中的信息,Android代码是以module形式存在的,所以必然存在build.gradle文件。

这个文件的代码,大部分也是固定的,内容如下(由于我们后面逻辑使用Kotlin编写,所以这里的插件中引入了kotlin相关的插件):

// android/build.gradle
buildscript {ext.safeExtGet = {prop, fallback ->rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback}repositories {google()gradlePluginPortal()}dependencies {classpath("com.android.tools.build:gradle:7.3.1")classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22")}
}apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
apply plugin: 'org.jetbrains.kotlin.android'android {compileSdkVersion safeExtGet('compileSdkVersion', 33)namespace "com.sample.rtntexthelper"
}repositories {mavenCentral()google()
}dependencies {implementation 'com.facebook.react:react-native'
}

这里需要注意的有几个点:

  • android配置块中
    • compileSdkVersion需要根据你的实际情况进行配
    • namespace是Android Gradle Plugin 7.3.0版本提出的一个新的配置字段,主要用于为编译时自动生成的R类和BuildConfig类指定包名称,这里我们设置为与package.json中配置的javaPackageName字段的值保持一致。

2.4.3.编写Android代码ReactPackage类

为了后面使用Codegen生成代码,我们还需要在对应的package目录下使用Java或者Kotlin写一个类继承自TurboReactPackage类,但我们可以先将实现留空,这一步只是为了Codegen可以正常帮我们生成一部分模板代码。

我们在android目录下创建目录src/main/java/com/sample/rtntexthelper目录,然后在文件夹中创建一个Kotlin类(当然你也可以使用Java编写代码)RTNTextHelperPackage.kt,具体内容如下:

package com.sample.rtntexthelper;import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfoProviderclass RTNTextHelperPackage : TurboReactPackage() {override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule?  = nulloverride fun getReactModuleInfoProvider(): ReactModuleInfoProvider? = null
}

除了package名称和类名,其他代码都是固定写法。

2.5.使用Codegen生成代码

接下来终于到了使用Codegen生成模板代码的步骤了,因为无论是编写Turbo Native Module还是自定义组件都存在很多模板代码,所以React Native官方使用Codegen来帮助我们生成一部分代码。

要使用Codegen,我们需要把我们的模块添加到一个完整的React Native App工程中,我们这里命名为ReactNativeSample,我们的自定义Turbo Native Module的文件夹与这个工程平级。

具体如何创建一个React Native App工程可以查看上一篇文章React Native新架构系列-新架构介绍。

这里提供2个库的Git下载地址,可以直接下载体验:

ReactNativeSample:https://github.com/linkecoding/ReactNativeSample

RTNTextHelper:https://github.com/linkecoding/RTNTextHelper

我们执行下面的命令来通过Gradle任务来调用Codegen:

cd ReactNativeSample
yarn add ../RTNTextHelper
cd android
./gradlew generateCodegenArtifactsFromSchema

最终我们在这个目录下会获得自动生成的代码ReactNativeSample/node_modules/rtn-texthelper/android/build/generated/source/codegen,最终生成的代码的目录结构如下:

image-20240806010536036

这里最主要的代码是NativeRTNTextHelperSpec这个类,它是根据我们JS侧的API定义生成了Android侧的抽象类,内容如下:


/*** This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).** Do not edit this file as changes may cause incorrect behavior and will be lost* once the code is regenerated.** @generated by codegen project: GenerateModuleJavaSpec.js** @nolint*/package com.sample.rtntexthelper;import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import javax.annotation.Nonnull;public abstract class NativeRTNTextHelperSpec extends ReactContextBaseJavaModule implements TurboModule {public static final String NAME = "RTNTextHelper";public NativeRTNTextHelperSpec(ReactApplicationContext reactContext) {super(reactContext);}@Overridepublic @Nonnull String getName() {return NAME;}@ReactMethod@DoNotStrippublic abstract void measureTextAsync(ReadableMap textInfo, Promise promise);@ReactMethod(isBlockingSynchronousMethod = true)@DoNotStrippublic abstract WritableMap measureTextSync(ReadableMap textInfo);
}

这里有几个特征:

  • 继承了ReactContextBaseJavaModule类并实现了TurboModule接口
  • getName返回我们的API模块名称
  • 根据我们的API定义生成了2个抽象方法等待我们实现
  • 同步方法直接返回结果,异步方法使用方法的第二个参数promise进行返回

这里需要注意的是,Codegen生成的代码,我们不应该提交到Git进行版本管理,我们最终分发的这个npm包中不应该包含这部分代码,自动生成的代码仅用于我们开发库时编译。最终当某个App的代码引入我们的库时,Codegen会在编译过程中自动为我们的库生成代码并将全部代码打入App中。

这里还踩到了另一个坑(没有遇到的可以直接跳过这里),由于我的Gradle在~/.gradle/init.gradle中重新配置了每个项目的build目录的生成文件位置,如下内容:

// ~/.gradle/init.gradle文件
gradle.projectsLoaded {rootProject.allprojects {buildDir = "/Users/xxx/.gradle-build/${rootProject.name}/${project.name}"}
}

所以我使用上面的gradle命令生成Codegen代码时,实际的生成位置位于~/.gradle-build/ReactNativeSample/rtn-texthelper/generated/source/codegen目录下。

2.6.编写具体API实现代码

经过上面的步骤,项目的配置,模板代码都已经生成好了,接下来我们就开始编写代码了,由于我们之前的yarn add ../RTNTextHelper命令会直接将我们的模块引入到我们的ReactNativeSample工程,所以我们可以直接使用AndroidStudio打开ReactNativeSample工程的android目录,同步代码后就会看到我们的module,如下图:

image-20240806012017590

但这里有个非常坑的点,我们使用yarn add ../RTNTextHelper这个命令时,它实际上是将我们的RTNTextHelper里面的代码拷贝了一份到ReactNativeSample工程的node_module/rtntexthelper目录下,我们在Android Studio中看到的是这个目录。

所以当我们在Android Studio里写好了代码后,只要再执行一次yarn add ../RTNTextHelper,我们的代码就丢失了(因为又重新安装了)。我目前的办法是在Android Studio中写好后,手动拷贝到RTNTextHelper这个模块里。如果有其他优雅的方法可以评论交流。

接下来就是实现我们2个API的具体逻辑了,我们需要定义个RTNTextHelperModule类,继承自上面自动生成的NativeRTNTextHelperSpec类,实现其中的2个方法,具体逻辑如下:

package com.sample.rtntexthelperimport android.graphics.Paint
import android.graphics.Typeface
import android.text.TextUtils
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMapclass RTNTextHelperModule(reactContext: ReactApplicationContext) :NativeRTNTextHelperSpec(reactContext) {companion object {const val NAME = "RTNTextHelper"}override fun getName() = NAMEoverride fun measureTextAsync(textInfo: ReadableMap?, promise: Promise?) {val width = getTextWidth(textInfo)val result = WritableNativeMap()result.putDouble("width", width)promise?.resolve(result)}override fun measureTextSync(textInfo: ReadableMap?): WritableMap {val width = getTextWidth(textInfo)val result = WritableNativeMap()result.putDouble("width", width)return result}private fun getTextWidth(textInfo: ReadableMap?): Double {return textInfo?.let {val text = it.getString("text")val fontSize = it.getInt("fontSize")val fontFamily = it.getString("fontFamily")val paint = Paint()paint.textSize = fontSize.toFloat()if (!TextUtils.isEmpty(fontFamily)) {paint.typeface = Typeface.create(fontFamily, Typeface.NORMAL)}val width = paint.measureText(text)return@let width.toDouble()} ?: 0.0}
}

这里需要注意同步API和异步API对应的方法的返回值写法稍有差异其他没有区别。

还需要注意,我们getName返回的名称,一定要与我们JS侧获取模块时的名称一致,这个名称也会在我们下面注册和查找模块逻辑中用到。

实现完具体逻辑以后,我们还需要将我们的模块进行注册,确保在JS侧调用时可以正确找到我们的模块,我们需要修改RTNTextHelperPackage.kt这个文件,具体内容如下:

package com.sample.rtntexthelperimport com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProviderclass RTNTextHelperPackage : TurboReactPackage() {override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =if (name == RTNTextHelperModule.NAME) {RTNTextHelperModule(reactContext)} else {null}override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {mapOf(RTNTextHelperModule.NAME to ReactModuleInfo(RTNTextHelperModule.NAME,RTNTextHelperModule.Companion::class.java.simpleName,false,false,false,true))}
}

最主要的就是根据getModule的name参数来匹配返回我们自定义的模块。

3.如何使用自定义Turbo Native Module

接下来就可以看一下如何在一个React Native工程中使用我们的模块。其实在之前使用Codegen生成代码时我们就一定用到了这样的方式。

3.1.引入

在本地调试时,我们可以直接通过下面的命令将我们自己实现的模块添加到React Native工程中。

cd ReactNativeSample
yarn add ../RTNTextHelper

这种方式会将我们的模块安装到工程的node_modules目录下。

如果我们将自己开发的模块发布到了npm仓库,也可以直接使用yarn add <npm包名>,来添加模块,方式几乎是一样的。

3.2.JS调用

由于我们之前已经写了JS侧的代码,我们就可以像使用一个正常的npm包那样使用我们的模块中的JS代码,只是逻辑最终会走到我们Native侧的实现并返回结果,这里给一个简单的使用示例:

import React, { useEffect } from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
import RTNTextHelper from 'rtn-texthelper/js/NativeRTNTextHelper';function App(): React.JSX.Element {useEffect(() => {const textInfo = {text: '测试文本',fontSize: 12,};const result = RTNTextHelper?.measureTextSync(textInfo);console.log('===RTNTextHelper.measureTextSync==', JSON.stringify(result));RTNTextHelper?.measureTextAsync(textInfo).then(res => {console.log('===RTNTextHelper.measureTextAsync==', JSON.stringify(res));}).catch(() => {// ignore});}, []);return (<SafeAreaView><StatusBar barStyle={'dark-content'} /><ScrollView contentInsetAdjustmentBehavior="automatic"><Text>Sample</Text></ScrollView></SafeAreaView>);
}export default App;

然后我们执行下面的命令即可:

# 首先连接好设备
cd ReactNativeSample
yarn android

最后就可以在控制台看到输出日志如下

image-20240807014007084

4.总结

本文详细介绍了React Native新架构中如何实现一个自定义的Turbo Native Module并成功在JS进行调用,对于其中遇到的问题和细节也都有详细的介绍,欢迎大家一起交流学习。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com