在这里插入图片描述

📋 前言

react-native-webview 是 React Native 社区最流行的 WebView 组件,提供了一套完整的网页加载和交互解决方案。它支持加载网络 URL、本地 HTML 文件、注入 JavaScript、处理网页事件等多种功能,并且完全兼容 Android、iOS 和 HarmonyOS 三端。

🎯 库简介

基本信息

  • 库名称: @react-native-ohos/react-native-webview
  • 版本信息:
    • 13.10.5: 支持 RN 0.72 版本
    • 13.15.1: 支持 RN 0.77 版本
  • 官方仓库: https://github.com/react-native-oh-library/react-native-webview
  • 主要功能:
    • 加载网络 URL 和本地 HTML
    • 注入 JavaScript 代码
    • 处理网页加载事件
    • 网页与原生通信
    • 支持多种配置选项
    • 兼容 Android、iOS 和 HarmonyOS
  • 兼容性验证:
    • 鸿蒙化版本 13.10.4-rc.4 及之后需要 DevEco Studio 6.0.0 (API20) 及以上版本

为什么需要这个库?

  • 功能完整: 提供完整的 WebView 功能
  • 跨平台: 在三端提供一致的体验
  • 易于使用: API 简单直观
  • 性能优异: 原生实现,高效稳定
  • 灵活配置: 支持多种配置选项

📦 安装步骤

1. 使用 npm 安装

根据您的 RN 版本选择对应的包名:

npm install @react-native-ohos/react-native-webview@13.10.5-rc.1

2. 验证安装

安装完成后,检查 package.json 文件,应该能看到新增的依赖:

{
  "dependencies": {
    "@react-native-ohos/react-native-webview": "^13.10.5-rc.1",
    // ... 其他依赖
  }
}

🔧 HarmonyOS 平台配置 ⭐

1. 在工程根目录的 oh-package.json5 添加 overrides 字段

首先需要使用 DevEco Studio 打开项目里的 HarmonyOS 工程 harmony

打开 harmony/oh-package.json5,添加以下配置:

{
  ...
  "overrides": {
    "@rnoh/react-native-openharmony": "^0.72.90"
  }
}

2. 引入原生端代码

方法一:通过 har 包引入(不推荐)

[!TIP] har 包位于三方库安装路径的 harmony 文件夹下。

打开 entry/oh-package.json5,添加以下依赖:

"dependencies": {
    "@rnoh/react-native-openharmony": "0.72.90",
    "@react-native-ohos/react-native-webview": "file:../../node_modules/@react-native-ohos/react-native-webview/harmony/rn_webview.har"
}

点击右上角的 sync 按钮

或者在终端执行:

cd entry
ohpm install
方法二:直接链接源码

步骤 1: 把 <RN工程>/node_modules/@react-native-ohos/react-native-webview/harmony 目录下的源码 rn_webview 复制到 harmony(鸿蒙壳工程)工程根目录下。

步骤 2: 在 harmony 工程根目录的 build-profile.template.json5(若存在)和 build-profile.json5 添加以下模块:

modules: [
  ...
  {
    name: 'rn_webview',
    srcPath: './rn_webview',
  }
]

步骤 3: 打开 rn_webview/oh-package.json5,修改 react-native-openharmony 和项目的版本一致。

步骤 4: 打开 entry/oh-package.json5,添加以下依赖:

"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  "@react-native-ohos/react-native-webview": "file:../rn_webview"
}

步骤 5: 点击 DevEco Studio 右上角的 sync 按钮

3. 配置 CMakeLists 和引入 WebViewPackage

打开 entry/src/main/cpp/CMakeLists.txt,添加:

project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")

set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")

set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp")
set(CMAKE_ASM_FLAGS "-Wno-error:unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,noexecstack -s -fPIE -pie")

add_compile_definitions(WITH_HITRACE_SYSTRACE)
set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use

add_subdirectory("${RNOH_CPP_DIR}" ./rn)

add_library(rnoh_app SHARED
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" "./PackageProvider.cpp")

target_link_libraries(rnoh_app PUBLIC rnoh)


set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")

+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-webview/src/main/cpp" ./webview)
+ target_link_libraries(rnoh_app PUBLIC rnoh_webview)

打开 entry/src/main/cpp/PackageProvider.cpp,添加:

#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
#include "SamplePackage.h"
+ #include "WebViewPackage.h"

using namespace rnoh;

std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
    return {
      std::make_shared<RNOHGeneratedPackage>(ctx),
      std::make_shared<SamplePackage>(ctx),
+     std::make_shared<WebViewPackage>(ctx)
    };
}

