基于s3對(duì)象存儲(chǔ)多文件分片上傳的Javascript實(shí)現(xiàn)(一)

nojsja.gitee.io/blogs 更多內(nèi)容已經(jīng)在個(gè)人博客發(fā)布洛姑,請(qǐng)知悉

Contents

  1. 概述

  2. 瀏覽器文件操作限制

  3. 前端多文件分片上傳的原理和實(shí)現(xiàn)

預(yù)覽

upload.png

概述

Amazon S3 提供了一個(gè)簡(jiǎn)單 Web 服務(wù)接口,可用于隨時(shí)在 Web 上的任何位置存儲(chǔ)和檢索任何數(shù)量的數(shù)據(jù)甚纲。此服務(wù)讓所有開(kāi)發(fā)人員都能訪問(wèn)同一個(gè)具備高擴(kuò)展性泰演、可靠性、安全性和快速價(jià)廉的數(shù)據(jù)存儲(chǔ)基礎(chǔ)設(shè)施洗搂, Amazon 用它來(lái)運(yùn)行其全球的網(wǎng)站網(wǎng)絡(luò)讯沈。此服務(wù)旨在為開(kāi)發(fā)人員帶來(lái)最大化的規(guī)模效益。
本文主要針對(duì)兼容aws-s3接口的第三方存儲(chǔ)服務(wù)祝迂,在不使用官方sdk的情況下直接使用Restful接口進(jìn)行存儲(chǔ)桶多文件分片上傳睦尽,主要包含瀏覽器端的多文件分片上傳邏輯的Javascript代碼實(shí)現(xiàn)。

瀏覽器文件操作限制

  • HTML5新特性input[type=file]支持調(diào)用瀏覽器文件訪問(wèn)窗口來(lái)獲取文件數(shù)據(jù)型雳,實(shí)際上JS代碼使用此特性訪問(wèn)本地文件系統(tǒng)后拿到的是一個(gè)指向文件的引用地址当凡,且如果頁(yè)面刷新了那么這個(gè)地址不可復(fù)用,JS代碼并沒(méi)有實(shí)際操作文件本身四啰。前端上傳數(shù)據(jù)時(shí)根據(jù)這個(gè)指向文件的地址把文件的一小塊分片數(shù)據(jù)載入到內(nèi)存并通過(guò)Ajax請(qǐng)求發(fā)送到中間件進(jìn)行處理宁玫。
  • 瀏覽器JS代碼沒(méi)有文件系統(tǒng)操作權(quán)限,不能任意存儲(chǔ)和讀取文件柑晒,因此不支持刷新瀏覽器后上傳進(jìn)度斷點(diǎn)恢復(fù)欧瘪,刷新之后斷點(diǎn)恢復(fù)的前提是能拿到文件數(shù)據(jù),但是JS代碼沒(méi)權(quán)限訪問(wèn)之前拿到的文件引用地址匙赞,并且存儲(chǔ)之前上傳過(guò)的文件分片數(shù)據(jù)這一做法也不合理佛掖。
  • 相對(duì)于文件上傳,文件下載則完全不可控涌庭,由于文件操作權(quán)限芥被,所以整個(gè)下載文件操作都是由瀏覽器自帶的的下載任務(wù)管理器控制的,沒(méi)有瀏覽器接口能拿到這些下載任務(wù)進(jìn)度坐榆,所以下載任務(wù)進(jìn)度也是不能獲取的拴魄。

前端多文件分片上傳的原理和實(shí)現(xiàn)

完整Github源碼

使用了React16/Webpack4/Mobx狀態(tài)管理庫(kù)

  • 支持批量文件分割并行上傳
  • 多文件操作:暫停/恢復(fù)/終止/續(xù)傳/重傳
  • 自定義上傳任務(wù)數(shù)目、單個(gè)分片大小

運(yùn)行流程圖

shards_upload.jpg

