教程:创建原生视图

关于使用 Expo Modules API 创建渲染 WebView 的原生视图的教程。


在本教程中,你将构建一个示例模块,其中包含一个渲染 WebView 的原生视图。对于 Android,你将使用 WebView 组件,对于 iOS,则使用 WKWebView。Web 支持可以使用 iframe 实现,这部分留作练习给你自己完成。

🌐 In this tutorial, you'll build an example module with a native view that renders a WebView. For Android, you'll use the WebView component, and for iOS, WKWebView. Web support can be implemented using an iframe and is left as an exercise for you.

1

初始化一个新模块

🌐 Initialize a new module

通过运行以下命令创建一个新模块,并将示例模块命名为 expo-web-view

🌐 Create a new module by running the following command and name the example module expo-web-view:

Terminal
npx create-expo-module expo-web-view

信息 由于这是一个示例库且不会发布,请在所有提示中按 回车 以接受默认值。

2

设置工作区

🌐 Set up workspace

清理默认模块,通过删除以下文件从零开始:

🌐 Clean up the default module to start with a clean slate by deleting the following files:

Terminal
cd expo-web-view
rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts
rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts

找到以下文件,并用提供的最小化模板替换它们:

🌐 Locate the following files and replace them with the provided minimal boilerplate:

android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
package expo.modules.webview import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoWebViewModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoWebView") View(ExpoWebView::class) {} } }
ios/ExpoWebViewModule.swift
import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) {} } }
src/ExpoWebView.tsx
import { ViewProps } from 'react-native'; import { requireNativeViewManager } from 'expo-modules-core'; import * as React from 'react'; export type Props = ViewProps; const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); export default function ExpoWebView(props: Props) { return <NativeView {...props} />; }
src/index.ts
export { default as WebView, Props as WebViewProps } from './ExpoWebView';
example/App.tsx
import { WebView } from 'expo-web-view'; export default function App() { return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />; }

3

运行示例项目

🌐 Run the example project

为了确保一切正常工作,启动 TypeScript 编译器以监视更改并重新构建模块的 JavaScript:

🌐 To ensure everything is working, start the TypeScript compiler to watch for changes and rebuild the module's JavaScript:

Terminal
# Run this in the root of the project to start the TypeScript compiler
npm run build
Terminal
# Navigate to the example directory
cd example
# Run the example app on Android
npx expo run:android
# Run the example app on iOS
npx expo run:ios

你现在应该能看到一个空白的紫色屏幕。虽然这并不特别吸引人,但这是一个好的开始。接下来,把它变成一个 WebView。

🌐 You should now see a blank purple screen. While it's not very exciting, it's a good start. Next, turn it into a WebView.

4

将系统 WebView 添加为子视图

🌐 Add the system WebView as a subview

将系统 WebView 作为 ExpoWebView 的子视图添加,并使用硬编码的 URL。ExpoWebView 类继承自 ExpoViewExpoView 又继承自 React Native 的 RCTView,最终在 Android 上继承 View,在 iOS 上继承 UIView

🌐 Add the system WebView with a hardcoded URL as a subview of ExpoWebView. The ExpoWebView class extends ExpoView, which extends RCTView from React Native, and eventually extends View on Android and UIView on iOS.

确保 WebView 子视图使用与 ExpoWebView 相同的布局,ExpoWebView 的布局是由 React Native 的布局引擎计算的。

🌐 Ensure that the WebView subview uses the same layout as ExpoWebView, whose layout is calculated by React Native's layout engine.

安卓视图

🌐 Android view

在 Android 上,使用 LayoutParams 将 WebView 的布局设置为匹配 ExpoWebView 布局。你可以在实例化 WebView 时执行此操作。

🌐 On Android, use LayoutParams to set the WebView's layout to match the ExpoWebView layout. You can do this when you instantiate the WebView.

android/src/main/java/expo/modules/webview/ExpoWebView.kt
package expo.modules.webview import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import expo.modules.kotlin.AppContext import expo.modules.kotlin.views.ExpoView class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { internal val webView = WebView(context).also { it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) it.webViewClient = object : WebViewClient() {} addView(it) it.loadUrl("https://expo.nodejs.cn/modules/") } }

iOS 视图

🌐 iOS view

在 iOS 上,将 clipsToBounds 设置为 true,并确保 WebView 的 framelayoutSubviewsExpoWebView 的边界匹配。当视图创建时会调用 init 方法,当布局发生变化时会调用 layoutSubviews

🌐 On iOS, set clipsToBounds to true and ensure the WebView's frame matches the bounds of ExpoWebView in layoutSubviews. The init method is called when the view is created, and layoutSubviews is called when the layout changes.

