教程:创建本机视图

编辑

创建一个本机视图的教程,该视图使用 Expo 模块 API 渲染 WebView。


在本教程中,您将构建一个示例模块,其中包含一个渲染 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 添加为子视图

将系统 WebView 以硬编码的 URL 添加为 ExpoWebView 的子视图。ExpoWebView 类扩展了 ExpoView,它扩展了来自 React Native 的 RCTView,最终在 Android 上扩展为 View,在 iOS 上扩展为 UIView

确保 WebView 子视图使用与 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,并确保 WebView 的 framelayoutSubviews 中与 ExpoWebView 的边界匹配。当创建视图时调用 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

要在视图上设置属性,请在 ExpoWebViewModule 中定义属性名称和设置器。在这种情况下,您可以直接访问 webView 属性以方便使用。然而,在现实场景中,请将逻辑保留在 ExpoWebView 类中,以最小化 ExpoWebViewModule 对其内部的了解。

使用 属性定义组件 来定义属性。在属性设置器块中,您可以访问视图和属性。指定 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 属性添加到 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

之后,您将看到 Expo 主页 在 WebView 中。

6

添加事件以通知页面已加载

视图回调 允许开发人员监听组件上的事件。它们通常通过组件上的属性注册,例如:<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

奖励:围绕它构建一个网页浏览器 UI

现在您有了 WebView,围绕它构建一个网页浏览器 UI。尝试重建一个浏览器 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' }}>Loading...</Text> </View> ); }

恭喜您!您已经为 Android 和 iOS 创建了第一个包含本机视图的 Expo 模块。

下一步

Expo 模块 API 参考

使用 Kotlin 和 Swift 创建本机模块。

教程:创建本机模块

创建一个使用 Expo 模块 API 持久化设置的本机模块的教程。