主要流程

  1. cacheFile
    前端通過(guò)input組件拿到所有文件地址并緩存起來(lái)。
 /**
   * [cacheFile 緩存即將注冊(cè)的文件]
   */
  @action
  cacheFile = (files, bucket) => {
    const symbolArr = this.filesCache.map(file => this.getSymbol(file));
    const filtedFiles = [];
    let uploadingFileFound = false;
    files.forEach((file) => {
      if (!symbolArr.includes(this.getSymbol(file))) {
        if (this.findIsUploading(this.getSymbol(file), bucket)) {
          uploadingFileFound = true;
          filtedFiles.push(file.name);
        } else {
          this.filesCache.push(file);
          symbolArr.push(this.getSymbol(file));
        }
      }
    });
    if (!files.length) openNotification('warning', null, this.lang.lang.noFilCanBeUploaded);
    if (uploadingFileFound) openNotification('warning', null, this.lang.lang.uploadingFileReuploadTips + filtedFiles.join(', '));
  }
  1. registry
    根據(jù)上一步拿到的文件地址數(shù)組創(chuàng)建多個(gè)Mobx observable對(duì)象跟蹤每個(gè)上傳對(duì)象的基本識(shí)別信息匹中,包括文件名夏漱、文件大小、類型顶捷、分片信息(分片大小和總分片數(shù))挂绰、上傳狀態(tài)信息:uninitial(未初始化)/pending(準(zhǔn)備)/uploading(上傳中)/pause(暫停)/error(錯(cuò)誤)/break(上傳完成)、上傳開(kāi)始時(shí)間服赎、上傳完成時(shí)間葵蒂,為了便于訪問(wèn)這些Mobx observable對(duì)象,建立一個(gè)weakMap存儲(chǔ)file對(duì)象和observable對(duì)象的弱映射關(guān)系重虑。
/**
   * [registry 注冊(cè)上傳文件信息]
   * @param {[Object]} file [文件對(duì)象]
   * @param {[String]} uploadId [文件上傳進(jìn)程id]
   * @param {[Object]} state [文件初始化狀態(tài)]
   */
  @action registry = (files, region, prefix) => {
    let fileObj = null;
    this.loading = true;
    files.forEach((file) => {
      if (this.files.includes(file)) {
        return;
      }
      this.files.push(file);
      fileObj = {
        name: file.webkitRelativePath || file.name,
        prefix: prefix || '',
        size: file.size,
        type: file.type || mapMimeType((file.webkitRelativePath || file.name).split('.').pop()).type,
        state: 'uninitial',
        creationTime: '',
        completionTime: '',
        index: 0,
        file,
        initialized: false,
        partEtags: [],
        region,
        blockSize: this.blockSize,
        total: Math.ceil(file.size / this.blockSize),
        activePoint: new Date(),
        speed: '0 MB/S',
        id: encodeURIComponent(new Date() + file.name + file.type + file.size),
      };
      this.taskType.uninitial.push(file);
      this.taskType.series.push(file);
      const obj = observable(fileObj);
      if (!this.fileStorage.get(region)) {
        this.fileStorage.set(region, [obj]);
      } else {
        this.fileStorage.get(region).push(obj);
      }
      this.fileStorageMap.set(file, obj);
    });
    this.loading = false;
  }
  1. startTasks
    獲取文件隊(duì)列中可用于上傳的文件對(duì)象践付,根據(jù)文件狀態(tài)對(duì)其做初始化或切割文件上傳的操作,同時(shí)實(shí)時(shí)修改對(duì)應(yīng)的Mobx observable上傳對(duì)象的元數(shù)據(jù)標(biāo)識(shí)嚎尤,包括當(dāng)前上傳文件的分片索引(單個(gè)文件上傳進(jìn)度=分片索引/總分片數(shù)目)荔仁、已上傳完成的分片etag信息(由服務(wù)器返回伍宦,可用于完成分片上傳時(shí)校驗(yàn)已上傳的所有分片數(shù)據(jù)是否匹配)芽死、當(dāng)前上傳對(duì)象4的上傳狀態(tài)(uninitial/pending/uploading/pause/eror/break)、當(dāng)前上傳對(duì)象的上傳速度(速度=單個(gè)分片大小/單個(gè)分片上傳所用時(shí)間)次洼。
