跳到主要内容
新架构实战课 实操 + 基建 + 原理全维度包揽,抢先掌握 React Native 新架构精髓 立即查看 >

原生组件

如果你想构建一个 新架构 的 React Native 组件,该组件可以包装一个 Host Component,例如 Android 上的 CheckBox,或者 iOS 上的 UIButton,你应该使用 Fabric 原生组件。

本指南将以实现一个 web 视图组件为例,展示如何构建 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
└── spec
  • android/app/src/main/java/com/webview 文件夹是存放 Android 代码的文件夹。
  • ios 文件夹是存放 iOS 代码的文件夹。
  • spec 文件夹是存放 Codegen 规范文件的文件夹。

1. 定义 Codegen 规范

你的规范必须使用 TypeScriptFlow 定义(更多详情请参阅 Codegen 文档)。这是由 Codegen 生成 C++、Objective-C++ 和 Java 代码,以连接你的平台代码到 React 运行的 JavaScript 运行时。

规范文件必须命名为 <MODULE_NAME>NativeComponent.{ts|js} 才能被 Codegen 识别。NativeComponent 后缀不仅是一个约定,实际上是由 Codegen 用于检测规范文件。

使用以下规范文件来创建 WebView 组件:

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

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

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

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

该规范文件由三部分组成,不包括导入:

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

与原生模块一样,你可以在 specs/ 目录中拥有多个规范文件。更多信息请参阅 附录

2. 配置 Codegen 运行

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

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

配置好 Codegen 后,我们需要准备原生代码以连接到生成的代码。

2. 构建原生代码

现在,是时候编写原生平台代码,以便当 React 需要渲染视图时,平台可以创建正确的原生视图并在屏幕上渲染它。

你应该分别在 Android 和 iOS 平台上工作。

note

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

现在,是时候编写一些 Android 平台代码,以便能够渲染 web 视图。以下是需要的步骤:

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

1. 使用 Gradle 运行 Codegen

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

Demo/
cd android
./gradlew generateCodegenArtifactsFromSchema

Codegen 将生成你需要实现 ViewManager 接口和 ViewManager 委托的 web 视图。

2. 编写 ReactWebView

ReactWebView 是包装 Android 原生视图的组件,React Native 将在使用自定义组件时渲染它。

android/src/main/java/com/webview 文件夹中创建一个 ReactWebView.javaReactWebView.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 在页面加载完成后通过实现 WebClientonPageFinished 方法来发出事件。

然后,代码定义了一个实际发出事件的帮助函数。要发出事件,你必须:

  • 获取 ReactContext 的引用;
  • retrieve the surfaceId of the view that you are presenting;
  • grab a reference to the eventDispatcher associated with the view;
  • build the payload for the event using a WritableMap object;
  • create the event object that you need to send to JavaScript;
  • call the eventDispatcher.dispatchEvent to send the event.

The last part of the file contains the definition of the data types you need to send the event:

  • The OnScriptLoadedEventResult, with the possible outcomes of the OnScriptLoaded event.
  • The actual ``OnScriptLoadedEventthat needs to extend the React Native'sEvent` class.

3. Write the WebViewManager

The WebViewManager is the class that connects the React Native runtime with the native view.

When React receives the instruction from the app to render a specific component, React uses the registered view manager to create the view and to pass all the required properties.

This is the code of the 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;
}
}

The ReactWebViewManager extends the SimpleViewManager class from React and implements the CustomWebViewManagerInterface, generated by Codegen.

It holds a reference of the CustomWebViewManagerDelegate, another element generated by Codegen.

It then overrides the getName function, which must return the same name used in the spec's codegenNativeComponent function call.

The createViewInstance function is responsible to instantiate a new ReactWebView.

Then, the ViewManager needs to define how all the React's compnoents props will update the native view. In the example, you need to decide how to handle the sourceURL property that React will set on the WebView.

Finally, if the component can emit an event, you need to map the event name by overriding the getExportedCustomBubblingEventTypeConstants for bubbling events, or the getExportedCustomDirectEventTypeConstants for direct events.

4. Write the ReactWebViewPackage

As you do with Native Modules, Native Components also need to implement the ReactPackage class. This is an object that you can use to register the component in the React Native runtime.

This is the code for the ReactWebViewPackage:

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

import com.facebook.react.TurboReactPackage;
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 TurboReactPackage {
@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> get() {
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;
}
};
}
}

The ReactWebViewPackage extends the TurboReactPackage and implements all the methods required to properly register our component.

  • the createViewManagers method is the factory method that creates the ViewManager that manage the custom views.
  • the getModule method returns the proper ViewManager depending on the View that React Native needs to render.
  • the getReactModuleInfoProvider provides all the information required when registering the module in the runtime,

5. Register the ReactWebViewPackage in the application

Finally, you need to register the ReactWebViewPackage in the application. We do that by modifying the MainApplication file by adding the ReactWebViewPackage to the list of packages returned by the getPackages function.

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 React from 'react';
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 网站的应用。

该应用还显示了一个当网页加载完成时弹出的警告。

5. 运行应用使用 WebView 组件

yarn run android
AndroidiOS