鸿蒙5.0 APP开发案例分析:基于RCP的文件上传与下载
Remote Communication Kit(远场通信服务)是HarmonyOS系统提供对HTTP发起数据请求的NAPI封装,通过@hms.collaboration.rcp(后续简称RCP)模块将相关能力开放给开发者。在现代应用开发中,文件上传与下载是较为常见的需求,本文将通过对带进度的上传下载、断点续传、后台文件上传下载场景的详细讲解,为开发者提供基于RCP的文件上传与下载的开发实践。
往期推文全新看点(文中附带全新鸿蒙5.0全栈学习笔录)
✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
✏️ 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
✏️ 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
✏️ 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
✏️ 记录一场鸿蒙开发岗位面试经历~
✏️ 持续更新中……
概述
Remote Communication Kit(远场通信服务)是HarmonyOS系统提供对HTTP发起数据请求的NAPI封装,通过@hms.collaboration.rcp(后续简称RCP)模块将相关能力开放给开发者。
在现代应用开发中,文件上传与下载是较为常见的需求,本文将通过对带进度的上传下载、断点续传、后台文件上传下载场景的详细讲解,为开发者提供基于RCP的文件上传与下载的开发实践。
关键技术说明
文件下载
- 推荐方式:使用RCP中提供封装好的 Session.downloadToFile() 方法进行文件下载,开发者只需提供下载到本地的文件地址,即可快速便捷实现下载请求。同时RCP提供 Session.downloadToStream() 方法实现流式下载。
- 自定义配置:若上述方法无法满足特定场景需求,开发者可使用 Session.get() 方法或 Session.fetch() 方法自行配置请求相关参数来实现文件下载。
文件上传
- 推荐方式:使用RCP中提供封装好的Session.uploadFromFile()方法进行文件上传,开发者只需提供本地要上传的文件地址,即可快速便捷实现文件上传请求。同时RCP提供Session.uploadFromStream()方法实现流式上传。
- 自定义配置:若上述方法无法满足特定场景需求,开发者可使用Session.post()方法或Session.fetch()方法自行配置请求相关参数来实现文件上传。
- 请求内容类型:RCP提供以下三种类型用于构造请求内容,使用这些类型时RCP默认会设置http请求头和请求体格式。
| 类型 | 可使用该类型的请求方法 | 对应原始http请求头和请求体类型 |
|---|---|---|
| rcp.UploadFromFile | Session.uploadFromFile()、Session.post()、Session.fetch() | 请求头content-type为application/octet-stream;请求体使用binary二进制格式 |
| rcp.UploadFromStream | Session.uploadFromStream()、Session.post()、Session.fetch() | 请求头content-type为application/octet-stream;请求体使用binary二进制格式 |
| rcp.MultipartForm | Session.post()、Session.fetch() | 请求头content-type为multipart/form-data;请求体使用form-data格式 |
带进度的上传下载
场景描述
用户可选择服务器文件并点击下载,下载过程中能够显示下载进度。点击上传按钮,可拉起系统相册,选中相册文件并确认后,将其上传至服务器,并显示上传进度。
- 下载效果图

- 上传效果图