/**
   * [startTasks 開(kāi)啟上傳任務(wù)隊(duì)列]
   * @param  {[String]} region [桶名]
   */
  startTasks = (region) => {
    // 根據(jù)空閑任務(wù)類型和空閑任務(wù)并發(fā)限制開(kāi)啟空閑任務(wù)
    this.refreshTasks(region);
    if (this.isUploadListEmpty(region)) return;

    const maxLength = this.multiTaskCount - this.taskType.uploading.length;
    const taskSeries = [];
    for (let i = 0; i < (maxLength) && this.taskType.series[i]; i += 1) {
      // const file = this.taskType.series.shift();
      const file = this.taskType.series[i];
      const storageObject = this.fileStorageMap.get(file);
      if (storageObject.state === 'uploading') continue; // 上傳中
      if (storageObject.state === 'pause') continue;
      taskSeries.push(storageObject);
    }

    let index;
    taskSeries.forEach((storageObject) => {
      index = this.taskType.series.indexOf(storageObject.file);
      index !== -1 && this.taskType.series.splice(index, 1);
      if (this.taskType.uninitial.includes(storageObject.file)) {
        this.initRequest(
          storageObject.file,
          {
            bucket: region,
            object: storageObject.name,
            prefix: storageObject.prefix,
          }
        ).then(({ err, init }) => {
          if (!err && init) {
            this.upload(storageObject.file, {
              bucket: region,
              object: storageObject.name,
              prefix: storageObject.prefix,
              uploadId: storageObject.uploadId,
            });
          }
        });
      } else {
        this.upload(storageObject.file, {
          bucket: region,
          object: storageObject.name,
          prefix: storageObject.prefix,
          uploadId: storageObject.uploadId,
        });
      }
    });
  }
  1. refreshTasks
    根據(jù)當(dāng)前設(shè)置的并行上傳任務(wù)數(shù)目和正在上傳的任務(wù)數(shù)目及時(shí)從文件預(yù)備上傳隊(duì)列提取文件放入上傳可調(diào)用文件隊(duì)列关贵。
/* 刷線任務(wù)列表 */
  @action
  refreshTasks = (region) => {
    // 統(tǒng)計(jì)空閑任務(wù)
    const storageObject = this.fileStorage.get(region);
    if (!storageObject) return;
    if (this.taskType.series.length >= this.multiTaskCount) return;

    for (let i = 0; i < storageObject.length; i += 1) {
      if (this.taskType.series.length === this.multiTaskCount) break;
      if (
        storageObject[i].index !== storageObject[i].total
        &&
        (storageObject[i].state === 'pending' || storageObject[i].state === 'uninitial')
        &&
        !this.taskType.series.includes(storageObject[i])
      ) {
        this.taskType.series.push(storageObject[i]);
      }
    }
  }
  1. upload & update
    根據(jù)當(dāng)前文件對(duì)象的上傳分片索引對(duì)文件進(jìn)行切割并更新索引,然后把切割下來(lái)的數(shù)據(jù)通過(guò)Ajax請(qǐng)求發(fā)送給中間件處理卖毁,中間件發(fā)送到后臺(tái)后返回得到的當(dāng)前分片的etag信息揖曾,前端拿到etag信息并存儲(chǔ)到當(dāng)前上傳對(duì)象分片etag信息數(shù)組里面。
