跳到主要内容
版本:0.86

原生组件

如果你想构建全新的 React Native 组件,用来封装一个 Host Component,例如 Android 上独特类型的 CheckBox,或者 iOS 上的 UIButton,你应该使用 Fabric 原生组件。

本指南将通过实现一个 web view 组件来展示如何构建 Fabric 原生组件。实现步骤如下:

  1. 使用 Flow 或 TypeScript 定义 JavaScript 规范。
  2. 配置依赖管理系统,以根据提供的规范生成代码并实现自动链接。
  3. 实现原生代码。
  4. 在应用中使用该组件。

你需要一个由普通模板生成的应用来使用该组件:

npx @react-native-community/cli@latest init Demo --install-pods false

创建 WebView 组件​

本指南将展示如何创建一个 Web View 组件。我们将使用 Android 的 WebView 组件,以及 iOS 的 WKWebView 组件来创建它。

让我们先创建用于存放组件代码的文件夹结构:

mkdir -p Demo/{specs,android/app/src/main/java/com/webview}

这会得到如下的工作目录布局:

Demo
├── android/app/src/main/java/com/webview
└── ios
└── specs
  • android/app/src/main/java/com/webview 文件夹将包含我们的 Android 代码。
  • ios 文件夹将包含我们的 iOS 代码。
  • specs 文件夹将包含 Codegen 的规范文件。

1. 为 Codegen 定义规范​

你的规范必须使用 TypeScript 或 Flow 定义(更多细节请参见 Codegen 文档)。Codegen 会利用它生成 C++、Objective-C++ 和 Java 代码,把你的平台代码连接到 React 运行的 JavaScript 运行时中。

规范文件必须命名为 <MODULE_NAME>NativeComponent.{ts|js} 才能与 Codegen 配合使用。后缀 NativeComponent 不仅仅是约定,它实际上会被 Codegen 用来检测规范文件。

WebView 组件使用以下规范:

Demo/specs/WebViewNativeComponent.ts
import type {
CodegenTypes,
HostComponent,
ViewProps,
} from 'react-native';
import {codegenNativeComponent} from 'react-native';

type WebViewScriptLoadedEvent = {
result: 'success' | 'error';
};

export interface NativeProps extends ViewProps {
sourceURL?: string;
onScriptLoaded?: CodegenTypes.BubblingEventHandler<WebViewScriptLoadedEvent> | null;
}

export default codegenNativeComponent<NativeProps>(
'CustomWebView',
) as HostComponent<NativeProps>;

该规范由三个主要部分组成,不包括导入部分:

  • WebViewScriptLoadedEvent 是一个辅助数据类型,用于定义事件需要从原生传递到 JavaScript 的数据。
  • NativeProps 是我们可以设置到组件上的属性定义。
  • codegenNativeComponent 语句允许我们为自定义组件生成代码,并定义一个用于匹配原生实现的组件名称。

与原生模块一样,你可以在 specs/ 目录中包含多个规范文件。有关可使用的类型以及它们映射到的平台类型的更多信息,请参见 附录。

2. 配置 Codegen 运行​

React Native 的 Codegen 工具会使用该规范为我们生成平台特定的接口和样板代码。为此,Codegen 需要知道去哪里找到我们的规范,以及如何处理它。更新你的 package.json,加入:

