教程:创建本机视图
编辑
创建一个本机视图的教程,该视图使用 Expo 模块 API 渲染 WebView。
在本教程中,您将构建一个示例模块,其中包含一个渲染 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 添加为子视图
将系统 WebView 以硬编码的 URL 添加为 ExpoWebView 的子视图。ExpoWebView 类扩展了 ExpoView,它扩展了来自 React Native 的 RCTView,最终在 Android 上扩展为 View,在 iOS 上扩展为 UIView。
确保 WebView 子视图使用与 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,并确保 WebView 的 frame 在 layoutSubviews 中与 ExpoWebView 的边界匹配。当创建视图时调用 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
要在视图上设置属性,请在 ExpoWebViewModule 中定义属性名称和设置器。在这种情况下,您可以直接访问 webView 属性以方便使用。然而,在现实场景中,请将逻辑保留在 ExpoWebView 类中,以最小化 ExpoWebViewModule 对其内部的了解。
使用 属性定义组件 来定义属性。在属性设置器块中,您可以访问视图和属性。指定 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 属性添加到 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之后,您将看到 Expo 主页 在 WebView 中。
6
添加事件以通知页面已加载
视图回调 允许开发人员监听组件上的事件。它们通常通过组件上的属性注册,例如:<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
奖励:围绕它构建一个网页浏览器 UI
现在您有了 WebView,围绕它构建一个网页浏览器 UI。尝试重建一个浏览器 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' }}>Loading...</Text> </View> ); }
恭喜您!您已经为 Android 和 iOS 创建了第一个包含本机视图的 Expo 模块。
下一步
使用 Kotlin 和 Swift 创建本机模块。
创建一个使用 Expo 模块 API 持久化设置的本机模块的教程。