/**
   * [upload 分割文件發(fā)起上傳請(qǐng)求]
   * @param  {[Object]} file    [description]
   * @param  {[Object]} _params [...]
   * @param  {[String]}   _params.bucket [bucket name]
   * @param  {[String]}   _params.object [object name]
   * @param  {[String]}   _params.uploadId [upload id]
   */
  @action
  upload = (file, _params) => {
    const storageObject = this.fileStorageMap.get(file);
    let single = false; // 不分片
    /* 異常狀態(tài)退出 */
    if (!this.isValidUploadingTask(storageObject)) return;

    if (storageObject.state === 'pending') {
      this.taskType.pending.splice(this.taskType.pending.indexOf(file), 1);
      this.taskType.uploading.push(file);
      storageObject.state = 'uploading';
    }


    const num = storageObject.index;

    if (num === 0 && file.size <= storageObject.blockSize) {
      // 不用分片的情況
      single = true;
    } else if (num === storageObject.total) {
      // 所有分片都已經(jīng)發(fā)出
      return;
    }
    const nextSize = Math.min((num + 1) * storageObject.blockSize, file.size);
    const fileData = file.slice(num * storageObject.blockSize, nextSize);
    const params = Object.assign(_params, {
      partNumber: num + 1,
    });
    storageObject.activePoint = new Date();
    this.uploadRequest({ params, data: fileData, single }).then((rsp) => {
      if (rsp.code !== 200) {
        openNotification('error', null, (rsp.result.data ? rsp.result.data.Code : this.lang.lang.uploadError));
        this.markError(file);
        this.startTasks(params.bucket);
        return;
      }
      const { completed, etags } = this.update({
        region: params.bucket,
        etag: rsp.result.etag,
        size: fileData.size,
        id: storageObject.id,
        index: params.partNumber,
      });
      if (completed) {
        (single ?
          () => {
            this.complete(file, params.bucket);
          } :
          (partEtags) => {
            this.completeRequest({
              bucket: params.bucket,
              uploadId: params.uploadId,
              object: params.object,
              prefix: params.prefix,
              partEtags,
            }, file);
          })(etags);
      } else {
        this.upload(file, {
          bucket: params.bucket,
          object: params.object,
          uploadId: params.uploadId,
          partNumber: params.partNumber,
          prefix: params.prefix,
        });
      }
    }).catch((error) => {
      this.markError(file);
      this.startTasks(params.bucket);
      console.log(`${params.bucket}_${params.object} upload error: ${error}`);
    });
    storageObject.index += 1;
  }

  1. complete
    當(dāng)最后一個(gè)分片上傳請(qǐng)求完成返回后亥啦,我們就拿到了服務(wù)端返回的這個(gè)文件的所有分片etag信息炭剪,前端需要校驗(yàn)當(dāng)前上傳對(duì)象etag數(shù)組的長(zhǎng)度是否匹配,數(shù)組內(nèi)每個(gè)etag元素的索引和etag值是否匹配翔脱,校驗(yàn)完成后發(fā)送最后一個(gè)請(qǐng)求到后端進(jìn)行校驗(yàn)和組裝分片奴拦,最終完成一個(gè)文件的分片上傳過(guò)程。
/**
   * [completeRequest 完成所有分片數(shù)據(jù)上傳]
   * @param  {[Object]} _params [...]
   * @param  {[String]}   _params.bucket [bucket name]
   * @param  {[String]}   _params.object [object name]
   * @param  {[String]}   _params.uploadId [upload id]
   * @param  {[String]}   _params.partEtags [upload id]
   * @param  {[Object]} file [文件對(duì)象]
   */
  @action completeRequest = (params, file) => {
    postDataPro(
      {
        ...{
          ...params,
          ...{
            object: params.prefix + params.object,
          },
        },
        partEtags: {
          CompleteMultipartUpload: {
            Part: params.partEtags.map(info => ({
              PartNumber: info.number,
              ETag: info.etag,
            })),
          },
        },
      },
      objectResourceApi.object.completeFragmentUpload
    ).then((data) => {
      this.complete(file, params.bucket);
    }).catch((error) => {
      this.startTasks(params.bucket);
      this.markError(file);
    });
  }

  /**
   * [complete 完成上傳]
   * @param {[Object]} file [文件對(duì)象]
   * @param {[String]} bucket [桶名]
   */
  @action
  complete = (file, bucket) => {
    const index = this.taskType.uploading.indexOf(file);
    this.taskType.uploading.splice(index, 1);
    this.taskType.break.push(file);
    const storageObject = this.fileStorageMap.get(file);
    storageObject.completionTime = (new Date().toTimeString()).split(' ')[0];
    storageObject.state = 'break';
    storageObject.index = storageObject.total;

    this.startTasks(bucket);
  };

