首页指南参考教程

教程:创建原生视图

有关创建使用 Expo 模块 API 渲染 WebView 的原生视图的教程。


在本教程中,我们将构建一个具有原生视图的模块,该视图将渲染 WebView。我们将在 Android 上使用 WebView 组件,在 iOS 上使用 WKWebView。可以使用 iframe 实现 Web 支持,但我们将其留给读者作为练习。

¥In this tutorial, we are going to build a module with a native view that will render a WebView. We will be using the WebView component for Android and WKWebView for iOS. It is possible to implement web support using iframe, but we'll leave that as an exercise for the reader.

1. 初始化一个新模块

¥ Initialize a new module

首先,我们将创建一个新模块。在此页面上,我们将使用名称 expo-web-view/ExpoWebView。你可以将其命名为任何你喜欢的名称,只需相应地调整说明即可:

¥First, we'll create a new module. On this page, we will use the name expo-web-view/ExpoWebView. You can name it whatever you like, just adjust the instructions accordingly:

Terminal
npx create-expo-module expo-web-view

提示:由于你实际上不会发布此库,因此你可以点击 return 以接受所有提示的默认值。

¥Tip: Since you aren't going to actually ship this library, you can hit return for all the prompts to accept the default values.

2. 设置我们的工作区

¥ Set up our workspace

现在让我们稍微清理一下默认模块,以便我们有更多的干净状态,并删除本指南中不会使用的代码。

¥Now let's clean up the default module a little bit so we have more of a clean slate and delete the code that we won't use in this guide.

Terminal
cd expo-web-view
rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts
rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts

找到以下文件并将其替换为提供的最小样板:

¥Find the following files and replace them with the provided minimal boilerplate:

ios/ExpoWebViewModule.swift
import ExpoModulesCore

public class ExpoWebViewModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ExpoWebView")

    View(ExpoWebView.self) {}
  }
}
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) {}
  }
}
src/index.ts
export { default as WebView, Props as WebViewProps } from './ExpoWebView';
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} />;
}
example/App.tsx
import { WebView } from 'expo-web-view';

export default function App() {
  return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />;
}

3. 运行示例项目

¥ Run the example project

现在让我们运行示例项目以确保一切正常。我们需要启动 TypeScript 编译器来监视更改并重建 JavaScript 模块,并在另一个终端窗口中单独编译并运行示例应用。

¥Now let's run the example project to make sure everything is working. We'll need to start the TypeScript compiler to watch for changes and rebuild the module JavaScript, and separately in another terminal window we'll compile and run the example app.

Terminal
# Run this in the root of the project to start the TypeScript compiler
npm run build
Terminal
cd example
# Run the example app on iOS
npx expo run:ios
# Run the example app on Android
npx expo run:android

我们现在应该看到一个空白的紫色屏幕。这不是很令人兴奋。然而,这是一个好的开始。现在让我们将其设为 WebView。

¥We should now see a blank purple screen. That's not very exciting. However, it's a good start. Let's make it a WebView now.

4. 添加系统 WebView 作为子视图

¥ Add the system WebView as a subview

现在我们将使用硬编码 URL 添加系统 WebView 作为 ExpoWebView 的子视图。我们的 ExpoWebView 类扩展了 ExpoView,后者扩展了 React Native 的 RCTView,最终扩展了 iOS 上的 UIView 和 Android 上的 View。我们需要确保 WebView 子视图与 ExpoWebView 具有相同的布局,其布局将由 React Native 的布局引擎计算。

¥Now we are going to add the system WebView with a hardcoded URL as a subview of our ExpoWebView. Our ExpoWebView class extends ExpoView, which extends RCTView from React Native, which finally extends UIView on iOS and View on Android. We need to ensure that the WebView subview has the same layout as ExpoWebView, whose layout will be calculated by React Native's layout engine.

iOS 视图

¥iOS view

在 iOS 上,我们将 clipsToBounds 设置为 true,并将 WebView 的 frame 设置为 layoutSubviews 中 ExpoWebView 的边界以匹配布局。init 在视图创建时调用,layoutSubviews 在布局改变时调用。

¥On iOS, we set clipsToBounds to true and set the frame of the WebView to the bounds of the ExpoWebView in layoutSubviews to match the layout. init is called when the view is created, and layoutSubviews is called when the layout changes.

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://expo.nodejs.cn/modules/")!
    let urlRequest = URLRequest(url:url)
    webView.load(urlRequest)
  }

  override func layoutSubviews() {
    webView.frame = bounds
  }
}

安卓视图

¥Android view

在 Android 上,我们使用 LayoutParams 设置 WebView 的布局以匹配 ExpoWebView 的布局。我们可以在实例化 WebView 时执行此操作。

¥On Android we use LayoutParams to set the layout of the WebView to match the layout of the ExpoWebView. We can do this when we instantiate the 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://expo.nodejs.cn/modules/")
  }
}

示例应用

¥Example app

无需任何更改,我们可以重建并运行应用,你将看到 Expo 模块 API 概述页面

¥No changes are needed, we can rebuild and run the app and you will see the Expo Modules API overview page.

