教程:创建原生视图

编辑页面

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


For the complete documentation index, see llms.txt. Use this 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 definition component 来定义该 prop。在 prop setter 代码块中,你可以同时访问视图和 prop。指定 URL 的类型为 URL — Expo modules 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={...} />。使用 Events definition component 为你的 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 创建可持久化设置的原生模块的教程。