其它操作

  1. 暫停文件上傳
    將上傳對(duì)象的狀態(tài)從uploading置為pause届吁,然后把該對(duì)象對(duì)應(yīng)的文件從可調(diào)用上傳文件隊(duì)列移除错妖。

  2. 開(kāi)始暫停的上傳任務(wù)
    將上傳對(duì)象的狀態(tài)從pause置為pending,然后把該對(duì)象對(duì)應(yīng)的文件放入可調(diào)用上傳文件隊(duì)列疚沐,等待下一次刷新文件上傳任務(wù)隊(duì)列暂氯。

  3. 續(xù)傳上傳錯(cuò)誤的任務(wù)
    將上傳對(duì)象的狀態(tài)從error置為pending,然后把該對(duì)象對(duì)應(yīng)的文件放入可調(diào)用上傳文件隊(duì)列亮蛔,保持文件的已上傳分片索引記錄痴施,等待下一次刷新文件上傳任務(wù)隊(duì)列,直接調(diào)用上傳函數(shù)進(jìn)行切割并上傳。

  4. 重傳上傳錯(cuò)誤的任務(wù)
    將上傳對(duì)象的狀態(tài)從error置為pending辣吃,然后把該對(duì)象對(duì)應(yīng)的文件放入可調(diào)用上傳文件隊(duì)列锉矢,并將文件已上傳分片索引記錄置為初始狀態(tài),等待下一次刷新文件上傳任務(wù)隊(duì)列齿尽,從文件初始位置重新開(kāi)始切割文件并上傳沽损。

一些關(guān)鍵代碼

  1. 一個(gè)分片上傳完成后將后臺(tái)返回的etag信息更新到本地的上傳對(duì)象屬性,并判斷此文件是否上傳完成循头。
/**
   * [update 更新本地上傳記錄](méi)
   * @param {[String]} region [桶名]
   * @param {[String]} etag [分片標(biāo)志]
   */
  @action
  update = ({
    region, etag, size, id, index,
  }) => {
    const target = this.fileStorage.get(region);
    for (let i = 0; i < target.length; i += 1) {
      if (target[i].id === id) {
        target[i].speed = `${(size / 1024 / 1024 / (((new Date() - target[i].activePoint) / 1000))).toFixed(2)} MB/S`;
        if (target[i].speed === '0.00 MB/S') {
          target[i].speed = `${formatSizeStr(size)}/S`;
        }
        target[i].partEtags = target[i].partEtags.filter(etagItem => etagItem.number !== index);
        target[i].partEtags.push({
          number: index,
          etag,
        });
        // 最后一個(gè)分片恰好又暫停的情況
        if (index === target[i].total) {
          if (target[i].state === 'pause') {
            index -= 1;
          }
        }
        // 判斷上傳是否完成
        if (target[i].total === 0 || target[i].partEtags.toJS().length === target[i].total) {
          return {
            completed: true,
            etags: target[i].partEtags,
          };
        }
        return {
          completed: false,
        };
      }
    }
  }
  1. 在Node.js中間件使用ak/sk預(yù)簽名算法調(diào)用 s3 restful 原生接口
    之前預(yù)研的時(shí)候嘗試根據(jù)aws s3-version4簽名文檔里面請(qǐng)求預(yù)簽名算法在使用Node.js中間件進(jìn)行實(shí)現(xiàn)绵估,結(jié)果很容易出現(xiàn)簽名的signature不一致報(bào)錯(cuò)的情況,所以最后在Node.js中間件采用了一個(gè)npm庫(kù)aws4卡骂,用里面的簽名方法對(duì)前端傳過(guò)來(lái)的ak/sk進(jìn)行url預(yù)簽名国裳,這里給出中間件Request方法的編寫(xiě)邏輯:

/**
  * templateStrTranform [模板字符串轉(zhuǎn)換,s3接口中可能存在一些動(dòng)態(tài)url參數(shù)全跨,比如bucket名和object名缝左,此方法動(dòng)態(tài)替換相關(guān)的字符串]
  * params1: {bucket: testBucket, uid: testUid, bucketId: testID}
  * params2: /admin/bucket?format=json&bucket={bucket}&uid={uid}&bucket-id={bucketId}
  * return: /admin/bucket?format=json&bucket=testBucket&uid=testUid&bucket-id=testID
  * 
  * @author nojsja
  * @param  {[Object]} varObj [替換變量對(duì)象]
  * @param {[String]} templateStr [模板字符串]
  * @return {[String]} result [模板字符串]
  */