JSON
"start": "react-native start",
"test": "jest"
},
"codegenConfig": {
"name": "AppSpec",
"type": "components",
"jsSrcsDir": "specs",
"android": {
"javaPackageName": "com.webview"
},
"ios": {
"componentProvider": {
"CustomWebView": "RCTWebView"
}
}
},
"dependencies": {

完成 Codegen 的所有接线后,我们需要准备原生代码,以便接入生成的代码。

请注意,对于 iOS,我们是以声明式方式将 spec 导出的 JS 组件名称(CustomWebView)映射到将以原生方式实现该组件的 iOS 类。

2. 构建你的原生代码​

现在该编写原生平台代码了,这样当 React 需要渲染一个视图时,平台就能创建正确的原生视图并将其渲染到屏幕上。

你应该同时处理 Android 和 iOS 平台。

备注

本指南展示了如何创建一个仅适用于新架构的原生组件。如果你需要同时支持新架构和旧架构,请参考我们的 向后兼容指南。

现在是时候编写一些 Android 平台代码,以便能够渲染 web view 了。你需要遵循的步骤是:

  • 运行 Codegen
  • 编写 ReactWebView 的代码
  • 编写 ReactWebViewManager 的代码
  • 编写 ReactWebViewPackage 的代码
  • 在应用中注册 ReactWebViewPackage

1. 通过 Gradle 运行 Codegen​

运行一次以生成你所使用的 IDE 可以使用的样板代码。

Demo/
cd android
./gradlew generateCodegenArtifactsFromSchema

Codegen 将生成你需要实现的 ViewManager 接口,以及 web view 的 ViewManager delegate。

2. 编写 ReactWebView​

ReactWebView 是一个组件,它封装了 Android 原生视图,React Native 在使用我们自定义组件时会渲染这个视图。

在 android/src/main/java/com/webview 文件夹中创建一个 ReactWebView.java 或 ReactWebView.kt 文件,并使用以下代码:

Demo/android/src/main/java/com/webview/ReactWebView.java
package com.webview;

import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.Event;

public class ReactWebView extends WebView {
public ReactWebView(Context context) {
super(context);
configureComponent();
}

public ReactWebView(Context context, AttributeSet attrs) {
super(context, attrs);
configureComponent();
}

public ReactWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
configureComponent();
}

private void configureComponent() {
this.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
this.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
emitOnScriptLoaded(OnScriptLoadedEventResult.success);
}
});
}

public void emitOnScriptLoaded(OnScriptLoadedEventResult result) {
ReactContext reactContext = (ReactContext) context;
int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, getId());
WritableMap payload = Arguments.createMap();
payload.putString("result", result.name());

OnScriptLoadedEvent event = new OnScriptLoadedEvent(surfaceId, getId(), payload);
if (eventDispatcher != null) {
eventDispatcher.dispatchEvent(event);
}
}

public enum OnScriptLoadedEventResult {
success,
error
}

private class OnScriptLoadedEvent extends Event<OnScriptLoadedEvent> {
private final WritableMap payload;

OnScriptLoadedEvent(int surfaceId, int viewId, WritableMap payload) {
super(surfaceId, viewId);
this.payload = payload;
}

@Override
public String getEventName() {
return "onScriptLoaded";
}

@Override
public WritableMap getEventData() {
return payload;
}
}
}

ReactWebView 继承了 Android 的 WebView,因此你可以轻松复用平台上已经定义好的所有属性。

该类定义了三个 Android 构造函数,但将它们的实际实现委托给了私有的 configureComponent 函数。这个函数负责初始化所有组件特有的属性:在这里你设置了 WebView 的布局,并定义了用于自定义 WebView 行为的 WebClient。在这段代码中,ReactWebView 通过实现 WebClient 的 onPageFinished 方法,在页面加载完成时发出一个事件。

然后,代码定义了一个辅助函数来真正发出事件。要发出一个事件,你需要:

  • 获取对 ReactContext 的引用;
  • 获取你正在展示的视图的 surfaceId;
  • 获取与该视图关联的 eventDispatcher 的引用;
  • 使用 WritableMap 对象构建该事件的负载;
  • 创建你需要发送到 JavaScript 的事件对象;
  • 调用 eventDispatcher.dispatchEvent 发送该事件。

文件最后一部分包含了你需要发送事件的数据类型定义:

  • OnScriptLoadedEventResult,表示 OnScriptLoaded 事件的可能结果。
  • 实际的 OnScriptLoadedEvent,它需要扩展 React Native 的 Event 类。

3. 编写 WebViewManager​

WebViewManager 是连接 React Native 运行时与原生视图的类。

当 React 接收到来自应用的指令,要渲染某个特定组件时,React 会使用已注册的视图管理器来创建视图并传递所有必需的属性。

下面是 ReactWebViewManager 的代码。

Demo/android/src/main/java/com/webview/ReactWebViewManager.java
package com.webview;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.CustomWebViewManagerInterface;
import com.facebook.react.viewmanagers.CustomWebViewManagerDelegate;

import java.util.HashMap;
import java.util.Map;