实现原理
关键技术
- 由于HarmonyOS系统的安全机制限制,RCP接口不能直接访问相册路径进行文件传输。可以先将相册文件拷贝到沙箱路径,然后从沙箱路径进行上传操作;下载时则可以先将文件下载到沙箱路径,再拷贝到相册文件中。
- 在创建http会话的rcp.sessionConfiguration参数类型中,可配置HTTP请求/响应过程中的特定操作,其中rcp.httpEventHandler属性可定义响应处理程序的回调函数来实现文件传输进度的监听。下载请求返回的响应头中需包含content-length字段,否则在下载进度的回调方法rcp.onDownloadProgress()中获取不到文件大小,无法计算下载进度。
下载流程
- 创建http会话,配置监听下载进度的回调方法rcp.onDownloadProgress()。
- 使用Session.downloadToFile()方法下载到沙箱路径。
- 使用phAccessHelper.showAssetsCreationDialog()方法获取相册路径。
- 使用fs.copyFileSync()方法将沙箱文件拷贝到相册。
上传流程
- 使用PhotoPicker.select()方法拉起系统相册,选中相册文件。
- 使用fs.copyFileSync()方法将相册文件拷贝到沙箱路径中。
- 创建http会话,配置监听上传进度的回调方法rcp.onUploadProgress()。
- 使用Session.post()方法发送上传请求。
开发步骤
说明
在使用Remote Communication Kit相关能力前,需配置以下权限。
- ohos.permission.INTERNET:用于应用的权限,决定是否允许应用访问互联网。
- ohos.permission.GET_NETWORK_INFO:用于获取设备网络信息的 API 。
为避免在每个RCP发起请求的响应中重复判断状态码和处理异常情况,可利用 rcp.Interceptor 提供的拦截器对响应进行统一处理。对于非200、206状态码的响应, 使其返回失败的promise,并对请求失败情况进行统一的弹窗提示。具体的拦截逻辑可根据业务需求灵活设置。
<pre class="screen prettyprint linenums hljs language-typescript" data-highlighted="yes" style="box-sizing: border-box; margin: 0px 0px 1em; padding: 0px; font-family: HarmonyOSHans-Regular, monospace, HarmonyOSHans-fallback, PingFangSC-Regular, "Microsoft YaHei", Arial, Helvetica, sans-serif; vertical-align: baseline; border: 0px; font-size: 14px; overflow: auto; line-height: 1.45; overflow-wrap: break-word; white-space: pre-wrap; border-radius: 0px 0px 16px 16px; position: relative;">
1. export class StatusCodeInterceptor implements rcp.Interceptor {
2. async intercept(context: rcp.RequestContext, next: rcp.RequestHandler): Promise<rcp.Response> {
3. const url = context.request.url;
4. return next.handle(context).then((res: rcp.Response) => {
5. if ([200, 206].includes(res.statusCode)) {
6. return Promise.resolve(res);
7. } else {
8. const message = `Failed to ${url}: statusCode is ${res.statusCode}, message is ${res.toString()}`;
9. Logger.error(message);
10. showErrorMessage(message);
11. return Promise.reject(new Error(message));
12. }
13. }).catch((err: BusinessError) => {
14. Logger.error(`Failed to ${url}: Code is ${err.code}, message is ${err.data}`);
15. // cancel request don't show err message prompt.
16. if (err.code !== 1007900992) {
17. showErrorMessage(JSON.stringify(err.data));
18. }
19. return Promise.reject(err);
20. });
21. }
22. }
</pre>
在 rcp.sessionConfiguration 中配置请求的基地址baseAddress、相关拦截器interceptors、超时时间timeout和添加进度监听回调方法的对象httpEventsHandler。
function genSessionConfig(httpEventsHandler?: rcp.HttpEventsHandler) {
const config: rcp.SessionConfiguration = {
baseAddress: BASE_URL,
interceptors: [new StatusCodeInterceptor()],
requestConfiguration: {
tracing: { httpEventsHandler },
transfer: {
timeout: {
connectMs: 1000 * 60 * 20,
transferMs: 1000 * 60 * 20
}
}
}
};
return config;
}
- 下载步骤
- 使用 rcp.createSession() 方法创建http会话,使用Session.downloadToFile()方法下载到沙箱路径,注意下载完成后使用Session.close()方法关闭会话,释放相关资源。
export function download(fileName: string, httpEventsHandler: rcp.HttpEventsHandler) {
const destPath = getSandboxPath(fileName);
const rcpSession = rcp.createSession(genSessionConfig(httpEventsHandler));
const downloadTo: rcp.DownloadToFile = {
kind: 'file',
file: destPath
};
return rcpSession.downloadToFile(`/${fileName}`, downloadTo)
.then(() => destPath)
.finally(() => {
rcpSession.close();
});
}
- 通过phAccessHelper.showAssetsCreationDialog()方法获取相册路径,并将下载到沙箱路径的文件拷贝到相册路径中。
export async function saveImageToAlbum(sandboxPath: string) {
const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(getContext());
const fileNameExtension = sandboxPath.split('.').pop() || 'png';
const photoCreationConfig: photoAccessHelper.PhotoCreationConfig = {
fileNameExtension,
photoType: getPhotoType(fileNameExtension),
};
const uri: string = fileUri.getUriFromPath(sandboxPath);
const desFileUris: string[] = await phAccessHelper.showAssetsCreationDialog([uri], [photoCreationConfig]);
const filePath = desFileUris[0];
if (!filePath) throw new Error('photo assets permission denied');
copyFileSync(sandboxPath, filePath);
return filePath;
}
- 上传步骤
- PhotoPicker.select() 方法拉起相册模块,选择相册文件后在该方法回调函数中将相册文件拷贝到沙箱路径。
export async function selectImagesFromAlbum(maxNumber: number = 1): Promise<string[]> {
const photoPicker = new photoAccessHelper.PhotoViewPicker();
const photoSelectOptions: photoAccessHelper.PhotoSelectOptions = {
MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE,
maxSelectNumber: maxNumber
};
return photoPicker.select(photoSelectOptions).then((photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
const filePaths = photoSelectResult.photoUris;
return filePaths.map(filePath => {
const imageName = filePath.split('/').pop() || '';
const sandboxPath = getSandboxPath(imageName);
copyFileSync(filePath, sandboxPath);
return sandboxPath;
});
});
}
- 使用rcp.createSession()方法创建http会话,使用rcp.MultipartForm类型构造请求体,使用Session.post()方法发起上传文件请求。
export function upload(srcPath: string, httpEventsHandler: rcp.HttpEventsHandler) {
const session = rcp.createSession(genSessionConfig(httpEventsHandler));
const formData = new rcp.MultipartForm({
file: {
contentOrPath: srcPath
}
});
return session.post('/', formData).finally(() => {
session.close();
});
}
断点续传
场景描述
在下载一些大文件时,可能会出现网络环境不稳定的情况,一旦网络波动导致传输中断,则需要从头开始重新下载,会极大浪费时间与流量,此时断点续传功能就显得尤为重要。本节通过手动暂停下载方式,讲解断点续传的关键原理和步骤。