exports.templateStrTransform = (varObj, templateStr) => {
  if (typeof varObj !== 'object' || !templateStr) return templateStr;
  for (const attr in varObj) {
    if (varObj.hasOwnProperty(attr) && (!Number(attr) && Number(attr) !== 0 )) {
      templateStr = templateStr.replace(new RegExp(`{${attr}}`, 'g'), varObj[attr]);
    }
  }
  return templateStr;
};

 /**
  * api對(duì)象實(shí)例:
    listFragmentUpload: {
      url: '/{bucket}/{object}?uploadId={uploadId}', // 包含動(dòng)態(tài)模板字符串
      method: 'get',
      port: '7480',
      type: 'xml', // 表明需要將接口返回?cái)?shù)據(jù)進(jìn)行xml -> json轉(zhuǎn)換
      reqType: 'xml', // 表明提交參數(shù)是xml格式,需要進(jìn)行 json -> xml轉(zhuǎn)換
    }
  *
 */
commonApiConfig = (headers, api, data) => {
  if (isEnvDev && !isEnvMock) {
    return {
      url: `http://10.0.9.149:${api.port}${templateStrTransform(data, api.url)}`,
      data: paramsObjectParse(data, api.url),
      host: `http://10.0.9.149:${api.port}`,
      hostname: `http://10.0.9.149`,
      ip: '10.0.9.149',
    };
  } else if(isEnvDev && isEnvMock) {
    return {
      url: `http://10.0.7.15/mock/63${templateStrTransform(data, api.url)}`,
      data: paramsObjectParse(data, api.url),
      host: `http://10.0.9.154:${api.port}`,
      hostname: `http://10.0.9.154`,
      ip: '10.0.9.154',
    };
  } else {
    return {
      url: `http://127.0.0.1:${api.port}${templateStrTransform(data, api.url)}`,
      data: paramsObjectParse(data, api.url),
      host: `http://127.0.0.1:${api.port}`,
      hostname: `http://127.0.0.1`,
      ip: `127.0.0.1`,
    };
  }

}

/**
  * aws4RequestSign [調(diào)用asw4的sign方法簽名一個(gè)url]
  * @param  {[Object]} req [Express.js框架路由函數(shù)的req對(duì)象]
  * @param  {[String]} path [調(diào)用的s3服務(wù)的接口url]
  * @param  {[Object]} api [自定義的api對(duì)象]
  * @param  {[Buffer|String]} data [請(qǐng)求body攜帶的參數(shù)]
  */

/**
 * Tips: 這里aws4.sign方法依賴node process 中的ak/sk env設(shè)置
 *  但是也可以使用sign方法的第二個(gè)options參數(shù)直接傳入ak/sk進(jìn)行顯式調(diào)用浓若,具體請(qǐng)查看此框架的npm文檔
*/
aws4RequestSign = (req, path, api, data) => {
  const aws4 = require('aws4');
  var opts = {
    host: `${(commonApiConfig(req.headers, api, data)).host}`,
    path,
    url: (commonApiConfig(req.headers, api, data)).hostname,
    signQuery: true,
    service: process.env.AWS_SERVICE,
    region: process.env.AWS_REGION,
    method: api.method.toUpperCase(),
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    body: req.body,
    data: '',
  }
  // assumes AWS credentials are available in process.env
  aws4.sign(opts)

  return opts;
}

/**
  * commonRequestAuth [簽名并調(diào)用一個(gè)url]
  * @param  {[Object]} req [Express.js框架路由函數(shù)的req對(duì)象]
  * @param  {[String]} path [調(diào)用的s3服務(wù)的接口url]
  * @param  {[Object]} api [自定義的api對(duì)象]
  * @param  {[Buffer|String]} data [請(qǐng)求body攜帶的參數(shù)]
  */