@ReactModule(name = ReactWebViewManager.REACT_CLASS)
class ReactWebViewManager extends SimpleViewManager<ReactWebView> implements CustomWebViewManagerInterface<ReactWebView> {
private final CustomWebViewManagerDelegate<ReactWebView, ReactWebViewManager> delegate =
new CustomWebViewManagerDelegate<>(this);

@Override
public ViewManagerDelegate<ReactWebView> getDelegate() {
return delegate;
}

@Override
public String getName() {
return REACT_CLASS;
}

@Override
public ReactWebView createViewInstance(ThemedReactContext context) {
return new ReactWebView(context);
}

@ReactProp(name = "sourceUrl")
@Override
public void setSourceURL(ReactWebView view, String sourceURL) {
if (sourceURL == null) {
view.emitOnScriptLoaded(ReactWebView.OnScriptLoadedEventResult.error);
return;
}
view.loadUrl(sourceURL, new HashMap<>());
}

public static final String REACT_CLASS = "CustomWebView";

@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
Map<String, Object> map = new HashMap<>();
Map<String, Object> bubblingMap = new HashMap<>();
bubblingMap.put("phasedRegistrationNames", new HashMap<String, String>() {{
put("bubbled", "onScriptLoaded");
put("captured", "onScriptLoadedCapture");
}});
map.put("onScriptLoaded", bubblingMap);
return map;
}
}

ReactWebViewManager 继承了 React 的 SimpleViewManager 类,并实现了由 Codegen 生成的 CustomWebViewManagerInterface。

它持有 CustomWebViewManagerDelegate 的引用,这也是由 Codegen 生成的另一个元素。

然后它重写了 getName 函数,该函数必须返回与 spec 中 codegenNativeComponent 函数调用所使用的名称相同的值。

createViewInstance 函数负责实例化一个新的 ReactWebView。

接着,ViewManager 需要定义 React 的所有组件属性将如何更新原生视图。在这个示例中,你需要决定如何处理 React 会在 WebView 上设置的 sourceURL 属性。

最后,如果组件可以发出事件,你需要通过重写 getExportedCustomBubblingEventTypeConstants 来映射冒泡事件的事件名,或者通过 getExportedCustomDirectEventTypeConstants 来映射直接事件的事件名。

4. 编写 ReactWebViewPackage​

和原生模块一样,原生组件也需要实现 ReactPackage 类。这是一个你可以用来在 React Native 运行时中注册组件的对象。

下面是 ReactWebViewPackage 的代码:

Demo/android/src/main/java/com/webview/ReactWebViewPackage.java
package com.webview;

import com.facebook.react.BaseReactPackage;
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;
import com.facebook.react.uimanager.ViewManager;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReactWebViewPackage extends BaseReactPackage {
@Override
public List<ViewManager<?, ?>> createViewManagers(ReactApplicationContext reactContext) {
return Collections.singletonList(new ReactWebViewManager(reactContext));
}

@Override
public NativeModule getModule(String s, ReactApplicationContext reactApplicationContext) {
if (ReactWebViewManager.REACT_CLASS.equals(s)) {
return new ReactWebViewManager(reactApplicationContext);
}
return null;
}

@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put(ReactWebViewManager.REACT_CLASS, new ReactModuleInfo(
ReactWebViewManager.REACT_CLASS, // name
ReactWebViewManager.REACT_CLASS, // className
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCxxModule
true // isTurboModule
));
return map;
}
};
}
}

ReactWebViewPackage 继承了 BaseReactPackage,并实现了正确注册我们的组件所需的所有方法。

  • createViewManagers 方法是工厂方法,用于创建管理自定义视图的 ViewManager。
  • getModule 方法会根据 React Native 需要渲染的 View 返回合适的 ViewManager。
  • getReactModuleInfoProvider 提供在运行时注册模块所需的全部信息,

5. 在应用中注册 ReactWebViewPackage​

最后,你需要在应用中注册 ReactWebViewPackage。我们通过修改 MainApplication 文件来实现这一点,即将 ReactWebViewPackage 添加到 getPackages 函数返回的包列表中。

Demo/app/src/main/java/com/demo/MainApplication.kt
package com.demo

import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.webview.ReactWebViewPackage

class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(ReactWebViewPackage())
}

override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)

override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
load()
}
}
}

3. 使用你的原生组件​

最后,你可以在应用中使用这个新组件。将生成的 App.tsx 更新为:

Demo/App.tsx
import {Alert, StyleSheet, View} from 'react-native';
import WebView from './specs/WebViewNativeComponent';

function App(): React.JSX.Element {
return (
<View style={styles.container}>
<WebView
sourceURL="https://react.dev/"
style={styles.webview}
onScriptLoaded={() => {
Alert.alert('Page Loaded');
}}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
alignContent: 'center',
},
webview: {
width: '100%',
height: '100%',
},
});

export default App;

这段代码创建了一个应用,使用我们创建的新 WebView 组件来加载 react.dev 网站。

当网页加载完成时,应用还会显示一个提示框。

4. 使用 WebView 组件运行你的应用​

yarn run android
AndroidiOS