React Native 鸿蒙实战:view-shot 视图截图在 HarmonyOS 上的接入与使用
React Native 鸿蒙实战:view-shot 视图截图在 HarmonyOS 上的接入与使用
库版本:@react-native-ohos/react-native-view-shot 4.0.1-rc.1(OpenHarmony 适配版)
上游依赖:react-native-view-shot 5.1.1
适配仓库:https://atomgit.com/CPF-RN/rntpc_react-native-view-shot
验证环境:RNOH 0.86.1(对齐 React Native 0.86.3)
设备:鸿蒙 PC(OpenHarmony,2in1 形态)


一、环境搭建
React Native 鸿蒙环境搭建请参考官方文档:RNOH 环境搭建指南
本章不重复展开。搭建完成后,确认 pnpm --version 输出 10.x 以上,DevEco Studio 可正常创建鸿蒙工程即可。
二、应用背景
2.1 当前的应用场景与痛点
分享海报、保存报表、生成图片凭证、页面快照反馈等场景都需要把屏幕上的某块区域变成图片。React Native 在 Android 和 iOS 上通过 react-native-view-shot 提供成熟的视图截图方案,但鸿蒙系统使用完全不同的 ArkUI 组件快照体系(componentSnapshot / window.snapshot),开发者如果自行适配,需要:
- 对接鸿蒙 @ohos.arkui.componentSnapshot 组件快照与 @ohos.window 窗口截图两套 API,与 RN 的 JS 层模型不同;
- 处理 RN 组件 tag 到 ArkUI 节点 id 的映射,Fabric 渲染树与原生组件树并不一一对应;
- 编写 ArkTS 原生 TurboModule 桥接 JS 调用、图片编码(imagePacker)与临时文件读写;
- 配置 codegen spec、HAR 包编译、autolinking 注册等 RNOH 构建流程。
2.2 为什么需要这个库
@react-native-ohos/react-native-view-shot 是 RNOH 社区基于 react-native-view-shot 进行鸿蒙适配的三方库,在 OpenHarmony 平台上通过 ArkTS TurboModule 重新实现了原生层:组件截图走 ArkUI componentSnapshot,整屏截图走 window.snapshot,图片编码走 imagePacker。JS 层 API 与上游 react-native-view-shot 保持一致,React Native 鸿蒙应用无需编写原生代码,即可完成视图与整屏截图。
2.3 解决什么问题
一句话总结:为 React Native 鸿蒙应用提供开箱即用的视图截图能力。具体包括:
- 指定视图截图(captureRef,传入组件 ref 或 tag);
- 整屏截图(captureScreen,截取当前窗口);
- 多种输出格式(png / jpg / webp);
- 多种返回结果(tmpfile 临时文件 / base64 / data-uri);
- 输出尺寸缩放(width / height 参数);
- 临时文件释放(releaseCapture,带路径安全校验);
- 组件化用法(ViewShot 组件,支持 mount / update / continuous 三种自动截图模式)。
三、功能介绍
| 功能 | 说明 | 适用场景 |
|---|---|---|
| 指定视图截图 | captureRef(ref, options),截取某个组件区域 | 分享海报、生成图片凭证 |
| 整屏截图 | captureScreen(options),截取当前窗口 | 页面快照、问题反馈 |
| 临时文件输出 | result=“tmpfile”,返回本地文件路径 | 配合 Image 预览、另存分享 |
| base64 输出 | result=“base64” 或 “data-uri” | 直接上传、内嵌展示 |
| 格式与质量 | format=“png” / “jpg”,quality 0.0-1.0 | 平衡清晰度与体积 |
| 尺寸缩放 | width / height 指定输出尺寸 | 生成缩略图 |
| 组件化自动截图 | ViewShot 组件 captureMode=“mount” / “update” / “continuous” | 挂载即截图、内容变更即截图、连续截图 |
| 临时文件释放 | releaseCapture(uri) 释放 tmpfile | 避免临时目录膨胀 |
四、使用方法
4.1 引入三方库
在 RNOH 工程中接入该库需要完成两个配置(npm 依赖本地引入、HAR 包引用),另需修复当前发布版 HAR 的一处运行时缺陷。该库支持 autolinking,无需手动注册 Package。
第一步:克隆适配仓库并添加 npm 依赖
将适配仓库克隆到根 node_modules:
cd node_modules/@react-native-oh-tpl
git clone https://atomgit.com/CPF-RN/rntpc_react-native-view-shot.git react-native-view-shot
在 tester 的 package.json 的 dependencies 中添加:
{
"dependencies": {
"@react-native-oh-tpl/react-native-view-shot": "file:../../node_modules/@react-native-oh-tpl/react-native-view-shot"
}
}
执行 pnpm install 拉取依赖。
第二步:添加 HAR 包引用
在 harmony/oh-package.json5 的 dependencies 中添加 HAR 文件引用:
{
"dependencies": {
"@react-native-ohos/react-native-view-shot": "file:../../../node_modules/@react-native-oh-tpl/react-native-view-shot/harmony/view_shot.har"
}
}
注意 HAR 路径从 oh-package.json5 所在目录(harmony/)算起,回退三级到根 node_modules。路径写错会导致 ohpm 安装失败。
第三步:修复 HAR 打包缺陷(重要)
当前发布的 view_shot.har(4.0.0-rc.1)存在一个运行时缺陷——ViewShotTurboModule.ets 使用了 getContext(this) 获取上下文,该 API 在当前 SDK 中不存在,JS 侧首次访问 ViewShotTurboModule 时会直接抛错:
Error: Exception in HostObject::get for prop 'ViewShotTurboModule': getContext is not defined
解决方法是从源码目录重建 HAR。项目根目录下提供了 scripts/rebuild-har-view-shot.js 脚本,执行即可:
node scripts/rebuild-har-view-shot.js
该脚本会:
- 从克隆的源码目录(4.0.1-rc.1,已改为构造器注入 ctx)完整重建 HAR 文件(tar.gz 格式);
- 验证所有关键文件存在(Index.ets、ViewShotPackage.ets、ViewShotTurboModule.ets、C++ 源码等);
- 检查 ViewShotTurboModule.ets 不含 getContext(this),防止旧代码回归;
- 验证 Index.ets 包含正确的 default export;
- 自动清除 ohpm 缓存,确保下次安装时重新解压。
执行完成后,运行 ohpm install 重新解压 HAR,然后 Clean Build 即可。
4.2 核心 API
库导出一个 ViewShot 组件和三个命令式方法:
import ViewShot, {captureRef, captureScreen, releaseCapture} from '@react-native-oh-tpl/react-native-view-shot';
// 方式一:命令式截取指定视图(传 ref)
const uri = await captureRef(viewShotRef, {
format: 'png', // 输出格式:png | jpg | webp
quality: 1, // 质量 0.0 - 1.0(仅 jpg 等有损格式有效)
result: 'tmpfile', // 结果类型:tmpfile | base64 | data-uri
});
// 方式二:截取整个屏幕
const screenUri = await captureScreen({
format: 'png',
quality: 1,
result: 'tmpfile',
});
// 方式三:组件化,挂载时自动截图
<ViewShot
options={{format: 'png', result: 'tmpfile'}}
captureMode="mount"
onCapture={uri => console.log(uri)}
onCaptureFailure={error => console.error(error)}>
{/* 需要截图的内容 */}
</ViewShot>
// 释放临时文件
releaseCapture(uri);
注意:result=“tmpfile” 返回的是 file:// 开头的本地路径,可直接交给 Image 组件预览。snapshotContentContainer 和 raw 格式在鸿蒙平台不支持,传入会返回明确错误。
4.3 完整示例代码
以下是在 RNOH tester 工程中验证通过的完整示例(ViewShotExample.tsx),提供目标视图截图和整屏截图两个按钮,并在页面底部预览截图结果:
import React, {useRef, useState} from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Image,
ScrollView,
Alert,
Platform,
} from 'react-native';
import ViewShot, {captureRef, captureScreen} from '@react-native-oh-tpl/react-native-view-shot';
export function ViewShotExample() {
const viewShotRef = useRef<any>(null);
const [capturedUri, setCapturedUri] = useState<string | null>(null);
const handleCaptureView = async () => {
try {
const uri = await captureRef(viewShotRef, {
format: 'png',
quality: 1,
result: 'tmpfile',
});
setCapturedUri(uri);
Alert.alert('截图成功', `视图截图已保存: ${uri}`);
} catch (error) {
Alert.alert('截图失败', String(error));
}
};
const handleCaptureScreen = async () => {
try {
const uri = await captureScreen({
format: 'png',
quality: 1,
result: 'tmpfile',
});
setCapturedUri(uri);
Alert.alert('截屏成功', `屏幕截图已保存: ${uri}`);
} catch (error) {
Alert.alert('截屏失败', String(error));
}
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>ViewShot Demo</Text>
<Text style={styles.subtitle}>
Platform: {Platform.OS === 'harmony' ? 'HarmonyOS' : Platform.OS}
</Text>
<View style={styles.card}>
<Text style={styles.cardTitle}>目标视图(待截图区域)</Text>
<ViewShot ref={viewShotRef} style={styles.targetView}>
<View style={styles.coloredBox}>
<Text style={styles.boxText}>React Native</Text>
<Text style={styles.boxSubText}>HarmonyOS</Text>
</View>
<View style={styles.row}>
<View style={[styles.smallBox, {backgroundColor: '#FF6B6B'}]} />
<View style={[styles.smallBox, {backgroundColor: '#4ECDC4'}]} />
<View style={[styles.smallBox, {backgroundColor: '#45B7D1'}]} />
<View style={[styles.smallBox, {backgroundColor: '#96CEB4'}]} />
</View>
<Text style={styles.hintText}>点击上方按钮截取此区域</Text>
</ViewShot>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>操作按钮</Text>
<TouchableOpacity style={styles.button} onPress={handleCaptureView}>
<Text style={styles.buttonText}>截取目标视图</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, {marginTop: 12, backgroundColor: '#34C759'}]}
onPress={handleCaptureScreen}>
<Text style={styles.buttonText}>截取整个屏幕</Text>
</TouchableOpacity>
</View>
{capturedUri ? (
<View style={styles.card}>
<Text style={styles.cardTitle}>截图结果</Text>
<Image
source={{uri: capturedUri}}
style={styles.resultImage}
resizeMode="contain"
/>
<Text style={styles.uriText} numberOfLines={2}>
{capturedUri}
</Text>
</View>
) : null}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {flex: 1, padding: 16, backgroundColor: '#F2F2F7'},
title: {fontSize: 24, fontWeight: '700', marginBottom: 4, color: '#000'},
subtitle: {fontSize: 14, color: '#666', marginBottom: 20},
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
cardTitle: {fontSize: 16, fontWeight: '600', color: '#333', marginBottom: 12},
targetView: {
padding: 16,
backgroundColor: '#F8F9FA',
borderRadius: 8,
alignItems: 'center',
},
coloredBox: {
backgroundColor: '#007AFF',
borderRadius: 12,
padding: 20,
alignItems: 'center',
marginBottom: 12,
width: '100%',
},
boxText: {color: '#fff', fontSize: 20, fontWeight: '700'},
boxSubText: {color: 'rgba(255,255,255,0.8)', fontSize: 14, marginTop: 4},
row: {flexDirection: 'row', gap: 8, marginBottom: 12},
smallBox: {width: 40, height: 40, borderRadius: 8},
hintText: {fontSize: 12, color: '#999'},
button: {
backgroundColor: '#007AFF',
borderRadius: 8,
paddingVertical: 14,
alignItems: 'center',
},
buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'},
resultImage: {
width: '100%',
height: 200,
borderRadius: 8,
backgroundColor: '#F0F0F0',
marginBottom: 8,
},
uriText: {fontSize: 11, color: '#999'},
});
运行效果:页面显示三张白色圆角卡片——第一张是待截图的目标视图(蓝色卡片和四个彩色方块),第二张是两个操作按钮,第三张在截图后出现,展示截图预览和文件路径。点击截取目标视图得到目标区域图片,点击截取整个屏幕得到当前窗口完整画面。
五、FAQ
5.1 常见问题
Q1:编译报 Module ‘react-native’ has no exported member ‘TurboModule’
TypeScript 编译期错误。tester 的 tsconfig 通过 paths 把 react-native 映射到了 react-native-harmony,而 RNOH 的类型入口没有再导出 TurboModule 类型(Libraries/TurboModule/RCTExport 被注释掉了),库源码里 import type {TurboModule} from “react-native” 就会解析失败。
解决方法是修改库源码 src/specs/NativeRNViewShot.ts,把类型导入改为深路径(上游 RN 和 react-native-harmony 都存在该文件):
import type {TurboModule} from "react-native/Libraries/TurboModule/RCTExport";
这是纯类型层面的修改,不影响运行时行为。
Q2:启动红屏报 Exception in HostObject::get for prop ‘ViewShotTurboModule’: getContext is not defined
发布版 HAR(4.0.0-rc.1)的 ViewShotTurboModule.ets 用类字段方式获取上下文,而 getContext 在当前 SDK 中不是全局 API,TurboModule 实例化时立即抛错:
// 旧版(4.0.0-rc.1),当前 SDK 上崩溃
private context: Context = getContext(this);
新版源码(4.0.1-rc.1)已改为通过构造器注入的 ctx 使用上下文,例如获取临时目录:
// 新版(4.0.1-rc.1),正常工作
const path: string = this.ctx.uiAbilityContext.tempDir + `/${title}.${extension}`;
解决方法是执行 node scripts/rebuild-har-view-shot.js 从源码重建 HAR,运行 ohpm install 重新解压,再 Clean Build。脚本内置 getContext(this) 检测,旧代码无法混入。
Q3:截图提示成功、路径也有了,但预览区域一片空白
这是最隐蔽的一个问题。截图文件本身是正常的(可以拉出来验证),但 RNOH 的 Image 组件无法加载裸绝对路径,加载失败后显示占位背景色,看起来像白图。设备日志里能看到明确的加载失败记录:
W Ace: GetAsset failed: data/storage/el2/base/haps/entry/temp/ComponentSnapshot-xxx.png
验证方法:把截图文件从应用沙箱拉到本地查看,内容完好:
hdc file recv /data/app/el2/100/base/com.rnoh.tester/haps/entry/temp/ComponentSnapshot-xxx.png ./check.png
根因是库的 ArkTS 侧 savePhotoOnDevice 用裸路径 resolve,修改 src/main/ets/ViewShotTurboModule.ets,给返回路径加上 file:// 前缀:
// 裸路径,RNOH Image 加载失败
resolve(path);
// 加 file:// 前缀,正常预览
resolve('file://' + path);
库的 releaseCapture 内部已兼容 file:// 前缀(会先剥离再校验 tempDir),无需额外处理。修改后重新执行 node scripts/rebuild-har-view-shot.js 并 Clean Build。
Q4:重建或替换 HAR 后 Sync,运行行为仍是旧版
ohpm 有缓存机制。即使 HAR 文件已更新,只要包名 + 版本哈希没变,ohpm 不会重新解压到 oh_modules,编译时读到的仍然是旧文件。可以通过检查 oh_modules 中的代码确认缓存是否生效:
# 输出仍含 getContext( 则缓存未更新
grep getContext harmony/oh_modules/@react-native-ohos/react-native-view-shot/src/main/ets/ViewShotTurboModule.ets
解决方法是手动清除 ohpm 缓存目录,然后重新安装:
rm -rf harmony/oh_modules/.ohpm/@react-native-ohos+react-native-view-shot*
rm -rf harmony/oh_modules/@react-native-ohos/react-native-view-shot
ohpm install
rebuild-har-view-shot.js 脚本已内置自动清缓存步骤,通过脚本重建 HAR 时通常无需手动执行本节的清理命令;若仅手动替换 HAR 文件(不经脚本),则必须在 ohpm install 前执行上述清理。
Q5:HAR 安装失败(ohpm Sync 报错)
检查 oh-package.json5 中 HAR 路径是否正确。路径从 harmony/ 目录算起,到根 node_modules 需要回退三级:
"@react-native-ohos/react-native-view-shot": "file:../../../node_modules/@react-native-oh-tpl/react-native-view-shot/harmony/view_shot.har"
路径层级写错会导致 ohpm 找不到 HAR 文件。
Q6:真机安装失败(HAP 安装报错)
用 DevEco Studio 打开工程,进入 File > Project Structure > Signing Configs,勾选 Automatically generate signature 后重新运行。
5.2 库本身存在问题:如何提交 Issue
- 打开适配仓库 https://atomgit.com/CPF-RN/rntpc_react-native-view-shot 的 Issues 页面,点击"新建 Issue";
- 标题格式:[Bug] 一句话现象,例如 [Bug] 截图返回裸路径导致 Image 无法预览;
- 正文必须包含:复现步骤 / 期望结果 / 实际结果 / 设备与系统版本 / RNOH 版本 / 最小复现代码、日志或截图;
- 提交后跟踪仓库维护者回复,修复发布后关注对应 Tag 更新依赖版本。
5.3 能自己解决:如何提交 PR
- Fork 适配仓库 https://atomgit.com/CPF-RN/rntpc_react-native-view-shot 到个人 AtomGit 账号;
- git clone 自己的 fork,基于 master 新建分支:git checkout -b fix/xxx;
- 修改代码(如 ArkTS 侧 TurboModule 实现、JS 侧类型声明)并 commit;
- push 到自己的 fork,在原仓库发起 Pull Request;
- PR 描述写清:问题背景 / 修改点 / 鸿蒙真机验证结果(附运行截图),等待维护者评审合入。
六、其他内容
6.1 总结
@react-native-ohos/react-native-view-shot 为 React Native 鸿蒙应用补齐了视图截图能力。底层是 ArkTS TurboModule:组件截图走 ArkUI componentSnapshot,整屏截图走 window.snapshot,编码落盘走 imagePacker,同时提供 ViewShot 组件支持挂载 / 更新 / 连续三种自动截图模式。接入时注意四点:oh-package.json5 中 HAR 路径层级要正确、发布版 HAR 存在 getContext 崩溃缺陷需通过 rebuild-har-view-shot.js 脚本重建、tmpfile 返回路径需带 file:// 前缀否则 Image 预览空白、tsconfig 将 react-native 映射到 RNOH 时库的类型导入需改用深路径。建议生产环境锁定依赖版本,遇到问题优先查看适配仓库 Issues。
6.2 三个已验证库的横向对比
| 维度 | file-selector | datetimepicker | view-shot |
|---|---|---|---|
| 组件类型 | TurboModule(原生模块) | Fabric UI 组件(原生视图) | TurboModule(原生模块) |
| 调用方式 | FileSelector.Show({…}) 命令式 | <DateTimePicker … /> 声明式 | captureRef / captureScreen 命令式 + ViewShot 组件 |
| 注册方式 | 需手动创建本地 Package | autolinking 自动注册 | autolinking 自动注册 |
| HAR 状态 | 基于旧版框架,需本地替代 | 有打包缺陷,需 rebuild-har.js 修复 | 旧版 HAR 运行时崩溃,需 rebuild-har-view-shot.js 修复 |
| 平台适配注意 | 回调必须放进单个 props 对象 | Fabric 组件必须指定 style 尺寸 | tmpfile 路径需 file:// 前缀 |
6.3 参考链接
RNOH 社区入口和三方库资源统一在这里:
更多推荐


所有评论(0)