const commonRequestAuth = (params, api, req, data) => {
  const iAxios = axios.create();
  iAxios.defaults.timeout = params['$no_timeout$'] ? 0 : 30e3;
  // 使用params對(duì)象轉(zhuǎn)換存在動(dòng)態(tài)變量的url  
  const parsedUrl = templateStrTransform(params, api.url);
  // aws env set
  awsEnvRegistry({
    key: req.cookies.access_key,
    secret: req.cookies.secret_key,
  });
  // 簽署請(qǐng)求頭
  const postData = jsonToXml(data, api.reqType);
  const awsOpts = exports.aws4RequestSign(req, parsedUrl, api, params);

  return new Promise((resolve, reject) => {
    iAxios.request({
      baseURL: awsOpts.host,
      url: awsOpts.path,
      method: awsOpts.method,
      headers: getContentType(awsOpts.headers, api.reqType, postData, params._headers),
      data: postData,
      responseType: api.resType,
    }).then((response) => {

      // 設(shè)置header返回
      if (api.type === 'header') return resolve({
        result: response.headers,
        code: 200,
      });
      
      // 轉(zhuǎn)換xml
      if (api.type === 'xml') {
        try {
          xmlToJson(response.data, api.type, (data) => {
            resolve({
              result: data,
              code: 200,
              // data: jsonArrayToString(data),
              headers: response.headers,
            });
          });
        } catch (error) {
          resolve({
            code: 500,
            result: global.lang.xml_parse_error,
          });
        }
      }

      resolve({
        result: response.data,
        code: 200,
      });
    }).catch((error) => {
      console.log(error.response.data);
      xmlToJson(error.response.data, 'xml', (data) => {
        resolve({
          code: 600,
          result: { headers: error.config, data: data.Error ? data.Error : error.response.data}
        })
      })
    });
  });
};
  1. 在web端使用ak/sk預(yù)簽名算法調(diào)用 s3 restful 原生接口
    如果項(xiàng)目需要從web端直連后端s3服務(wù)調(diào)用接口的話渺杉,上面的簽名方法就不能用了,其實(shí)很多時(shí)候直連可以帶來(lái)更好的性能挪钓,比如文件上傳/下載等等是越,不用在中間件做文件轉(zhuǎn)存,其他的接口調(diào)用直連的話也不用中間層做request轉(zhuǎn)發(fā)了碌上。這里推薦一個(gè)能夠進(jìn)行s3請(qǐng)求預(yù)簽名的axios插件aws4-axios倚评,用法如下:
import axios from "axios";
import { aws4Interceptor } from "aws4-axios";

const client = axios.create();

const interceptor = aws4Interceptor({
  region: "eu-west-2",
  service: "execute-api"
}, {
  accessKeyId: '',
  secretAccessKey: ''
});

client.interceptors.request.use(interceptor);

// Requests made using Axios will now be signed
client.get("https://example.com/foo").then(res => {
  // ...
});

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馏予,隨后出現(xiàn)的幾起案子天梧,更是在濱河造成了極大的恐慌,老刑警劉巖霞丧,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呢岗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蚯妇,警方通過(guò)查閱死者的電腦和手機(jī)敷燎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)箩言,“玉大人硬贯,你說(shuō)我怎么就攤上這事≡墒眨” “怎么了饭豹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵鸵赖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拄衰,道長(zhǎng)它褪,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任翘悉,我火速辦了婚禮茫打,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妖混。我一直安慰自己老赤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布制市。 她就那樣靜靜地躺著抬旺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祥楣。 梳的紋絲不亂的頭發(fā)上开财,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音误褪,去河邊找鬼责鳍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛振坚,可吹牛的內(nèi)容都是我干的薇搁。 我是一名探鬼主播斋扰,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼渡八,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了传货?” 一聲冷哼從身側(cè)響起屎鳍,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎问裕,沒(méi)想到半個(gè)月后逮壁,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粮宛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年窥淆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巍杈。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忧饭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出筷畦,到底是詐尸還是另有隱情词裤,我是刑警寧澤刺洒,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站吼砂,受9級(jí)特大地震影響逆航,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜渔肩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一因俐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧周偎,春花似錦女揭、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至袍嬉,卻和暖如春境蔼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伺通。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工箍土, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人罐监。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓吴藻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親弓柱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子沟堡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容