国内开发React Native的基本都是Web前端,一般都没有原生开发相关的知识,所以有一些与原生相关的功能就难以实现。
近期因为要封装一个SDK给React Native使用,所以仔细研究了一番,实际上原生模块的编写并不复杂。
本文简单的记录一下原生模块的编写过程,发布至npm并在主项目中使用,需要熟悉React Native及其目录结构。
项目创建
原生模块的项目创建比较麻烦,我没有深入研究如何搭建项目,而是使用一个很好的脚手架项目,能够直接为我们创建一个模版项目。
本文名称就使用默认的react-native-awesome-module
。
npx create-react-native-library react-native-awesome-module
输入对应的信息
这里语言选择为 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提供的Arguments
,WritableMap
之类的。
转数组用Arguments.makeNativeArray
,转Map
用Arguments.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个小时,都没玩游戏。
春节了,终于可以好好休息了!祝大家春节快乐!