实现原理
关键技术
- 断点续传的基本原理就是利用http请求头中的range字段,对文件进行部分请求。
- 使用 rcp.Request 类型中的transferRange字段进行分片请求起始位置的配置。使用 Session.fetch(request: Request) 方法发送下载请求。
- 通过 fs.writeSync() 方法写入文件,利用offset参数保证每一块分片写入的顺序正确。
- 使用 Session.cancel(request: Request) 取消当前请求中断下载。
开发流程
- 使用Session.head()方法发送head请求获取文件的大小。
- 发送下载文件请求,流式接收下载数据,同步写入文件,通过文件总大小和已下载大小计算下载进度。
- 暂停下载时,使用Session.cancel()方法取消当前分片下载。
- 继续下载时,从文件中读取已下载数据大小,设置rcp.Request中的transferRange字段,重复步骤2操作。
- 直至下载请求成功,则文件下载完成。

开发步骤
- 使用Session.head()方法发送head请求,通过响应头中的content-length属性获取文件大小。
export function getFileSize(fileName: string): Promise<number> {
const session = rcp.createSession(genSessionConfig());
return session.head(`/${fileName}`).then(res => {
const contentLength = res.headers['content-length'];
return contentLength ? Number(contentLength): 0;
}).finally(() => {
session.close();
});
}
- 每次开始下载时,从文件中读取已下载并写入本地文件的大小,配置transferRange字段,用fetch请求下载文件,使用currentRequest属性记录当前下载请求(用于暂停时取消当前请求),使用流式传输同步将下载数据通过 fs.writeSync() 写入本地文件,通过offset属性控制写入文件位置,根据已下载分片总大小和文件总大小计算下载进度。
async start() {
this.totalSize = this.totalSize || await getFileSize(this.url)
const request = new rcp.Request(this.url);
const writeSync: (buffer: ArrayBuffer) => void = buffer => {
fileIo.writeSync(this.file.fd, buffer, { offset: this.downloadedSize })
this.downloadedSize = fileIo.statSync(this.file.fd).size;
this.onProgress(this.totalSize, this.downloadedSize);
}
request.destination = {
kind: 'stream',
stream: { writeSync }
};
request.transferRange = { from: this.downloadedSize };
this.currentRequest = request;
return this.session.fetch(request).then(() => {
this.session.close();
fileIo.close(this.file)
});
}
- pause()方法用于暂停下载,使用Session.cancel() 方法取消正在进行的分片下载请求。
pause() {
this.session.cancel(this.currentRequest);
}
后台文件上传下载
场景描述
在应用切换到后台时,需要继续保持文件上传或下载,在后台静默完成文件传输。

实现原理
关键技术
- 目前鸿蒙系统的规则是应用退到后台2s会被冻结,同时释放相关网络资源,上传下载请求会被中断。需使用backgroundTaskManager.startBackgroundRunning()方法申请长时任务。
- 使用backgroundTaskManager.stopBackgroundRunning()方法取消长时任务。
开发流程
- 申请网络类型后台长时任务。
- 在申请长时任务的回调中发送上传下载请求。
开发步骤
- 在申请长时任务前,需配置相关权限,长时任务权限类型为网络类型。
- 使用backgroundTaskManager.startBackgroundRunning()方法申请后台长时任务。申请长时任务成功后执行上传下载任务task(),在请求完成后使用backgroundTaskManager.stopBackgroundRunning()方法关闭长时任务。
async startBackgroundTask(task: () => Promise<void>) {
try {
const wantAgentObj = await wantAgent.getWantAgent(this.getWantAgentInfo());
await backgroundTaskManager.startBackgroundRunning(
getContext(this),
backgroundTaskManager.BackgroundMode.DATA_TRANSFER,
wantAgentObj
);
task().finally(() => {
backgroundTaskManager.stopBackgroundRunning(getContext(this));
});
} catch (err) {
Logger.error('Failed to start background task', JSON.stringify(err));
showErrorMessage('A background task is running');
}
}
- 上传下载请求的发送根据需求选择合适的方式即可。
更多推荐
所有评论(0)