rcp表单格式数据传输
===========

#### 问题现象

表单传输在列表筛选、携带参数页面跳转、用户身份认证、内容创作与发布、电子商务交易、文件上传等场景中有着广泛应用。那么，如何使用rcp模块实现表单数据的传输功能呢？

#### 背景知识

1.  HTTP协议为表单数据传输主要提供了两种表单传输场景：简单字符串键值对表单数据传输和多表单混合数据传输。
    *   键值对以等号连接，多个参数以&符号分隔，表单数据直接附加在URL地址栏中，随请求头一同发送，请求体为空，或者请求体采用application/x-www-form-urlencoded格式的编码发送表单数据。
    *   采用multipart/form-data格式的数据编码，表单数据位于HTTP请求体中，不暴露在URL中，在敏感信息传输、混合数据传输、大数据传输等场景广泛使用。
2.  rcp模块为上述两种数据编码方式提供了多种接口
    *   [URLOrString](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/remote-communication-rcp#section320653742718)：作为HTTP/HTTPS地址的入参，包括[URL](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/remote-communication-rcp#section66401230152412)类型和string类型，URL可通过[append](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-url#append9)接口将表单数据拼接到请求地址中传输。
    *   [Form](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/remote-communication-rcp#section1188301010515)：HTTP简单的表格数据，通过POST方法将表单数据以application/x-www-form-urlencoded编码方式上传表单数据。
    *   [MultipartForm](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/remote-communication-rcp#section1420174317517)：HTTP多部分表格数据传输。

#### 解决方案

开发准备，申请获取网络权限：[ohos.permission.INTERNET](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/permissions-for-all#ohospermissioninternet)。

**场景一：简单字符串表单数据传输**

*   **方案1：使用URL对参数进行拼接，实现表单数据传输。**
    
    1.  新建URLParams类。
        
        let paramsObject = new url.URLParams();
        
    2.  添加表单参数。
        
        paramsObject.append('fod', '3'); _// 开发者自行设置参数_
        paramsObject.append('fed', '4'); _// 开发者自行设置参数_
        
    3.  将表单参数与基地址合成完整URL。
        
        let urlWithParam = url.URL.parseURL('/get?' + paramsObject.toString(), baseUrl);
        
    4.  新建会话发送表单数据。
        
        let session = rcp.createSession();
        session.get(urlWithParam).then((response) => {
          console.info(\`urlWithParamGetData Succeeded in getting the response ${response}\`);
        }).catch((err: BusinessError) => {
          console.error(\`err: err code is ${err.code}, err message is ${JSON.stringify(err)}\`);
        });
        
    
    运行日志如下：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/34/v3/9XKEUTQeQ6i6VSNfq150Ag/zh-cn_image_0000002658851751.png?HW-CC-KV=V1&HW-CC-Date=20260709T134215Z&HW-CC-Expire=86400&HW-CC-Sign=5FFE9DF37E57302A3BD23EEAD5A209F90A21BA8B73C0AFB77EF2BA8A0F9F75CB)
    
*   **方案2：使用rcp模块中的Form类型数据传输表单数据。**
    
    1.  使用参数新建Form类。
        
        const simpleForm = new rcp.Form({
          'key1': 'value1',
          'key2': \['valueList0', 'valueList1'\],
        }); _// 开发者自行设置参数_
        
    2.  指定表单数据的传输顺序，部分服务端校验传输参数的顺序，顺序不对可能导致传输失败。
        
        simpleForm.keys = \['key2', 'key1'\]; _// 开发者自行设置参数顺序_
        
    3.  新建会话发送表单数据多部分表单数据。
        
        const session = rcp.createSession();
        session.post('xxx.xxx.xxx', simpleForm).then((response) => { _// 开发者自行设置请求地址_
          console.info(\`Succeeded in getting the response ${response}\`);
        }).catch((err: BusinessError) => {
          console.error(\`err: err code is ${err.code}, err message is ${JSON.stringify(err)}\`);
        });
        
    
    运行日志如下，如果用户不在请求头设置数据大小，rcp模块会自动计算：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/8f/v3/Id86sy_MTH2RR0n0vjLFqQ/zh-cn_image_0000002628772386.png?HW-CC-KV=V1&HW-CC-Date=20260709T134215Z&HW-CC-Expire=86400&HW-CC-Sign=E1F65630E96C73EF4B593F7049B0E2F4CAF813C25C955E1E33FCF17881E45E3F "点击放大")
    

**场景二：复杂混合表单数据传输**

1.  使用picker组件选择图片文件并将文件复制到沙箱路径，rcp模块的MultipartForm传输只支持沙箱路径下的文件路径、读取的文件buffer数据和读取文件数据的回调函数作为入参，不支持文件流和文件操作符作为入参。
    
    let sandBoxFileName = await copyFileToSandBox(sandboxDir);
    
2.  构建传输文件的表单数据。详细说明见[rcp如何实现文件上传](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-remote-communication-15)。
    
    let multiFormFieldValue = this.buildMultipartFormFieldValue(remoteFileName, sandBoxFileName, 'image/jpeg');
    
3.  将字符串参数与文件数据合并为多部分表单数据。
    
    let multiForm = new rcp.MultipartForm({
      'key1': 'value1',
      'key2': multiFormFieldValue,
      'key3': multiFormFieldValue
    }); _// 开发者自行设置参数_
    
4.  设置keys顺序用于指定多部分表单各部分传输顺序，不设置系统会随机生成传输顺序。
    
    multiForm.keys = \['key3', 'key1', 'key2'\]; _// 开发者自行设置参数顺序_
    
5.  自定义表单分隔符，如果不设置，系统使用随机字符串生成分隔符。
    
    multiForm.boundary = '--MULTIPARTFORM BEGIN AND END BOUNDARY'; _// 开发者自行设置分隔符_
    

运行日志如下：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/a2/v3/aloi8zGFSPO2-KsffbIYHg/zh-cn_image_0000002658971707.png?HW-CC-KV=V1&HW-CC-Date=20260709T134215Z&HW-CC-Expire=86400&HW-CC-Sign=19C90E52790EBA2675C62F2DD882CCA3EA59012E9CBFD8F649BFD0B2C58B74D5 "点击放大")

完整示例代码如下：

import { ItemRestriction, SegmentButton, SegmentButtonOptions, SegmentButtonTextItem } from '@kit.ArkUI';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import fileIo from '@ohos.file.fs';
import { rcp } from '@kit.RemoteCommunicationKit';
import { url } from '@kit.ArkTS';

async function getFileAssetsFromType(): Promise<string> {
  let photoPicker = new photoAccessHelper.PhotoViewPicker();
  const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); _// 创建图片-音频类型文件-预览的图库选项实例_
  _// 选择媒体文件类型和选择媒体文件的最大数目_
  photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE\_TYPE; _// 选择媒体文件类型为Image_
  photoSelectOptions.maxSelectNumber = 1; _// 选择媒体文件的最大数目_
  photoSelectOptions.isEditSupported = true;
  photoSelectOptions.isOriginalSupported = true;
  let photoSelectResult: photoAccessHelper.PhotoSelectResult = await photoPicker.select(photoSelectOptions);
  let uris: Array<string> = photoSelectResult.photoUris;
  if (uris.length === 0) {
    console.info('getFileAssetsFromType 没有图片选中');
  }
  return uris\[0\];
}

async function copyFileToSandBox(sandboxDir: string): Promise<string> {
  let uri: string = await getFileAssetsFromType();
  if (uri.length === 0) {
    console.info('copyFileToSandBox 没有图片选中');
    return '';
  }
  const fileName = uri.split('/').pop() || 'default.jpg'; _// 提取文件名_
  console.info('沙箱路径:' + sandboxDir);
  console.info('选择图片文件名称:' + fileName);
  console.info('uris文件路径:' + uri);
  let sandboxFilename = \`${sandboxDir}/${fileName}\`; _// 也可以直接使用沙箱路径字符串如：/data/storage/el2/base/files_
  console.info('沙箱路径图片文件名:' + sandboxFilename);
  const srcFile = fileIo.openSync(uri, fileIo.OpenMode.READ\_ONLY); _// 复制文件到沙箱_
  try {
    fileIo.copyFileSync(srcFile.fd, sandboxFilename);
  } catch (error) {
    console.error('copy file error');
  }
  fileIo.closeSync(srcFile);
  return sandboxFilename;
}

@Component
struct SimpleFormData {
  urlWithParamMsg: string = 'url携带参数';
  formDataMsg: string = 'requestContent传输表单';

  urlWithParamGetData() {
    let baseUrl: string = 'xxx.xxx.xxx'; _// 开发者自行设置请求地址_
    let paramsObject = new url.URLParams();
    paramsObject.append('fod', '3'); _// 开发者自行设置参数_
    paramsObject.append('fed', '4'); _// 开发者自行设置参数_
    console.info('param: ' + paramsObject.toString());
    let urlWithParam = url.URL.parseURL('/get?' + paramsObject.toString(), baseUrl);
    console.info('url: ' + urlWithParam.toString());
    let session = rcp.createSession();
    session.get(urlWithParam).then((response) => {
      console.info(\`urlWithParamGetData Succeeded in getting the response ${response}\`);
    }).catch((err: BusinessError) => {
      console.error(\`err: err code is ${err.code}, err message is ${JSON.stringify(err)}\`);
    });
  }

  formDataTrans() {
    const simpleForm = new rcp.Form({
      'key1': 'value1',
      'key2': \['valueList0', 'valueList1'\],
    }); _// 开发者自行设置参数_
    simpleForm.keys = \['key2', 'key1'\]; _// 开发者自行设置参数顺序_
    const session = rcp.createSession();
    session.post('xxx.xxx.xxx', simpleForm).then((response) => { _// 开发者自行设置请求地址_
      console.info(\`Succeeded in getting the response ${response}\`);
    }).catch((err: BusinessError) => {
      console.error(\`err: err code is ${err.code}, err message is ${JSON.stringify(err)}\`);
    });
  }

  build() {
    Column() {
      Button(this.urlWithParamMsg)
        .onClick(() => {
          this.urlWithParamGetData();
        })
        .margin(16)
        .width('100%');
      Button(this.formDataMsg)
        .onClick(() => {
          this.formDataTrans();
        })
        .margin(16)
        .width('100%');
    }
    .width('100%')
    .height('100%');
  }
}

@Component
struct MultiPartFormData {
  hybridDataMsg: string = '多部分表单传输混合数据';
  sandboxDir: string = '';

  aboutToAppear(): void {
    this.sandboxDir = this.getUIContext().getHostContext()!.filesDir;
  }

  buildMultipartFormFieldValue(fileName: string | undefined,
    pathOrContent: rcp.Path | rcp.FileContent | rcp.GetDataCallback,
    contentType: string | undefined): rcp.MultipartFormFieldValue {
    let result: rcp.MultipartFormFieldValue = {
      remoteFileName: fileName,
      contentType: contentType,
      contentOrPath: pathOrContent
    };
    return result;
  }

  uploadMultiPartFormFileForCommon(multiForm: rcp.MultipartForm) {
    let req = new rcp.Request('xxx.xxx.xxx'); _// 需开发者自行配置请求地址_
    req.content = multiForm;
    req.method = 'POST';
    try {
      const session = rcp.createSession();
      session.fetch(req).then((resp: rcp.Response) => {
        console.info(\`Response succeeded: ${JSON.stringify(resp)}\`);
        session.close();
      }).catch((err: BusinessError) => {
        _// 请求错误处理。_
        console.error(\`Response err: Code is ${JSON.stringify(err.code)}, message is ${JSON.stringify(err)}\`);
        session.close();
      });
    } catch (err) {
      _// 创建会话错误处理。_
      console.error(\`createSession err: Code is ${JSON.stringify(err.code)}, message is ${JSON.stringify(err)}\`);
    }
  }

  async uploadFormSandBoxFileWithParam(sandboxDir: string) {
    let remoteFileName: string = 'test.jpg';
    let sandBoxFileName = await copyFileToSandBox(sandboxDir);
    let multiFormFieldValue = this.buildMultipartFormFieldValue(remoteFileName, sandBoxFileName, 'image/jpeg');
    let multiForm = new rcp.MultipartForm({
      'key1': 'value1',
      'key2': multiFormFieldValue,
      'key3': multiFormFieldValue
    }); _// 开发者自行设置参数_
    multiForm.keys = \['key3', 'key1', 'key2'\]; _// 开发者自行设置参数顺序_
    multiForm.boundary = '--MULTIPARTFORM BEGIN AND END BOUNDARY'; _// 开发者自行设置分隔符_
    this.uploadMultiPartFormFileForCommon(multiForm);
  }

  build() {
    Column() {
      Button(this.hybridDataMsg)
        .onClick(() => {
          this.uploadFormSandBoxFileWithParam(this.sandboxDir);
        })
        .margin(16)
        .width('100%');
    }
    .width('100%')
    .height('100%');
  }
}

@Entry
@Component
struct FormDataTransfer {
  fontColor: string = '#182431';
  selectedFontColor: string = '#0A59F7';
  @State currentIndex: number = 0;
  @State selectedIndex: number = 0;
  @State tabSelectedIndexes: number\[\] = \[0\]; _// SegmentButton默认选项_
  @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.tab({
    buttons: \[{ text: '简单表单传输' }, { text: '多部分表单传输' },\] as ItemRestriction<SegmentButtonTextItem>,
    backgroundColor: '#0d000000',
    selectedBackgroundColor: $r('sys.color.white'),
    fontWeight: 400,
    selectedFontWeight: 500,
    textPadding: 6
  });
  private controller: TabsController = new TabsController();

  @Builder
  tabBuilder(index: number, name: string) {
    Column() {
      Text(name)
        .fontColor(this.selectedIndex === index ? this.selectedFontColor : this.fontColor)
        .fontSize(16)
        .fontWeight(this.selectedIndex === index ? 500 : 400)
        .lineHeight(22)
        .margin({ top: 17, bottom: 7 });
      Divider()
        .strokeWidth(2)
        .color('#007DFF')
        .opacity(this.selectedIndex === index ? 1 : 0);
    }.width('100%');
  }

  build() {
    Column() {
      SegmentButton({
        options: this.tabOptions,
        selectedIndexes: $tabSelectedIndexes,
        onItemClicked: (index) => {
          this.getUIContext().animateTo({ duration: 400 }, () => {
            this.currentIndex = index;
            this.controller.changeIndex(index);
          });
        }
      })
        .borderRadius(20)
        .margin({
          bottom: 16
        })
        .width('100%')
        .height(40);

      Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.controller }) {
        TabContent() {
          SimpleFormData();
        }.tabBar(this.tabBuilder(0, '简单表单传输'));

        TabContent() {
          MultiPartFormData();
        }.tabBar(this.tabBuilder(1, '多部分表单传输'));

      }
      .vertical(false)
      .barMode(BarMode.Fixed)
      .barWidth(360)
      .barHeight(0)
      .onChange((index: number) => {
        _// currentIndex控制TabContent显示页签_
        this.currentIndex = index;
        this.selectedIndex = index;
      })
      .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
        if (index === targetIndex) {
          return;
        }
        console.info(\`event currentOffset ${event.currentOffset}\`);
        _// selectedIndex控制自定义TabBar内Image和Text颜色切换_
        this.selectedIndex = targetIndex;
      })
      .width('100%')
      .height('100%');
    }
    .width('100%')
    .padding({
      left: 16,
      right: 16,
      top: 12
    });
  }
}