# RNSaveMedia **Repository Path**: scenario-samples/rnsave-media ## Basic Information - **Project Name**: RNSaveMedia - **Description**: 在应用开发过程中,应用会遇到需要将媒体资源(图片或者视频)保存到设备图库的情况。通过安全控件的方式,应用无需申请相册管理模块权限'ohos.permission.WRITE_IMAGEVIDEO'就可以将用户指定的媒体资源保存到图库中。 本示例演示RN应用中使用Fabric自定义组件实现RN侧引用ArkTS侧的SaveButton,保存媒体资源文件到设备图库。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-02-06 - **Last Updated**: 2026-02-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RN使用安全控件保存媒体资源文件示例 ## 场景介绍 在应用开发过程中,应用会遇到需要将媒体资源(图片或者视频)保存到设备图库的情况。通过安全控件的方式,应用无需申请相册管理模块权限'ohos.permission.WRITE_IMAGEVIDEO'就可以将用户指定的媒体资源保存到图库中。 本示例演示RN应用中使用[Fabric](https://gitcode.com/openharmony-sig/ohos_react_native/blob/master/docs/zh-cn/%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6.md)自定义组件实现RN侧引用ArkTS侧的[SaveButton](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-security-components-savebutton),保存媒体资源文件到设备图库。 ## 效果预览 ## 实现思路 1.声明Fabric。 ``` import {HostComponent, ViewProps} from 'react-native'; import { DirectEventHandler, Int32, } from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; export type OnSuccessEventData = Readonly<{}>; export type OnFailureEventData = Readonly<{ errorCode: Int32; errorMessage: string; }>; export type CustomButtonParams = Readonly<{ buttonStyle?: Int32; icon?: Int32; text?: Int32; fontColor?: Int32; backgroundColor?: string; borderRadius?: Int32; }>; interface SaveButtonProps extends ViewProps { uri: string; title?: string; customButtonParams?: CustomButtonParams; onSuccess?: DirectEventHandler; onFailure?: DirectEventHandler; } export default codegenNativeComponent( 'SaveButton', ) as HostComponent; ``` 2.ArkS侧实现。 ``` import { RNOHContext, RNViewBase } from '@rnoh/react-native-openharmony'; import { RNC } from '@rnoh/react-native-openharmony/generated'; import { fileIo as fs } from '@kit.CoreFileKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { common } from '@kit.AbilityKit'; import { http } from '@kit.NetworkKit'; @Component export struct RNSaveButton { public static readonly NAME = RNC.SaveButton.NAME; public ctx!: RNOHContext; public tag: number = 0; @State descriptorWrapper: RNC.SaveButton.DescriptorWrapper = {} as RNC.SaveButton.DescriptorWrapper; private eventEmitter: RNC.SaveButton.EventEmitter | undefined = undefined; private cleanUpCallbacks: (() => void)[] = []; uiContext: UIContext = this.getUIContext(); context: common.UIAbilityContext = this.uiContext.getHostContext() as common.UIAbilityContext; aboutToAppear() { this.eventEmitter = new RNC.SaveButton.EventEmitter(this.ctx.rnInstance, this.tag); this.onDescriptorWrapperChange(this.ctx.descriptorRegistry.findDescriptorWrapperByTag(this.tag)!); this.cleanUpCallbacks.push(this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag, (descriptor) => { this.onDescriptorWrapperChange(new RNC.SaveButton.DescriptorWrapper(descriptor)); } )); } private onDescriptorWrapperChange(descriptorWrapper: RNC.SaveButton.DescriptorWrapper) { this.descriptorWrapper = descriptorWrapper; } aboutToDisappear() { this.cleanUpCallbacks.forEach(cb => cb()); } build() { RNViewBase({ ctx: this.ctx, tag: this.tag }) { SaveButton({ icon: this.descriptorWrapper.props.customButtonParams?.icon, text: this.descriptorWrapper.props.customButtonParams?.text, buttonType: this.descriptorWrapper.props.customButtonParams?.buttonStyle }) .onClick(async (_event: ClickEvent, result: SaveButtonOnClickResult) => { if (result === SaveButtonOnClickResult.SUCCESS) { try { let sourceUri = this.descriptorWrapper.props.uri; let photoType = photoAccessHelper.PhotoType.IMAGE; let extension = sourceUri.split('.').pop() ?? ''; let options: photoAccessHelper.CreateOptions = {}; let media: ArrayBufferLike | null = null; if (['mp4', 'mov'].includes(extension?.toLowerCase())) { photoType = photoAccessHelper.PhotoType.VIDEO } if (this.descriptorWrapper.props.title) { options.title = this.descriptorWrapper.props.title; } if (sourceUri.startsWith('asset://')) { media = this.context.resourceManager.getRawFileContentSync(sourceUri.replace('asset://', this.ctx.rnInstance.getAssetsDest())).buffer; } else if (sourceUri.startsWith('resource://')) { media = this.context.resourceManager.getRawFileContentSync(sourceUri.replace('resource://rawfile/', '')) .buffer; } else if (sourceUri.startsWith('http://') || sourceUri.startsWith('https://')) { let httpResult = (await http.createHttp().request(sourceUri)); if (httpResult.responseCode === 200 && httpResult.result instanceof ArrayBuffer) { media = httpResult.result; } } else { if (!fs.accessSync(sourceUri)) { this.eventEmitter?.emit('failure', { errorCode: -1, errorMessage: '保存失败' }) return; } let file = fs.openSync(sourceUri, fs.OpenMode.READ_ONLY); media = new ArrayBuffer(fs.statSync(file.fd).size); fs.readSync(file.fd, media); } if (!media) { this.eventEmitter?.emit('failure', { errorCode: -1, errorMessage: '保存失败' }) return; } let helper = photoAccessHelper.getPhotoAccessHelper(this.context); // onClick触发后一分钟内通过createAsset接口创建图片文件,一分钟后createAsset权限收回。 let uri = await helper.createAsset(photoType, extension, options); // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制 let file = fs.openSync(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); fs.writeSync(file.fd, media); fs.closeSync(file.fd); this.eventEmitter?.emit('success', new Object()); } catch (error) { console.error(`errorCode is ${error.code}, errorMessage is ${error.message}`); this.eventEmitter?.emit('failure', { errorCode: error.code, errorMessage: error.message }); } } else { this.eventEmitter?.emit('failure', { errorCode: -1, errorMessage: '设置权限失败' }); } }) .width(this.descriptorWrapper.width) .height(this.descriptorWrapper.height) .borderRadius(this.descriptorWrapper.props.customButtonParams?.borderRadius) .backgroundColor(this.descriptorWrapper.props.customButtonParams?.backgroundColor) .fontColor(this.descriptorWrapper.props.customButtonParams?.fontColor) } } } ``` 3.RN侧保存。 ``` { Toast.show('保存成功', Toast.SHORT); }} onFailure={e => { console.error( `errCode: ${e.nativeEvent.errorCode},errMessage: ${e.nativeEvent.errorMessage}`, ); }} /> ``` ## 说明 - RN工程目录下执行`npm i`命令安装依赖。 - 用DevEco Studio打开harmony工程。 - RN工程目录下执行`npm run dev`命令安装包和生成bundle。 ## 约束与限制 - 本示例支持API Version 20 Release及以上版本。 - 本示例支持HarmonyOS 6.0.0 Release SDK及以上版本。 - 本示例需要使用DevEco Studio 6.0.0 Release及以上版本进行编译运行。 - 本示例使用0.72.96版本RNOH。 ## 工程目录 ``` RNSaveMedia // RN工程 ├──harmony // harmony工程 │ ├──entry/src/main/cpp // native相关配置 │ │ ├──CMakeLists.txt │ │ └──PackageProvider.cpp │ ├──entry/src/main/ets │ │ ├──entryability │ │ │ └──EntryAbility.ets │ │ ├──pages │ │ │ └──Index.ets // harmony侧入口 │ │ ├──RNComponent │ │ │ └──RNSaveButton.ets // 自定义组件实现 │ │ └──RNPackagesFactory.ets │ └──entry/src/main/resources/rawfile // bundle、静态资源存放目录 ├──App.tsx // 截图保存图库实现 ├──rn-save-button // Fabric │ ├──src/specs │ │ └──SaveButtonNativeComponent.ts │ └──index.ts ├──index.js // 入口文件 ├──metro.config.js // 打包配置 ``` ## 参考文档 [Fabric自定义组件](https://gitcode.com/openharmony-sig/ohos_react_native/blob/master/docs/zh-cn/%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6.md) [RNOH使用文档](https://gitcode.com/openharmony-sig/ohos_react_native/blob/master/docs/zh-cn/%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA.md)