ZHANGYU.dev

October 14, 2023

为React Native编写原生模块

国内开发React Native的基本都是Web前端,一般都没有原生开发相关的知识,所以有一些与原生相关的功能就难以实现。

近期因为要封装一个SDK给React Native使用,所以仔细研究了一番,实际上原生模块的编写并不复杂。

本文简单的记录一下原生模块的编写过程,发布至npm并在主项目中使用,需要熟悉React Native及其目录结构。

项目创建

原生模块的项目创建比较麻烦,我没有深入研究如何搭建项目,而是使用一个很好的脚手架项目,能够直接为我们创建一个模版项目。

本文名称就使用默认的react-native-awesome-module

npx create-react-native-library react-native-awesome-module

输入对应的信息

image-20220129135010332

这里语言选择为 Kotlin & Swift,因为这2种语言对TypeScript开发者更加友好。

下一步类型选择 Native module (to expose native APIs)。

接着根据提示执行yarn安装依赖。

目录结构

.├── CONTRIBUTING.md├── LICENSE├── README.md├── android├── babel.config.js├── example├── ios├── package.json├── react-native-awesome-module.podspec├── scripts├── src├── tsconfig.build.json└── tsconfig.json

只需要关注以下目录

目前此模板生成的example项目React Native版本为0.63.4,需要修改一段代码才能运行example iOS端。

example/ios中的Podfile文件中

use_flipper!({ 'Flipper' => '0.80.0' })

修改为

use_flipper!({ 'Flipper-Folly' => '2.5.3', 'Flipper' => '0.87.0', 'Flipper-RSocket' => '1.3.1' })

语法简介

简单以TypeScript为例子介绍下语法

let

const

function

object

const array:string[] = []

iOS端

iOS端的.podspec文件在项目根目录,我们需要在这里将我们模块的依赖写上。

require "json"package = JSON.parse(File.read(File.join(__dir__, "package.json")))Pod::Spec.new do |s|  s.name         = "react-native-awesome-module"  s.version      = package["version"]  s.summary      = package["description"]  s.homepage     = package["homepage"]  s.license      = package["license"]  s.authors      = package["author"]  s.platforms    = { :ios => "10.0" }  s.source       = { :git => "https://github.com/zhangyu1818/react-native-awesome-module.git", :tag => "#{s.version}" }  s.source_files = "ios/**/*.{h,m,mm,swift}"  s.dependency "React-Core"end

s.platforms表示此模块最低的iOS版本,如果主项目的版本低于此文件的版本,那就无法使用此模块。

s.dependency表示此模块依赖的Pod包,如果我们要依赖额外的Pod包,则需要添加。

比如我要将GooglePlaces作为依赖,那我需要添加以下内容。

s.dependency "GooglePlaces", "~>4.2.0"

因为GooglePlaces在iOS 10最高只能使用4.2.0的版本,所以我需要标记版本。

接下来我们看iOS模块的文件。

编写iOS模块

在此之前建议先阅读官方iOS模块文档

双击ios/AwesomeModule.xcodeproj使用Xcode打开项目。

接下来看目录。

├── AwesomeModule-Bridging-Header.h├── AwesomeModule.m└── AwesomeModule.swift
AwesomeModule.m

此文件里的语法还是Objective-C语法,一看就会令人难以理解。

#import <React/RCTBridgeModule.h>@interface RCT_EXTERN_MODULE(AwesomeModule, NSObject)RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b                 withResolver:(RCTPromiseResolveBlock)resolve                 withRejecter:(RCTPromiseRejectBlock)reject)@end

RCT_EXTERN_MODULE为OC里面的宏用来导出我们的模块和方法。

@interface RCT_EXTERN_MODULE(AwesomeModule, NSObject)

这里参数里的AwesomeModule为我们的模块名称。

RCT_EXTERN_METHOD(multiply:(float)a withB:(float)b                 withResolver:(RCTPromiseResolveBlock)resolve                 withRejecter:(RCTPromiseRejectBlock)reject);

这里就是导出了一个计算乘积结果的原生方法,方法名为multiply

因为Objective-C中传入参数是有名字的,所以会看上去怪怪的,伪代码举个例子。

multiply(10, withB: 20)
AwesomeModule.swift

这里Swift的代码,至少是没学过也可读的。

@objc(AwesomeModule)class AwesomeModule: NSObject {    @objc func multiply(a: Float, b: Float, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock){        resolve(a*b)    }}

AwesomeModule继承NSObject,里面有一个方法multiply,没有返回值,它有4个参数,由于要传递给OC调用,所以需要添加@objc

添加新方法

比方说我们要添加一个新方法,在JS端应该这样被调用。

queryPlace("成都",{	filter: "city",}).then(result =>{  console.log(result);}).catch(error =>{  console.log(error);})

添加定义

// AwesomeModule.m@interface RCT_EXTERN_MODULE(AwesomeModule, NSObject)// ...RCT_EXTERN_METHOD(queryPlace: (NSString *)query                  options:(NSDictionary *)options                  resolve:(RCTPromiseResolveBlock)resolve                  reject:(RCTPromiseRejectBlock)reject);@end

实现方法

这只是一个简单的例子,实际场景肯定是调用别的包的方法。

// AwesomeModule.swift// ...@objc func queryPlace(_ query: String, options: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock){  let filter = options["filter"] as? String  DispatchQueue.main.sync{    if let filter = filter {      resolve(query + filter)    }else {      let error = NSError(domain: "error", code: 500, userInfo: nil)      reject("出错了","没有传入filter", error)    }  }}