4. 在 ArkTs 侧引入 WebView 组件

找到 function buildCustomComponent(),一般位于 entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets,添加:

+ import { WebView, WEB_VIEW } from "@react-native-ohos/react-native-webview"

@Builder
function buildCustomComponent(ctx: ComponentBuilderContext) {
+ if (ctx.componentName === WEB_VIEW) {
+   WebView({
+     ctx: ctx.rnComponentContext,
+     tag: ctx.tag
+   })
+ }
...
}

[!TIP] 本库使用了混合方案,需要添加组件名。

entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets 找到常量 arkTsComponentNames 在其数组里添加组件名:

const arkTsComponentNames: Array<string> = [
+ WEB_VIEW
  ];

5. 在 ArkTs 侧引入 WebViewPackage

打开 entry/src/main/ets/RNPackagesFactory.ts,添加:

+ import { WebViewPackage } from '@react-native-ohos/react-native-webview/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new SamplePackage(ctx),
+   new WebViewPackage(ctx)
  ];
}

6. 运行

点击右上角的 sync 按钮

或者在终端执行:

cd entry
ohpm install

然后编译、运行即可。

💻 完整代码示例

下面是一个完整的示例,展示了 react-native-webview 的各种使用场景:

import React, { useRef, useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  TextInput,
  ScrollView,
  SafeAreaView,
  ActivityIndicator,
} from 'react-native';
import { WebView, WebViewNavigation } from 'react-native-webview';