5. 添加一个 prop 来设置 URL

¥ Add a prop to set the URL

要在我们的视图上设置 prop,我们需要在 ExpoWebViewModule 内部定义 prop 名称和设置器。在本例中,为了方便起见,我们将直接访问 webView 属性,但在许多现实情况下,你可能希望将此逻辑保留在 ExpoWebView 类内部,并最大限度地减少 ExpoWebViewModuleExpoWebView 内部结构的了解。

¥To set a prop on our view, we'll need to define the prop name and setter inside of ExpoWebViewModule. In this case we're going to reach in and access webView property directly for convenience, but in many real world cases you will likely want to keep this logic inside of the ExpoWebView class and minimize the knowledge that ExpoWebViewModule has about the internals of ExpoWebView.

我们使用 Prop 定义组件 来定义 prop。在 prop setter 块中,我们可以访问视图和 prop。请注意,我们指定 url 的类型为 URL — Expo 模块 API 将为我们负责将字符串转换为原生 URL 类型。

¥We use the Prop definition component to define the prop. Within the prop setter block we can access the view and the prop. Note that we specify the url is of type URL — the Expo modules API will take care of converting strings to the native URL type for us.

iOS 模块

¥iOS module

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)
        }
      }
    }
  }
}

安卓模块

¥Android module

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())
      }
    }
  }
}

TypeScript 模块

¥TypeScript module

我们在这里需要做的就是将 url 属性添加到 Props 类型。

¥All we need to do here is add the url prop to the Props type.

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} />;
}

示例应用

¥Example app

最后,我们可以将 URL 传递给示例应用中的 WebView 组件。

¥Finally, we can pass in a URL to our WebView component in the example app.

example/App.tsx
import { WebView } from 'expo-web-view';

export default function App() {
  return <WebView style={{ flex: 1 }} url="https://expo.dev" />;
}

当你重建并运行应用时,你现在将看到 Expo 主页。

¥When you rebuild and run the app, you will now see the Expo homepage.

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={...} />. We can use the Events definition component to define an event for our WebView. We'll call it onLoad as well.

iOS 视图和模块

¥iOS view and module

在 iOS 上,我们需要实现 webView(_:didFinish:) 并使 ExpoWebView 扩展 WKNavigationDelegate。然后我们可以从该委托方法调用 onLoad

¥On iOS, we need to implement webView(_:didFinish:) and make ExpoWebView extend WKNavigationDelegate. We can then call the onLoad from that delegate method.

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 事件。

¥And we need to indicate in ExpoWebViewModule that the View has an onLoad event.

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)
        }
      }
    }
  }
}

Android 视图和模块

¥Android view and module

在 Android 上,我们需要添加覆盖 onPageFinished 功能。然后我们可以调用我们在模块中定义的 onLoad 事件处理程序。

¥On Android, we need to add override the onPageFinished function. We can then call the onLoad event handler that we defined in the module.

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 事件。

¥And we need to indicate in ExpoWebViewModule that the View has an onLoad event.

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())
      }
    }
  }
}

TypeScript 模块

¥TypeScript module

请注意,事件有效负载包含在事件的 nativeEvent 属性中,因此要从 onLoad 事件访问 url,我们将读取 event.nativeEvent.url

¥Note that event payloads are included within the nativeEvent property of the event, so to access the url from the onLoad event we would read 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

现在我们可以更新示例应用以在页面加载时显示警报。复制以下代码,然后重建并运行你的应用,你应该会看到警报!

¥Now we can update the example app to show an alert when the page has loaded. Copy in the following code, then rebuild and run your app, and you should see the alert!

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

¥ Bonus: Build a web browser UI around it

现在我们有了一个 Web 视图,我们可以围绕它构建一个 Web 浏览器 UI。尝试重建浏览器用户界面,享受一些乐趣,甚至可以根据需要添加新的原生功能(例如,支持后退或重新加载按钮)。如果你想要一些灵感,下面有一个简单的示例。

¥Now that we have a web view, we can build a web browser UI around it. Have some fun trying to rebuild a browser UI, and maybe even add new native capabilities as needed (for example, to support a back or reload buttons). If you'd like some inspiration, there's a simple example below.

example/App.tsx
import { useState } from 'react';
import { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native';
import { WebView } from 'expo-web-view';
import { StatusBar } from 'expo-status-bar';

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} />
      <StatusBar style="auto" />
    </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>
  );
}

A simple web browser UI built around our WebView

下一步

¥Next steps

恭喜,你已经创建了第一个简单但不平凡的 Expo 模块,其中包含适用于 Android 和 iOS 的原生视图!了解有关 Expo 模块 API 参考 中 API 的更多信息。

¥Congratulations, you have created your first simple yet non-trivial Expo module with a native view for Android and iOS! Learn more about the API in the Expo Module API reference.

如果你喜欢本教程但尚未完成原生模块教程,请参阅 创建原生模块

¥If you enjoyed this tutorial and haven't done the native module tutorial, see creating a native module.

Expo 中文网 - 粤ICP备13048890号