教程:创建原生视图
编辑页面
一份关于使用 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
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 定义组件 来定义该 prop。在 prop setter 块中,你可以同时访问视图和 prop。指定 URL 的类型为 URL — Expo 模块 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={...} />。使用 事件定义组件 为你的 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 创建可持久化设置的原生模块的教程。