教程:创建原生视图

编辑页面

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


For the complete documentation index, see llms.txt. Use this file to discover all available pages.

在本教程中,你将构建一个带有原生视图的示例模块,该视图会渲染一个 WebView。对于 Android,你将使用 WebView 组件;对于 iOS,你将使用 WKWebView。Web 支持可以使用 iframe 来实现,这部分留给你作为练习。

1

初始化一个新模块

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

Terminal
npx create-expo-module expo-web-view
由于这是一个示例库,不会发布,因此在所有提示中按 Return 键即可接受默认值。

2

设置工作区

通过删除以下文件来清理默认模块,以便从一个干净的状态开始:

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

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

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

运行示例项目

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

Terminal
# 在项目根目录中运行此命令以启动 TypeScript 编译器
npm run build
Terminal
# 切换到示例目录
cd example
# 在 Android 上运行示例应用
npx expo run:android
# 在 iOS 上运行示例应用
npx expo run:ios

现在你应该能看到一个空白的紫色屏幕。虽然这还不算很有意思,但这已经是一个不错的开始。接下来,把它变成一个 WebView。

4

将系统 WebView 作为子视图添加

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

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

Android 视图

在 Android 上,使用 LayoutParams 将 WebView 的布局设置为与 ExpoWebView 的布局匹配。你可以在实例化 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://docs.expo.dev/modules/") } }

iOS 视图

在 iOS 上,将 clipsToBounds 设置为 true,并确保在 layoutSubviews 中 WebView 的 frameExpoWebView 的 bounds 匹配。init 方法在视图创建时调用,而 layoutSubviews 在布局发生变化时调用。

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://docs.expo.dev/modules/")! let urlRequest = URLRequest(url:url) webView.load(urlRequest) } override func layoutSubviews() { webView.frame = bounds } }

示例应用

这里不需要做任何更改。使用以下命令重新构建并运行应用:

Terminal
# 使用 --clean 标志预构建示例应用,以确保构建干净
npx expo prebuild --clean
# 在 Android 上运行示例应用
npx expo run:android
# 在 iOS 上运行示例应用
npx expo run:ios

之后,你会看到渲染出的 Expo Modules API 概览页面。如果更改没有生效,请尝试重新安装应用。

5

添加一个用于设置 URL 的 prop

要在视图上设置 prop,请在 ExpoWebViewModule 中定义 prop 名称和 setter。在这个例子中,为了方便,你可以直接访问 webView 属性。不过在实际场景中,应将逻辑保留在 ExpoWebView 类内部,以尽量减少 ExpoWebViewModule 对其内部实现的了解。

使用 Prop 定义组件 来定义该 prop。在 prop setter 块中,你可以同时访问视图和 prop。指定 URL 的类型为 URL — Expo 模块 API 会将字符串转换为原生 URL 类型。

Android 模块

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/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 模块

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

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} />; }

示例应用

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

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

重新构建示例应用:

Terminal
npx expo prebuild --clean
# 在 Android 上运行示例应用
npx expo run:android
# 在 iOS 上运行示例应用
npx expo run:ios

之后,你会在 WebView 中看到 Expo 主页

6

添加一个事件来通知页面已加载

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

Android 视图和模块

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

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 事件。

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 上,实现 webView(_:didFinish:),并让 ExpoWebView 继承 WKNavigationDelegate。然后从该代理方法中调用 onLoad

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 事件。

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 模块

事件载荷包含在事件的 nativeEvent 属性中。要从 onLoad 事件中访问 url,请读取 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.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:围绕它构建一个网页浏览器 UI

现在你已经有了一个 WebView,可以围绕它构建一个网页浏览器 UI。尝试重建一个浏览器界面,并在需要时自由添加新的原生能力(例如支持返回或刷新按钮)。如果你需要灵感,请查看下面的示例。

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://docs.expo.dev/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' }}>加载中...</Text> </View> ); }

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

下一步

Expo 模块 API 参考

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

教程:创建原生模块

一个关于使用 Expo Modules API 创建可持久化设置的原生模块的教程。