教程:创建原生视图
编辑页面
一份关于使用 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
2
设置工作区
通过删除以下文件来清理默认模块,以便从一个干净的状态开始:
- cd expo-web-view- rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts- rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts找到以下文件,并用提供的最小样板代码替换它们:
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) {} } }
import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) {} } }
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} />; }
export { default as WebView, Props as WebViewProps } from './ExpoWebView';
import { WebView } from 'expo-web-view'; export default function App() { return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />; }
3
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 时完成这一步。
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 的 frame 与 ExpoWebView 的 bounds 匹配。init 方法在视图创建时调用,而 layoutSubviews 在布局发生变化时调用。
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 } }
示例应用
这里不需要做任何更改。使用以下命令重新构建并运行应用:
# 使用 --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 模块
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 模块
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 类型中。
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。
import { WebView } from 'expo-web-view'; export default function App() { return <WebView style={{ flex: 1 }} url="https://expo.dev" />; }
重新构建示例应用:
- 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 事件处理器。
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 事件。
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。
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 事件。
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。
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} />; }
示例应用
更新示例应用,在页面加载完成时显示一个提示框。复制以下代码,然后重新构建并运行你的应用,你就会看到提示框!
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
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 模块。
下一步
使用 Kotlin 和 Swift 创建原生模块。
一个关于使用 Expo Modules API 创建可持久化设置的原生模块的教程。