Native Modules

Native Modules

需要本机代码的项目

此页面仅适用于react-native init使用Create React Native App 制作的或使用此类应用程序弹出的项目。有关弹出的更多信息,请参阅创建React Native App存储库的指南。

有时候应用程序需要访问平台API,React Native目前还没有相应的模块。也许你想重用一些现有的Java代码,而不必用JavaScript重新实现它,或者编写一些高性能,多线程的代码,例如图像处理,数据库或任何数量的高级扩展。

我们设计了React Native,因此您可以编写真实的本机代码并访问平台的全部功能。这是一个更高级的功能,我们不希望它成为通常开发过程的一部分,但它存在必不可少。如果React Native不支持您需要的本地功能,您应该可以自己构建它。

启用Gradle

如果您打算对Java代码进行更改,我们建议启用Gradle Daemon以加快构建。

Toast模块

本指南将使用Toast示例。假设我们希望能够从JavaScript创建Toast消息。

我们首先创建一个本地模块。本地模块是一个Java类,通常扩展ReactContextBaseJavaModule该类并实现JavaScript所需的功能。我们的目标是能够使用ToastExample.show('Awesome', ToastExample.SHORTJavaScript 编写在屏幕上显示简短的敬酒。

package com.facebook.react.modules.toast; import android.widget.Toast; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import java.util.Map; import java.util.HashMap; public class ToastModule extends ReactContextBaseJavaModule { private static final String DURATION_SHORT_KEY = "SHORT"; private static final String DURATION_LONG_KEY = "LONG"; public ToastModule(ReactApplicationContext reactContext) { super(reactContext } }

ReactContextBaseJavaModule要求实现一个被调用的方法getName。此方法的目的是NativeModule在JavaScript中返回表示该类的字符串名称。所以我们在这里调用这个,ToastExample以便我们可以通过React.NativeModules.ToastExampleJavaScript 访问它。

@Override public String getName() { return "ToastExample"; }

一个可选的方法调用getConstants返回暴露给JavaScript的常量值。它的实现不是必需的,但对于需要同步从JavaScript到Java进行通信的关键预定义值非常有用。

@Override public Map<String, Object> getConstants() { final Map<String, Object> constants = new HashMap<>( constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG return constants; }

要向JavaScript公开方法,必须使用Java方法注释Java方法@ReactMethod。桥接方法的返回类型始终是void。React Native桥是异步的,因此将结果传递给JavaScript的唯一方法是使用回调或发射事件(请参见下文)。

@ReactMethod public void show(String message, int duration) { Toast.makeText(getReactApplicationContext(), message, duration).show( }

参数类型

下面的参数类型支持注释为的方法,@ReactMethod并且它们直接映射到它们的JavaScript等价物

Boolean -> Bool Integer -> Number Double -> Number Float -> Number String -> String Callback -> function ReadableMap -> Object ReadableArray -> Array

阅读有关ReadableMapReadableArray的更多信息

注册模块

Java中的最后一步是注册模块; 这发生在createNativeModules你的应用程序包中。如果一个模块没有注册,它将不能从JavaScript获得。

package com.facebook.react.modules.toast; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class AnExampleReactPackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList( } @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>( modules.add(new ToastModule(reactContext) return modules; } }

该包需要getPackagesMainApplication.java文件的方法中提供。该文件存在于react-native应用程序目录中的android文件夹下。这个文件的路径是:android/app/src/main/java/com/your-app-name/MainApplication.java

protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new AnExampleReactPackage() // <-- Add this line with your package name. }

为了更简单地从JavaScript访问您的新功能,通常会将本机模块包装到JavaScript模块中。这不是必须的,但是可以节省您的图书馆的消费者NativeModules每次都需要关闭它。这个JavaScript文件也成为您添加JavaScript端功能的好地方。

'use strict'; /** * This exposes the native ToastExample module as a JS module. This has a * function 'show' which takes the following parameters: * * 1. String message: A string with the text to toast * 2. int duration: The duration of the toast. May be ToastExample.SHORT or * ToastExample.LONG */ import { NativeModules } from 'react-native'; module.exports = NativeModules.ToastExample;

现在,从您的其他JavaScript文件中,您可以调用像这样的方法:

import ToastExample from './ToastExample'; ToastExample.show('Awesome', ToastExample.SHORT

Beyond Toasts

回调

本机模块还支持一种特殊的参数 - 回调。在大多数情况下,它用于将函数调用结果提供给JavaScript。

public class UIManagerModule extends ReactContextBaseJavaModule { ... @ReactMethod public void measureLayout( int tag, int ancestorTag, Callback errorCallback, Callback successCallback) { try { measureLayout(tag, ancestorTag, mMeasureBuffer float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0] float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1] float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2] float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3] successCallback.invoke(relativeX, relativeY, width, height } catch (IllegalViewOperationException e) { errorCallback.invoke(e.getMessage() } } ...

这个方法可以用JavaScript访问:

UIManager.measureLayout( 100, 100, (msg) => { console.log(msg }, (x, y, width, height) => { console.log(x + ':' + y + ':' + width + ':' + height }

本地模块应该只调用一次回调。但是,它可以存储回调并稍后调用它。

强调在本地函数完成后不立即调用回调是非常重要的 - 记住,桥接通信是异步的,这也与运行循环有关。

承诺

原生模块也可以实现承诺,这可以简化您的代码,特别是在使用ES2016的async/await语法时。当桥接本机方法的最后一个参数是a时Promise,其相应的JS方法将返回一个JS Promise对象。

重构上述代码以使用promise而不是回调,如下所示:

import com.facebook.react.bridge.Promise; public class UIManagerModule extends ReactContextBaseJavaModule { ... private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR"; @ReactMethod public void measureLayout( int tag, int ancestorTag, Promise promise) { try { measureLayout(tag, ancestorTag, mMeasureBuffer WritableMap map = Arguments.createMap( map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0]) map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1]) map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2]) map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3]) promise.resolve(map } catch (IllegalViewOperationException e) { promise.reject(E_LAYOUT_ERROR, e } } ...

此方法的JavaScript对应方返回Promise。这意味着您可以使用await异步函数中的关键字来调用它并等待其结果:

async function measureLayout() { try { var { relativeX, relativeY, width, height, } = await UIManager.measureLayout(100, 100 console.log(relativeX + ':' + relativeY + ':' + width + ':' + height } catch (e) { console.error(e } } measureLayout(

思路

原生模块不应该对它们被调用的线程有任何假设,因为当前的任务将来可能会发生变化。如果需要阻塞调用,那么应该将繁重的工作分派给内部管理的工作者线程,并从那里分发任何回调。

发送事件到JavaScript

原生模块可以向JavaScript发送事件而不用直接调用。最简单的方法是使用RCTDeviceEventEmitter可以从ReactContext下面的代码片段中获得的。

... private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params } ... WritableMap params = Arguments.createMap( ... sendEvent(reactContext, "keyboardWillShow", params

然后,JavaScript模块可以通过addListenerOn使用Subscribablemixin 进行注册以接收事件。

import { DeviceEventEmitter } from 'react-native'; ... var ScrollResponderMixin = { mixins: [Subscribable.Mixin], componentWillMount: function() { ... this.addListenerOn(DeviceEventEmitter, 'keyboardWillShow', this.scrollResponderKeyboardWillShow ... }, scrollResponderKeyboardWillShow:function(e: Event) { this.keyboardWillOpenTo = e; this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e },

您也可以直接使用DeviceEventEmitter模块来监听事件。

... componentWillMount: function() { DeviceEventEmitter.addListener('keyboardWillShow', function(e: Event) { // handle event. } } ...

从中获取活动结果 startActivityForResult

onActivityResult如果您想从您开始的活动中获得结果,您需要听取意见startActivityForResult。要做到这一点,您必须扩展BaseActivityEventListener或实施ActivityEventListener。前者是首选,因为它对API更改具有更强的适应性。然后,你需要在模块的构造函数中注册监听器,

reactContext.addActivityEventListener(mActivityResultListener

现在您可以onActivityResult通过执行以下方法来收听:

@Override public void onActivityResult( final Activity activity, final int requestCode, final int resultCode, final Intent intent) { // Your logic here }

我们将实现一个简单的图像选择器来演示这一点。图像选择器会将该方法暴露pickImage给JavaScript,它将在调用时返回图像的路径。

public class ImagePickerModule extends ReactContextBaseJavaModule { private static final int IMAGE_PICKER_REQUEST = 467081; private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"; private static final String E_PICKER_CANCELLED = "E_PICKER_CANCELLED"; private static final String E_FAILED_TO_SHOW_PICKER = "E_FAILED_TO_SHOW_PICKER"; private static final String E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND"; private Promise mPickerPromise; private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { if (requestCode == IMAGE_PICKER_REQUEST) { if (mPickerPromise != null) { if (resultCode == Activity.RESULT_CANCELED) { mPickerPromise.reject(E_PICKER_CANCELLED, "Image picker was cancelled" } else if (resultCode == Activity.RESULT_OK) { Uri uri = intent.getData( if (uri == null) { mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "No image data found" } else { mPickerPromise.resolve(uri.toString() } } mPickerPromise = null; } } } }; public ImagePickerModule(ReactApplicationContext reactContext) { super(reactContext // Add the listener for `onActivityResult` reactContext.addActivityEventListener(mActivityEventListener } @Override public String getName() { return "ImagePickerModule"; } @ReactMethod public void pickImage(final Promise promise) { Activity currentActivity = getCurrentActivity( if (currentActivity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist" return; } // Store the promise to resolve/reject when picker returns data mPickerPromise = promise; try { final Intent galleryIntent = new Intent(Intent.ACTION_PICK galleryIntent.setType("image/*" final Intent chooserIntent = Intent.createChooser(galleryIntent, "Pick an image" currentActivity.startActivityForResult(chooserIntent, IMAGE_PICKER_REQUEST } catch (Exception e) { mPickerPromise.reject(E_FAILED_TO_SHOW_PICKER, e mPickerPromise = null; } } }

监听LifeCycle事件

监听活动的LifeCycle事件(例如onResumeonPause等等)与我们的实施方式非常相似ActivityEventListener。该模块必须执行LifecycleEventListener。然后,你需要在模块的构造函数中注册一个监听器,

reactContext.addLifecycleEventListener(this

现在您可以通过实施以下方法来收听活动的LifeCycle事件:

@Override public void onHostResume() { // Activity `onResume` } @Override public void onHostPause() { // Activity `onPause` } @Override public void onHostDestroy() { // Activity `onDestroy` }