往期推文全新看点(文中附带全新鸿蒙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()中获取不到文件大小,无法计算下载进度。

下载流程

  1. 创建http会话,配置监听下载进度的回调方法rcp.onDownloadProgress()。
  2. 使用Session.downloadToFile()方法下载到沙箱路径。
  3. 使用phAccessHelper.showAssetsCreationDialog()方法获取相册路径。
  4. 使用fs.copyFileSync()方法将沙箱文件拷贝到相册。

上传流程

  1. 使用PhotoPicker.select()方法拉起系统相册,选中相册文件。
  2. 使用fs.copyFileSync()方法将相册文件拷贝到沙箱路径中。
  3. 创建http会话,配置监听上传进度的回调方法rcp.onUploadProgress()。
  4. 使用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, &quot;Microsoft YaHei&quot;, 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;
}
  • 下载步骤
  1. 使用 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();
    });
}
  1. 通过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;
}
  • 上传步骤
  1. 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;
    });
  });
}
  1. 使用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) 取消当前请求中断下载。

开发流程

  1. 使用Session.head()方法发送head请求获取文件的大小。
  2. 发送下载文件请求,流式接收下载数据,同步写入文件,通过文件总大小和已下载大小计算下载进度。
  3. 暂停下载时,使用Session.cancel()方法取消当前分片下载。
  4. 继续下载时,从文件中读取已下载数据大小,设置rcp.Request中的transferRange字段,重复步骤2操作。
  5. 直至下载请求成功,则文件下载完成。

开发步骤

  • 使用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()方法取消长时任务。

开发流程

  1. 申请网络类型后台长时任务。
  2. 在申请长时任务的回调中发送上传下载请求。

开发步骤

  • 在申请长时任务前,需配置相关权限,长时任务权限类型为网络类型。
  • 使用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');
  }
}
  • 上传下载请求的发送根据需求选择合适的方式即可。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