关于使用 Expo Modules API 创建渲染 WebView 的原生视图的教程。
在本教程中,你将构建一个带有原生视图的示例模块,该视图可渲染 WebView。对于 Android,你将使用 WebView
组件,对于 iOS,你将使用 WKWebView
。可以使用 iframe
实现 Web 支持,这留给你作为练习。
¥In this tutorial, you'll build an example module with a native view that renders a WebView. For Android, you'll use the WebView
component, and for iOS, WKWebView
. Web support can be implemented using an iframe
and is left as an exercise for you.
1
2
¥Set up workspace
通过删除以下文件清理默认模块以从头开始:
¥Clean up the default module to start with a clean slate by deleting the following files:
-
cd expo-web-view
-
rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts
-
rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts
找到以下文件并将其替换为提供的最小样板文件:
¥Locate the following files and replace them with the provided minimal boilerplate:
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
¥Run the example project
要确保一切正常,请启动 TypeScript 编译器来监视更改并重建模块的 JavaScript:
¥To ensure everything is working, start the TypeScript compiler to watch for changes and rebuild the module's JavaScript:
# Run this in the root of the project to start the TypeScript compiler
-
npm run build
# Navigate to the example directory
-
cd example
# Run the example app on Android
-
npx expo run:android
# Run the example app on iOS
-
npx expo run:ios
你现在应该看到一个空白的紫色屏幕。虽然它不是很令人兴奋,但它是一个好的开始。接下来,将其转换为 WebView。
¥You should now see a blank purple screen. While it's not very exciting, it's a good start. Next, turn it into a WebView.
4
¥Add the system WebView as a subview
使用硬编码 URL 添加系统 WebView
作为 ExpoWebView
的子视图。ExpoWebView
类扩展了 ExpoView
,后者扩展了 React Native 中的 RCTView
,并最终扩展了 Android 上的 View
和 iOS 上的 UIView
。
¥Add the system WebView
with a hardcoded URL as a subview of ExpoWebView
. The ExpoWebView
class extends ExpoView
, which extends RCTView
from React Native, and eventually extends View
on Android and UIView
on iOS.
确保 WebView
子视图使用与 ExpoWebView
相同的布局,其布局由 React Native 的布局引擎计算。
¥Ensure that the WebView
subview uses the same layout as ExpoWebView
, whose layout is calculated by React Native's layout engine.
¥Android view
在 Android 上,使用 LayoutParams
设置 WebView 的布局以匹配 ExpoWebView
布局。你可以在实例化 WebView 时执行此操作。
¥On Android, use LayoutParams
to set the WebView's layout to match the ExpoWebView
layout. You can do this when you instantiate the 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://expo.nodejs.cn/modules/")
}
}
¥iOS view
在 iOS 上,将 clipsToBounds
设置为 true
,并确保 WebView 的 frame
与 layoutSubviews
中 ExpoWebView
的边界匹配。创建视图时调用 init
方法,布局更改时调用 layoutSubviews
。
¥On iOS, set clipsToBounds
to true
and ensure the WebView's frame
matches the bounds of ExpoWebView
in layoutSubviews
. The init
method is called when the view is created, and layoutSubviews
is called when the layout changes.
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://expo.nodejs.cn/modules/")!
let urlRequest = URLRequest(url:url)
webView.load(urlRequest)
}
override func layoutSubviews() {
webView.frame = bounds
}
}
¥Example app
无需更改。使用以下命令重建并运行应用:
¥No changes are required. Rebuild and run the app using the following commands:
# Prebuild the example app with the --clean flag to ensure a clean build
-
npx expo prebuild --clean
# Run the example app on Android
-
npx expo run:android
# Run the example app on iOS
-
npx expo run:ios
之后,你将看到渲染的 Expo 模块 API 概述页面。如果更改未反映出来,请尝试重新安装应用。
¥After that, you'll see the Expo Modules API overview page rendered. If the changes aren't reflected, try reinstalling the app.
5
¥Add a prop to set the URL
要在视图上设置 prop,请在 ExpoWebViewModule
中定义 prop 名称和 setter。在这种情况下,你可以直接访问 webView
属性以方便使用。但是,在实际场景中,请将逻辑保留在 ExpoWebView
类中,以尽量减少 ExpoWebViewModule
对其内部的了解程度。
¥To set a prop on the view, define the prop name and setter inside ExpoWebViewModule
. In this case, you can access the webView
property directly for convenience. However, in real-world scenarios, keep the logic inside the ExpoWebView
class to minimize how much ExpoWebViewModule
knows about its internals.
使用 Prop 定义组件 定义 prop。在 prop setter 块中,你可以访问视图和 prop。指定 URL 为 URL
类型 - Expo 模块 API 会将字符串转换为原生 URL
类型。
¥Use the Prop definition component to define the prop. In the prop setter block, you can access both the view and the prop. Specify that the URL is of type URL
— the Expo modules API will convert strings to the native URL
type.
¥Android module
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 module
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 module
接下来,将 url
属性添加到 Props
类型。
¥Next, add the url
prop to the Props
type.
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} />;
}
¥Example app
最后,将 URL
传递给示例应用中的 WebView
组件。
¥Finally, pass a URL
to your WebView
component in the example app.
import { WebView } from 'expo-web-view';
export default function App() {
return <WebView style={{ flex: 1 }} url="https://expo.dev" />;
}
重建示例应用:
¥Rebuild the example app:
-
npx expo prebuild --clean
# Run the example app on Android
-
npx expo run:android
# Run the example app on iOS
-
npx expo run:ios
之后,你将在 WebView 中看到 Expo 主页。
¥After that, you'll see the Expo homepage in the WebView.
6
¥Add an event to notify when the page has loaded
查看回调 允许开发者监听组件上的事件。它们通常通过组件上的 props 注册,例如:<Image onLoad={...} />
。使用 事件定义组件 为你的 WebView 定义事件。称之为 onLoad
。
¥View callbacks allow developers to listen for events on components. They are typically registered through props on the component, for example: <Image onLoad={...} />
. Use the Events definition component to define an event for your WebView. Call it onLoad
.
¥Android view and module
在 Android 上,覆盖 onPageFinished
函数。然后,调用你在模块中定义的 onLoad
事件处理程序。
¥On Android, override the onPageFinished
function. Then, call the onLoad
event handler that you defined in the module.
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
事件。
¥Indicate in ExpoWebViewModule
that the View
has an onLoad
event.
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 view and module
在 iOS 上,实现 webView(_:didFinish:)
并使 ExpoWebView
扩展 WKNavigationDelegate
。然后,从该委托方法调用 onLoad
。
¥On iOS, implement webView(_:didFinish:)
and make ExpoWebView
extend WKNavigationDelegate
. Then, call onLoad
from that delegate method.
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
事件。
¥Indicate in ExpoWebViewModule
that the View
has an onLoad
event.
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 module
事件有效负载包含在事件的 nativeEvent
属性中。要从 onLoad
事件访问 url
,请阅读 event.nativeEvent.url
。
¥Event payloads are included within the nativeEvent
property of the event. To access the url
from the onLoad
event, read 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} />;
}
¥Example app
更新示例应用,在页面加载时显示警报。复制以下代码,然后重建并运行你的应用,你将看到警报!
¥Update the example app to show an alert when the page has loaded. Copy the following code, then rebuild and run your app, and you'll see the alert!
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: Build a web browser UI around it
现在你有了 WebView,请围绕它构建一个 Web 浏览器 UI。尝试重建浏览器 UI,并根据需要随意添加新的原生功能(例如,支持后退或重新加载按钮)。如果你需要灵感,请参见下面的示例。
¥Now that you have a WebView, build a web browser UI around it. Try rebuilding a browser UI, and feel free to add new native capabilities as needed (for example, support for back or reload buttons). If you need inspiration, see the example below.
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://expo.nodejs.cn/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>
);
}
恭喜!你已经创建了第一个 Expo 模块,其中包含适用于 Android 和 iOS 的原生视图。
¥Congratulations! You've created your first Expo module with a native view for Android and iOS.
¥Next steps
使用 Kotlin 和 Swift 创建原生模块。
关于使用 Expo Modules API 创建保留设置的原生模块的教程。