ios/ExpoWebView.swift
import ExpoModulesCore import WebKit class ExpoWebView: ExpoView { let webView = WKWebView() required init(appContext: AppContext? = nil) { super.init(appContext: appContext) clipsToBounds = true addSubview(webView) let url = URL(string:"https://expo.nodejs.cn/modules/")! let urlRequest = URLRequest(url:url) webView.load(urlRequest) } override func layoutSubviews() { webView.frame = bounds } }

示例应用

🌐 Example app

无需进行更改。使用以下命令重建并运行应用:

🌐 No changes are required. Rebuild and run the app using the following commands:

Terminal
# Prebuild the example app with the --clean flag to ensure a clean build
npx expo prebuild --clean
# Run the example app on Android
npx expo run:android
# Run the example app on iOS
npx expo run:ios

之后,你将看到渲染后的 Expo 模块 API 概览页面。如果更改没有显示,请尝试重新安装应用。

🌐 After that, you'll see the Expo Modules API overview page rendered. If the changes aren't reflected, try reinstalling the app.

5

添加一个属性来设置 URL

🌐 Add a prop to set the URL

要在视图上设置属性,请在 ExpoWebViewModule 中定义属性名称和设置方法。在这种情况下,你可以直接访问 webView 属性以方便操作。然而,在实际应用中,应将逻辑保存在 ExpoWebView 类中,以尽量减少 ExpoWebViewModule 对其内部细节的了解。

🌐 To set a prop on the view, define the prop name and setter inside ExpoWebViewModule. In this case, you can access the webView property directly for convenience. However, in real-world scenarios, keep the logic inside the ExpoWebView class to minimize how much ExpoWebViewModule knows about its internals.

使用 Prop 定义组件 来定义 prop。在 prop 设置块中,你可以访问视图和 prop。指定 URL 的类型为 URL —— Expo 模块 API 会将字符串转换为本地的 URL 类型。

🌐 Use the Prop definition component to define the prop. In the prop setter block, you can access both the view and the prop. Specify that the URL is of type URL — the Expo modules API will convert strings to the native URL type.

安卓模块

🌐 Android module

android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
package expo.modules.webview import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.net.URL class ExpoWebViewModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoWebView") View(ExpoWebView::class) { Prop("url") { view: ExpoWebView, url: URL? -> view.webView.loadUrl(url.toString()) } } } }

iOS 模块

🌐 iOS module

ios/ExpoWebViewModule.swift
import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) { Prop("url") { (view, url: URL) in if view.webView.url != url { let urlRequest = URLRequest(url: url) view.webView.load(urlRequest) } } } } }

TypeScript 模块

🌐 TypeScript module

接下来,将 url 属性添加到 Props 类型中。

🌐 Next, add the url prop to the Props type.

src/ExpoWebView.tsx
import { ViewProps } from 'react-native'; import { requireNativeViewManager } from 'expo-modules-core'; import * as React from 'react'; export type Props = { url?: string; } & ViewProps; const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); export default function ExpoWebView(props: Props) { return <NativeView {...props} />; }

示例应用

🌐 Example app

最后,在示例应用中向你的 WebView 组件传递一个 URL

🌐 Finally, pass a URL to your WebView component in the example app.

example/App.tsx
import { WebView } from 'expo-web-view'; export default function App() { return <WebView style={{ flex: 1 }} url="https://expo.dev" />; }

重建示例应用:

🌐 Rebuild the example app:

Terminal
npx expo prebuild --clean
# Run the example app on Android
npx expo run:android
# Run the example app on iOS
npx expo run:ios

之后,你将在 WebView 中看到 Expo 首页

🌐 After that, you'll see the Expo homepage in the WebView.

6

添加一个事件以在页面加载完成时通知

🌐 Add an event to notify when the page has loaded

查看回调 允许开发者监听组件上的事件。它们通常通过组件的属性注册,例如:<Image onLoad={...} />。使用 事件定义组件 为你的 WebView 定义一个事件。将其命名为 onLoad

Android 视图和模块

🌐 Android view and module

在 Android 上,重写 onPageFinished 函数。然后,调用你在模块中定义的 onLoad 事件处理器。

🌐 On Android, override the onPageFinished function. Then, call the onLoad event handler that you defined in the module.

android/src/main/java/expo/modules/webview/ExpoWebView.kt
package expo.modules.webview import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { private val onLoad by EventDispatcher() internal val webView = WebView(context).also { it.layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT ) it.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { onLoad(mapOf("url" to url)) } } addView(it) } }

ExpoWebViewModule 中指出 View 有一个 onLoad 事件。

🌐 Indicate in ExpoWebViewModule that the View has an onLoad event.

android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
package expo.modules.webview import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.net.URL class ExpoWebViewModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoWebView") View(ExpoWebView::class) { Events("onLoad") Prop("url") { view: ExpoWebView, url: URL? -> view.webView.loadUrl(url.toString()) } } } }

iOS 视图和模块

🌐 iOS view and module