function WebViewDemo() {
  const webViewRef = useRef<WebView>(null);
  const [url, setUrl] = useState('https://reactnative.dev/');
  const [canGoBack, setCanGoBack] = useState(false);
  const [canGoForward, setCanGoForward] = useState(false);
  const [loading, setLoading] = useState(false);
  const [currentUrl, setCurrentUrl] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [progress, setProgress] = useState(0);

  const handleGoBack = () => {
    if (webViewRef.current && typeof webViewRef.current.goBack === 'function') {
      webViewRef.current.goBack();
    }
  };

  const handleGoForward = () => {
    if (webViewRef.current && typeof webViewRef.current.goForward === 'function') {
      webViewRef.current.goForward();
    }
  };

  const handleReload = () => {
    webViewRef.current?.reload();
  };

  const handleLoad = () => {
    webViewRef.current?.injectJavaScript(`
        (function() {
          window.ReactNativeWebView.postMessage(JSON.stringify({
            type: 'pageInfo',
            title: document.title,
            url: window.location.href
          }));
        })();
        true;
      `);
  };

  const handleMessage = (event: any) => {
    try {
      const data = JSON.parse(event.nativeEvent.data);
      console.log('收到网页消息:', data);
    } catch (e) {
      console.log('收到网页消息:', event.nativeEvent.data);
    }
  };

  const loadUrl = () => {
    if (url.trim()) {
      setError(null);
    }
  };

  const useDefaultUrl = () => {
    setUrl('https://reactnative.dev/');
    setError(null);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.container}>
        {/* 顶部工具栏 */}
        <View style={styles.toolbar}>
          <TouchableOpacity
            style={[styles.navButton, !canGoBack && styles.disabledButton]}
            onPress={handleGoBack}
            disabled={!canGoBack}
          >
            <Text style={styles.navButtonText}></Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={[styles.navButton, !canGoForward && styles.disabledButton]}
            onPress={handleGoForward}
            disabled={!canGoForward}
          >
            <Text style={styles.navButtonText}></Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={styles.navButton}
            onPress={handleReload}
          >
            <Text style={styles.navButtonText}></Text>
          </TouchableOpacity>

          <TextInput
            style={styles.urlInput}
            value={url}
            onChangeText={setUrl}
            onSubmitEditing={loadUrl}
            placeholder="输入网址"
            autoCapitalize="none"
            autoCorrect={false}
          />

          <TouchableOpacity
            style={styles.goButton}
            onPress={loadUrl}
          >
            <Text style={styles.goButtonText}>前往</Text>
          </TouchableOpacity>
        </View>

        {/* 进度条 */}
        {loading && progress > 0 && progress < 1 && (
          <View style={styles.progressBarContainer}>
            <View style={[styles.progressBar, { width: `${progress * 100}%` }]} />
          </View>
        )}

        {/* 错误提示 */}
        {error && (
          <View style={styles.errorContainer}>
            <Text style={styles.errorText}>{error}</Text>
          </View>
        )}

        {/* WebView */}
        <View style={styles.webviewContainer}>
          {loading && (
            <View style={styles.loadingContainer}>
              <ActivityIndicator size="large" color="#42a5f5" />
              <Text style={styles.loadingText}>加载中...</Text>
            </View>
          )}
          <WebView
            ref={webViewRef}
            source={{ uri: url }}
            style={styles.webview}
            javaScriptEnabled={true}
            domStorageEnabled={true}
            startInLoadingState={true}
            scalesPageToFit={true}
            showsHorizontalScrollIndicator={true}
            showsVerticalScrollIndicator={true}
            onNavigationStateChange={(navState: WebViewNavigation) => {
              setCanGoBack(navState.canGoBack);
              setCanGoForward(navState.canGoForward);
              setCurrentUrl(navState.url);
            }}
            onLoadStart={() => {
              setLoading(true);
              setError(null);
            }}
            onLoadEnd={() => {
              setLoading(false);
              // 延迟执行 JavaScript 注入,确保 WebView 已完全加载
              setTimeout(() => {
                handleLoad();
              }, 100);
            }}
            onLoadProgress={({ nativeEvent }) => {
              setProgress(nativeEvent.progress);
            }}
            onHttpError={(syntheticEvent) => {
              const { nativeEvent } = syntheticEvent;
              setError(`HTTP 错误: ${nativeEvent.statusCode}`);
              setLoading(false);
            }}
            onError={(syntheticEvent) => {
              const { nativeEvent } = syntheticEvent;
              setError(`加载错误: ${nativeEvent.description}`);
              setLoading(false);
            }}
            onMessage={handleMessage}
            injectedJavaScriptBeforeContentLoaded={`
              window.addEventListener('load', function() {
                window.ReactNativeWebView.postMessage(JSON.stringify({
                  type: 'loaded',
                  timestamp: new Date().toISOString()
                }));
              });
            `}
            ignoreSilentHardwareSwitch={true}
          />
        </View>

        {/* 当前 URL 显示 */}
        {currentUrl && (
          <View style={styles.urlDisplay}>
            <Text style={styles.urlLabel}>当前页面:</Text>
            <Text style={styles.urlText} numberOfLines={1}>
              {currentUrl}
            </Text>
          </View>
        )}

        {/* 功能按钮 */}
        <View style={styles.buttonContainer}>
          <TouchableOpacity
            style={styles.actionButton}
            onPress={useDefaultUrl}
          >
            <Text style={styles.actionButtonText}>React Native 官网</Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={styles.actionButton}
            onPress={() => {
              webViewRef.current?.injectJavaScript(`
                  alert('这是一个来自 React Native 的注入消息!');
                  true;
                `);
            }}
          >
            <Text style={styles.actionButtonText}>注入 JS</Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={styles.actionButton}
            onPress={() => {
              if (webViewRef.current && typeof webViewRef.current.clearCache === 'function') {
                webViewRef.current.clearCache(true);
              }
            }}
          >
            <Text style={styles.actionButtonText}>清除缓存</Text>
          </TouchableOpacity>
        </View>

        {/* 说明 */}
        <View style={styles.infoSection}>
          <Text style={styles.infoTitle}>功能说明:</Text>
          <Text style={styles.infoText}>
            • 支持加载网络 URL 和本地 HTML 文件
          </Text>
          <Text style={styles.infoText}>
            • 支持注入 JavaScript 代码
          </Text>
          <Text style={styles.infoText}>
            • 支持网页与原生通信
          </Text>
          <Text style={styles.infoText}>
            • 支持前进、后退、刷新等导航功能
          </Text>
          <Text style={styles.infoText}>
            • 支持加载进度显示
          </Text>
          <Text style={styles.infoText}>
            • 支持错误处理
          </Text>
        </View>

        {/* 注意事项 */}
        <View style={styles.noteSection}>
          <Text style={styles.noteTitle}>注意事项:</Text>
          <Text style={styles.noteText}>
            • ignoreSilentHardwareSwitch 需要设置为 true,网页播放才有声音
          </Text>
          <Text style={styles.noteText}>
            • 需要配置网络权限才能访问网络
          </Text>
          <Text style={styles.noteText}>
            • 部分属性 HarmonyOS 暂不支持
          </Text>
          <Text style={styles.noteText}>
            • 编译需要 DevEco Studio 6.0.0 (API20) 及以上版本
          </Text>
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  toolbar: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 10,
    backgroundColor: '#fff',
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
    gap: 5,
  },
  navButton: {
    width: 40,
    height: 40,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
  },
  disabledButton: {
    opacity: 0.3,
  },
  navButtonText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333',
  },
  urlInput: {
    flex: 1,
    height: 40,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 10,
    backgroundColor: '#f9f9f9',
    fontSize: 14,
  },
  goButton: {
    paddingHorizontal: 15,
    paddingVertical: 8,
    backgroundColor: '#42a5f5',
    borderRadius: 8,
  },
  goButtonText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: 'bold',
  },
  progressBarContainer: {
    height: 2,
    backgroundColor: '#e0e0e0',
  },
  progressBar: {
    height: '100%',
    backgroundColor: '#42a5f5',
  },
  errorContainer: {
    padding: 10,
    backgroundColor: '#ffebee',
    borderBottomWidth: 1,
    borderBottomColor: '#ffcdd2',
  },
  errorText: {
    color: '#c62828',
    fontSize: 14,
    textAlign: 'center',
  },
  webviewContainer: {
    flex: 1,
    backgroundColor: '#fff',
  },
  webview: {
    flex: 1,
  },
  loadingContainer: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(255, 255, 255, 0.8)',
  },
  loadingText: {
    marginTop: 10,
    fontSize: 16,
    color: '#666',
  },
  urlDisplay: {
    padding: 10,
    backgroundColor: '#fff',
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  urlLabel: {
    fontSize: 12,
    color: '#666',
    marginBottom: 5,
  },
  urlText: {
    fontSize: 14,
    color: '#333',
  },
  buttonContainer: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    padding: 10,
    gap: 10,
    backgroundColor: '#fff',
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  actionButton: {
    paddingHorizontal: 15,
    paddingVertical: 10,
    backgroundColor: '#42a5f5',
    borderRadius: 8,
    minWidth: 100,
    alignItems: 'center',
  },
  actionButtonText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: '500',
  },
  infoSection: {
    margin: 10,
    padding: 15,
    backgroundColor: '#e3f2fd',
    borderRadius: 8,
    borderLeftWidth: 4,
    borderLeftColor: '#42a5f5',
  },
  infoTitle: {
    fontSize: 14,
    fontWeight: 'bold',
    marginBottom: 8,
    color: '#1565c0',
  },
  infoText: {
    fontSize: 13,
    color: '#1976d2',
    marginBottom: 4,
  },
  noteSection: {
    margin: 10,
    padding: 15,
    backgroundColor: '#fff3cd',
    borderRadius: 8,
    borderLeftWidth: 4,
    borderLeftColor: '#ffc107',
  },
  noteTitle: {
    fontSize: 14,
    fontWeight: 'bold',
    marginBottom: 8,
    color: '#856404',
  },
  noteText: {
    fontSize: 13,
    color: '#856404',
    marginBottom: 4,
  },
});