在这个方法里,如果我们传入了filter,则会调用resolve返回query + filter,否则reject抛出错误。

此方法使用DispatchQueue.main.async在iOS的主线程开启了一个任务,里面调用了resolve

我们称为回调的在iOS里称为闭包,@escaping表示resolve是一个逃逸闭包。

其实这里比较好理解,在JS里,如果A函数里返回了B函数,B函数使用了A函数里变量,自动就闭包里,iOS需要使用@escaping来保持对此变量的持有。

需要实现的额外方法

在iOS中,它的布局UIKit是运行在主线程的,而我们的React Native是运行在别的线程的,在别的线程里是不能操作主线程的UIKit的,所以这个时候通常需要调用DispatchQueue.main.sync或者DispatchQueue.main.async来执行我们的操作。

所以我们需要需要实现额外方法,来告诉React Native此模块应该运行在那个线程。

// AwesomeModule.swift// ...@objc var methodQueue = DispatchQueue.main@objc static func requiresMainQueueSetup() -> Bool {  return true}

如果我们的模块不需要在主线程初始化,我们需要将requiresMainQueueSetup的值返回false,也不需要methodQueue这个属性了。

Android端

Android端相比iOS端会简单很多很多,没有OC上古语法,没有声明文件,直接写就行了。

Android端如果我们的模块有额外依赖,写在android/build.gradle里就行了。

还是以添加GooglePlaces为例。

dependencies {  // noinspection GradleDynamicVersion  api 'com.facebook.react:react-native:+'  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"  implementation 'com.google.android.libraries.places:places:2.5.0' // 加上这一行}

同一个SDK,Android端和iOS端可能包名版本都不一样哦~

编写Android模块

src└── main    ├── AndroidManifest.xml    └── java        └── com            └── reactnativeawesomemodule                ├── AwesomeModuleModule.kt                └── AwesomeModulePackage.kt

我们只需要关注AwesomeModuleModule.kt这个文件就行了。

AwesomeModuleModule.kt

class AwesomeModuleModule(reactContext: ReactApplicationContext) :  ReactContextBaseJavaModule(reactContext) {  override fun getName(): String {    return "AwesomeModule"  }  @ReactMethod  fun multiply(a: Int, b: Int, promise: Promise) {    promise.resolve(a * b)  }}

只需要在需要导出的方法上加一个@ReactMethod注解就行了。

还是以上文需要新增的JS方法为例。

@ReactMethodfun queryPlace(query: String, options: ReadableMap, promise: Promise) {  val filter = options.getString("filter")  if (filter != null) {    promise.resolve(query + filter)  } else {    promise.reject("error","没有传入filter")  }}

是不是很简单!

值的互相转换

iOS,Android和React Native的转换需要我们自己做一些操作。

React Native传值给原生

有以下结构的值

const options = {	name: "zhangyu1818",  coordinate: {    latitude: 30.656994,    longitude: 104.080009  }}

iOS

func test(options:[String:Any]){  if let name = options["name"] as? String {    // 操作name  }  if let coordinate = as? [String: [String: Double]] {    if let latitude = coordinate["latitude"], let longitude = coordinate["longitude"] {      // 操作latitude和longitude    }   }}

Android

fun test(options: ReadableMap){  val name = options.getString("filter")  val coordinate = options.getMap("coordinate")    val latitude = coordinate?.getDouble("latitude")  val longitude = coordinate?.getDouble("longitude")}

原生传值给React Native

一个简单的例子

有以下结构的原生对象(这里仅仅以TypeScript做为类型定义)

interface Result {  name: string  types: string[]  complex: {    values?: string[]    address: {      text: string      coordinate?: {        latitude: number        longitude: number      }    }  }}

以下仅供参考,实际情况不单单只是转成Dictionary或者Map就行。

iOS

func convert(value: Result) -> [String: Any] {  let dic:[String:Any] = [    "name": value.name,    "types": value.complex.values,    "complex": [      "values": value.complex.values,      "address": [        "text": value.complex.address.text,        "coordinate": [          "latitude": value.complex.address.coordinate?.latitude,          "longitude": value.complex.address.coordinate?.longitude        ]      ]    ]  ]  return dic}

Android

Android这边需要使用React Native提供的ArgumentsWritableMap之类的。

转数组用Arguments.makeNativeArray,转MapArguments.makeNativeMap

fun convert(value: Result): WritableMap {  return Arguments.makeNativeMap(    mapOf(      "name" to value.name,      "types" to value.types,      "complex" to mapOf(        "values" to value.complex.values,        "address" to mapOf(          "text" to value.complex.address.text,          "coordinate" to mapOf(            "latitude" to value.complex.address.coordinate?.latitude,            "longitude" to value.complex.address.coordinate?.longitude          )        )      )    )  )}

总结

总体还是比较简单,因为我也是从零开始花了几天时间做了一个,但是公司内不让开源,不能发出来给大家参考。

我仔细想了想,虽然我Swift一年时间里都断断续续的在学,但是开发模块其实并不怎么需要原生基础,基本就是调用原生SDK方法,然后暴露给React Native。

只需要看看Swift文档,Kotlin文档,简单了解下语法就行了。

如果需要写原生视图的包就比较复杂了,这就要求必须掌握一定的原生能力了,目前我也还没涉及。


春节放假第一天,写了2个小时,都没玩游戏。

春节了,终于可以好好休息了!祝大家春节快乐!