在 iOS 上,实现 webView(_:didFinish:) 并让 ExpoWebView 继承 WKNavigationDelegate。然后,从该代理方法中调用 onLoad

🌐 On iOS, implement webView(_:didFinish:) and make ExpoWebView extend WKNavigationDelegate. Then, call onLoad from that delegate method.

ios/ExpoWebView.swift
import ExpoModulesCore import WebKit class ExpoWebView: ExpoView, WKNavigationDelegate { let webView = WKWebView() let onLoad = EventDispatcher() required init(appContext: AppContext? = nil) { super.init(appContext: appContext) clipsToBounds = true webView.navigationDelegate = self addSubview(webView) } override func layoutSubviews() { webView.frame = bounds } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { if let url = webView.url { onLoad([ "url": url.absoluteString ]) } } }

ExpoWebViewModule 中指出 View 有一个 onLoad 事件。

🌐 Indicate in ExpoWebViewModule that the View has an onLoad event.

ios/ExpoWebViewModule.swift
import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) { Events("onLoad") Prop("url") { (view, url: URL) in if view.webView.url != url { let urlRequest = URLRequest(url: url) view.webView.load(urlRequest) } } } } }

TypeScript 模块

🌐 TypeScript module

事件的有效负载包含在事件的 nativeEvent 属性中。要从 onLoad 事件访问 url,请读取 event.nativeEvent.url

🌐 Event payloads are included within the nativeEvent property of the event. To access the url from the onLoad event, read event.nativeEvent.url.

src/ExpoWebView.tsx
import { ViewProps } from 'react-native'; import { requireNativeViewManager } from 'expo-modules-core'; import * as React from 'react'; export type OnLoadEvent = { url: string; }; export type Props = { url?: string; onLoad?: (event: { nativeEvent: OnLoadEvent }) => void; } & ViewProps; const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); export default function ExpoWebView(props: Props) { return <NativeView {...props} />; }

示例应用

🌐 Example app

更新示例应用,以在页面加载完成时显示警报。复制以下代码,然后重新构建并运行你的应用,你就会看到警报!

🌐 Update the example app to show an alert when the page has loaded. Copy the following code, then rebuild and run your app, and you'll see the alert!

example/App.tsx
import { WebView } from 'expo-web-view'; export default function App() { return ( <WebView style={{ flex: 1 }} url="https://expo.dev" onLoad={event => alert(`loaded ${event.nativeEvent.url}`)} /> ); }

7

加分项:为它构建一个网页浏览器界面

🌐 Bonus: Build a web browser UI around it

既然你已经有了一个 WebView,就围绕它构建一个网页浏览器界面。尝试重建一个浏览器界面,并可以根据需要添加新的本地功能(例如,支持后退或刷新按钮)。如果需要灵感,可以参考下面的示例。

🌐 Now that you have a WebView, build a web browser UI around it. Try rebuilding a browser UI, and feel free to add new native capabilities as needed (for example, support for back or reload buttons). If you need inspiration, see the example below.

example/App.tsx
App.tsx
import { useState } from 'react'; import { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native'; import { WebView } from 'expo-web-view'; export default function App() { const [inputUrl, setInputUrl] = useState('https://expo.nodejs.cn/modules/'); const [url, setUrl] = useState(inputUrl); const [isLoading, setIsLoading] = useState(true); return ( <View style={{ flex: 1, paddingTop: Platform.OS === 'ios' ? 80 : 30 }}> <TextInput value={inputUrl} onChangeText={setInputUrl} returnKeyType="go" autoCapitalize="none" onSubmitEditing={() => { if (inputUrl !== url) { setUrl(inputUrl); setIsLoading(true); } }} keyboardType="url" style={{ color: '#fff', backgroundColor: '#000', borderRadius: 10, marginHorizontal: 10, paddingHorizontal: 20, height: 60, }} /> <WebView url={url.startsWith('https://') || url.startsWith('http://') ? url : `https://${url}`} onLoad={() => setIsLoading(false)} style={{ flex: 1, marginTop: 20 }} /> <LoadingView isLoading={isLoading} /> </View> ); } function LoadingView({ isLoading }: { isLoading: boolean }) { if (!isLoading) { return null; } return ( <View style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, backgroundColor: 'rgba(0,0,0,0.5)', paddingBottom: 10, justifyContent: 'center', alignItems: 'center', flexDirection: 'row', }}> <ActivityIndicator animating={isLoading} color="#fff" style={{ marginRight: 10 }} /> <Text style={{ color: '#fff' }}>Loading...</Text> </View> ); }

恭喜!你已经创建了第一个带有 Android 和 iOS 原生视图的 Expo 模块。

🌐 Congratulations! You've created your first Expo module with a native view for Android and iOS.

下一步

🌐 Next steps

Expo 模块 API 参考

使用 Kotlin 和 Swift 创建原生模块。

教程:创建原生视图

关于使用 Expo Modules API 创建保留设置的原生模块的教程。