zhangyu1818.

Tag »SwiftReact NativeKotlin

为React Native编写原生模块

Jan 29, 2022View on Github

国内开发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

只需要关注以下目录

  • src —— npm publish后别的项目会引用这里的文件,这里会做一些原生方法导出,类型定义。

  • ios —— React Native iOS端会加载的文件,这部分我们会使用Swift编写

  • android —— React Native Android端会加载的文件,这部分我们会使用Kotlin编写

  • example —— 运行example项目来调试我们的原生模块

目前此模板生成的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

  • Swift => var
  • Kotlin => var

const

  • Swift => let
  • Kotlin => val

function

  • Swift => func
  • Kotlin => fun

object

  • Swift => [String: Any]()
  • Kotlin => Map

const array:string[] = []

  • Swift => let array = [String]()
  • Kotlin => val a: 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-Bridging-Header —— 此文件为Objective-C和Swift的bridge文件,因为我们是用Swift来编写所以需要此文件,通常我们不需要改这个文件。

  • 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

  • (float)a 代表第一个无名参数,类型为float

  • withB:(float)b 代表第二个名称为withB的参数,变量名为b,类型为float

  • withResolver:(RCTPromiseResolveBlock)resolve 代表JS中Promise的resolve回调。

  • withRejecter:(RCTPromiseRejectBlock)reject)代表JS中Promise的reject回调。

因为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方法为例。

@ReactMethod
fun 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个小时,都没玩游戏。

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