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。

参数名称 参数类型 参数说明
text string 需要测量的文本内容
fontSize number 字体大小
fontFamily string 字体名称

3.返回值

对象object,类型MeasureResult。

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

2.2.创建模块文件夹

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

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

shell 复制代码
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定义进行相关代码编写如下,具体可以查看代码中的注释:

typescript 复制代码
// 文件名:NativeRTNTextHelper.ts
import { TurboModule, TurboModuleRegistry } from "react-native";

export type TextInfo = {
    text: string
    fontSize: number
    fontFamily?: 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目录下),文件的具体内容如下:

JSON 复制代码
{
    "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相关的插件):

groovy 复制代码
// 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.ReactModuleInfoProvider

class RTNTextHelperPackage : TurboReactPackage() {
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule?  = null

  override 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,最终生成的代码的目录结构如下:

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

java 复制代码
/**
 * 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);
  }

  @Override
  public @Nonnull String getName() {
    return NAME;
  }

  @ReactMethod
  @DoNotStrip
  public abstract void measureTextAsync(ReadableMap textInfo, Promise promise);

  @ReactMethod(isBlockingSynchronousMethod = true)
  @DoNotStrip
  public abstract WritableMap measureTextSync(ReadableMap textInfo);
}

这里有几个特征:

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

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

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

groovy 复制代码
// ~/.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,如下图:

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

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

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

kotlin 复制代码
package com.sample.rtntexthelper

import 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.WritableNativeMap

class RTNTextHelperModule(reactContext: ReactApplicationContext) :
    NativeRTNTextHelperSpec(reactContext) {
    companion object {
        const val NAME = "RTNTextHelper"
    }

    override fun getName() = NAME

    override 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这个文件,具体内容如下:

kotlin 复制代码
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.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class 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工程中。

shell 复制代码
cd ReactNativeSample
yarn add ../RTNTextHelper

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

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

3.2.JS调用

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

jsx 复制代码
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;

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

shell 复制代码
# 首先连接好设备
cd ReactNativeSample
yarn android

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

4.总结

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

相关推荐
前端小小王28 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发38 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
盛派网络小助手1 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
不是鱼5 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
快乐非自愿5 小时前
分布式系统架构2:服务发现
架构·服务发现
2401_854391086 小时前
SSM 架构中 JAVA 网络直播带货查询系统设计与 JSP 有效实现方法
java·开发语言·架构
264玫瑰资源库6 小时前
从零开始C++棋牌游戏开发之第二篇:初识 C++ 游戏开发的基本架构
开发语言·c++·架构
神一样的老师6 小时前
面向高精度网络的时间同步安全管理架构
网络·安全·架构
2401_857026236 小时前
基于 SSM 架构的 JAVA 网络直播带货查询系统设计与 JSP 实践成果
java·开发语言·架构
9527华安6 小时前
FPGA实现MIPI转FPD-Link车载同轴视频传输方案,基于IMX327+FPD953架构,提供工程源码和技术支持
fpga开发·架构·mipi·imx327·fpd-link·fpd953