export default WebViewDemo;

🎨 实际应用场景

react-native-webview 可以应用于以下实际场景:

  1. 混合开发: 在原生应用中嵌入 H5 页面
  2. 在线文档: 查看 PDF、Word 等在线文档
  3. 第三方登录: 集成微信、支付宝等第三方登录
  4. 内容展示: 展示富文本、图表等复杂内容
  5. 广告展示: 加载第三方广告页面

⚠️ 注意事项与最佳实践

1. 网络权限配置

在使用 WebView 之前,需要在 HarmonyOS 的 module.json5 中配置网络权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

2. 重要属性配置

// ✅ 推荐:启用声音播放
<WebView
  ignoreSilentHardwareSwitch={true}
  // ...
/>

// ✅ 推荐:启用 JavaScript
<WebView
  javaScriptEnabled={true}
  domStorageEnabled={true}
  // ...
/>

// ✅ 推荐:处理加载状态
<WebView
  startInLoadingState={true}
  onLoadStart={() => setLoading(true)}
  onLoadEnd={() => setLoading(false)}
  // ...
/>

3. JavaScript 注入

// ✅ 推荐:页面加载完成后注入
<WebView
  onLoad={() => {
    webViewRef.current?.injectJavaScript(`
      // 你的 JavaScript 代码
      true;
    `);
  }}
/>

// ✅ 推荐:页面加载前注入
<WebView
  injectedJavaScriptBeforeContentLoaded={`
    // 在页面加载前执行的 JavaScript 代码
  `}
/>

4. 网页与原生通信

// 在网页中发送消息
window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'message',
  data: 'hello from web'
}));

// 在 React Native 中接收消息
<WebView
  onMessage={(event) => {
    const data = JSON.parse(event.nativeEvent.data);
    console.log('收到消息:', data);
  }}
/>

5. 性能优化

// ✅ 推荐:启用缓存
<WebView
  cacheEnabled={true}
  // ...
/>

// ✅ 推荐:清除缓存
webViewRef.current?.clearCache(true);

// ✅ 推荐:减少不必要的渲染

6. HarmonyOS 特殊处理

在 HarmonyOS 上,需要注意:

  • 声音播放: ignoreSilentHardwareSwitch 需要设置为 true,网页播放才有声音
  • 编译版本: 需要使用 DevEco Studio 6.0.0 (API20) 及以上版本
  • 部分属性: 部分属性 HarmonyOS 暂不支持
  • 混合方案: 需要在 ArkTs 侧添加组件名和 WebView 组件

🧪 测试验证

1. Android 平台测试

npm run android

测试要点:

  • 测试网页加载
  • 测试导航功能
  • 验证 JavaScript 注入
  • 检查音频播放

2. iOS 平台测试

npm run ios

测试要点:

  • 测试网页加载质量
  • 验证 JavaScript 执行
  • 测试通信功能
  • 检查缓存机制

3. HarmonyOS 平台测试

npm run harmony

测试要点:

  • 验证网页加载功能
  • 测试声音播放(需要设置 ignoreSilentHardwareSwitch)
  • 检查 JavaScript 注入
  • 验证网页与原生通信

4. 常见问题排查

问题 1: 网页无法加载

  • 检查网络权限配置
  • 确认 URL 是否正确
  • 验证网络连接

问题 2: 音频没有声音

  • 确认 ignoreSilentHardwareSwitch 设置为 true
  • 检查设备音量设置
  • 验证音频文件格式

问题 3: JavaScript 不执行

  • 确认 javaScriptEnabled 设置为 true
  • 检查 JavaScript 代码语法
  • 验证注入时机

📊 对比:原生 WebView vs react-native-webview

特性 原生 WebView react-native-webview
跨平台一致性 ✅ 完全一致
API 简洁性 ⚠️ 复杂 ✅ 简洁
JavaScript 注入 ✅ 支持 ✅ 支持
网页通信 ⚠️ 复杂 ✅ 简单
导航控制 ✅ 完整 ✅ 完整
缓存管理 ✅ 完整 ✅ 完整

📝 总结

通过集成 react-native-webview,我们为项目添加了强大的 WebView 功能。这个库提供了完整的网页加载和交互解决方案,支持多种配置选项,并且完全跨平台兼容。

关键要点回顾

  • 安装依赖: npm install @react-native-ohos/react-native-webview
  • 配置平台: 通过 har 包或直接链接源码,配置 CMakeLists.txt、PackageProvider.cpp、RNPackagesFactory.ts 和 buildCustomComponent
  • 集成代码: 使用 WebView 组件
  • 支持功能: 加载 URL、注入 JS、网页通信、导航控制等
  • 重要: 本库使用了混合方案,需要在 ArkTs 侧添加组件名和 WebView 组件
  • 注意: ignoreSilentHardwareSwitch 需要设置为 true,网页播放才有声音

实际效果

  • Android: 原生 WebView 体验
  • iOS: 高质量的 WebView 体验
  • HarmonyOS: 一致的 WebView 体验

已知限制

  • ⚠️ 部分属性 HarmonyOS 暂不支持
  • ⚠️ 需要使用 DevEco Studio 6.0.0 (API20) 及以上版本
  • ⚠️ 中文乱码问题已修复
  • ⚠️ 部分属性未实现 HarmonyOS 化

遗留问题

  • webview 部分属性未实现 HarmonyOS 化
  • webview 部分属性未实现 HarmonyOS 化
  • 中文乱码(已修复)

希望这篇教程能帮助你顺利集成 react-native-webview,构建出色的 WebView 体验!


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