如何简单地在浏览器中使用阿里云的文件上传功能?

前言

我开通了一个微信公共号“王和阳的航海日志”,在上面记录着自己的学习、思考、实践和成长的过程,欢迎关注、交流和拍砖。

公共号二维码

正文

明哥从办公室里发出一声怒吼:“这阿里云OSS服务的上传功能也太难用了,没有批量重命名功能,难道这几百个文件要我一个个手动重命名??!!”然后我和明哥就花了半天时间研究了下阿里云 SDK for JavaScript,顺利把文件上传功能集成到咱们的网站里了。
首先明确这次的需求:

  1. 能在后台进行文件的上传(单个文件大概在20M左右)
  2. 自动根据传入的ID和文件夹名对文件进行重命名
  3. 智能识别文件的后缀名,减少传错的可能性
  4. 显示上传进度且对上传的结果进行提示

接着去看一看阿里开放云存储OSS的介绍和基础概念 在申请了服务之后,阿里会提供一个bucket(可以理解为在阿里云上你上传的文件所在的文件夹名),同时拥有有对应的 Access Key ID & Access Key Secret (用于加密),阿里云把每一个文件当做Object对待,我们在浏览器端使用POST方式来上传文件,对于用Node.js上传文件,阿里在GitHub上提供了一个例子,基本上参考这个例子就可以写出完整的上传功能了。地址如下:https://github.com/aliyun-UED
首先我们要用npm安装阿里云SDK:

npm install aliyun-sdk

接下来需要添加aliyun-sdk.min和oss-js-upload这两个js文件,前者能在浏览器端调用aliyun sdk,后者对文件上传进行了一系列的封装,在这里我们对oss-js-upload.js文件进行了改造,使其增加了显示上传进度的功能,这是本文的重点所在,具体改动详见源代码文件,一开始根本不知道如何显示进度,但是在调试GitHub上阿里云团队提供的例子时,发现后台会打印出诸如 “Completed part 1”、”Completed part 2” 的信息,联系到Http POST请求的特征(一次最多传2M左右的文件),所以说在oss-js-upload中就是使用Mutilpart方法进行上传的,那么我们只要获取到各个块的上传进度,除以总的块数,不就可以显示上传进度了么?关键的思路就在这里!想通了这一点,那么接下来就好办多了!

最后贴上源代码供大家参考,重点需要注意的地方我已经加了标注,如果再有什么不懂的可以在评论区给我留言。

upload.js

/**
 * Created by Young on 2015/8/28.
 * wang645788@gmail.com
 */

//阿里云提供功能的上传类
var ossUpload = new OssUpload({
  bucket: 'keju-video',
  // 选择杭州的 oss 实例所在地区选择填入,这里选的是
  endpoint: 'http://oss-cn-hangzhou.aliyuncs.com',
  // 如果文件大于 chunkSize 则分块上传, chunkSize 不能小于 100KB 即 102400
  chunkSize: 1048576,
  // 分块上传的并发数
  concurrency: 5,
  aliyunCredential: {
    "accessKeyId": "在阿里云申请的accessKeyId",
    "secretAccessKey": "在阿里云申请的secretAccessKey"
  },
  stsToken: null
});
var videoMp4;
var folderName;
var lessonId = "将要传的文件的ID以某种方式传入即可"; //获取要上传视频的ID,便于填写文件名

//检查视频文件的后缀名
String.prototype.endsWith = function (suffix) {
  return !!this.match(suffix + "$");
};

var uploadVideo = function (type, file, fnProgress, fnSuccess, fnFail) {

  var video = file;
  var progress = 0;

  ossUpload.upload({
    // 必传参数, 需要上传的文件对象
    file: video,
    // 必传参数, 文件上传到 oss 后的名称, 包含路径
    key: type + '/' + folderName + '/' + lessonId + '.' + type,
    // 上传失败后重试次数
    maxRetry: 3,
    headers: {
      'CacheControl': 'public',
      'Expires': '',
      'ContentEncoding': '',
      'ContentDisposition': '',
      // oss 支持的 header, 目前仅支持 x-oss-server-side-encryption
      'ServerSideEncryption': ''
    },
    // 文件上传失败后调用, 可选参数
    onerror: function (evt) {
      console.log("error");
      console.log(evt);
      fnFail();
    },
    // 文件上传时的进度,每完成一片更新一次
    onprogress: function (loaded, total) {
      console.log(loaded, total, (loaded / total * 100).toFixed(1) + "%");
      progress = (loaded / total * 100).toFixed(1) + "%";

      fnProgress(progress);
    },
    // 文件上传成功调用, 可选参数
    oncomplete: function (res) {
      console.log("success");
      console.log(res);
      fnSuccess();
    }
  });
};

//选择上传的MP4文件
$("#btnSelectMp4").click(function () {
  $("#mp4File")
    .trigger("click")
    .change(function (evt) {
      //得到上传的文件对象
      videoMp4 = evt.target.files[0];
      folderName = $("#inputFolderName").val();
      if (videoMp4.name.endsWith('mp4')) {
        $("#btnUploadMp4").show();
      } else {
        return $("#tipNotMp4").show();
      }
    });
});

//点击开始上传Mp4文件
$("#btnUploadMp4").click(function () {
  var btnUploadMp4 = $("#btnUploadMp4");
  btnUploadMp4.prop('disabled', true);
  btnUploadMp4.text("正在上传");
  uploadVideo('mp4', videoMp4, function (progress) {
    console.log("the progress is " + progress);
    btnUploadMp4.text(progress);
  }, function () {
    btnUploadMp4.prop('disabled', false);
    btnUploadMp4.hide();
    btnUploadMp4.text('上传.mp4文件');
    $("#uploadMp4Succeed").show();
    $("#btnSelectMp4").show();
    $('#mp4Address').val('');
    $("#mp4File").replaceWith($("#mp4File").val('').clone(true));
  }, function () {
      //上传如果失败,则再次显示按钮让用户重新上传
    btnUploadMp4.prop('disabled', false);
    btnUploadMp4.text('上传.mp4文件');
    btnUploadMp4.hide();
    $("#uploadMp4Failed").show();
    $("#btnSelectMp4").show();
    $('#mp4Address').val('');
    $("#mp4File").replaceWith($("#mp4File").val('').clone(true));
  });
});

oss-js-upload.js

'use strict';
(function () {

  var detectIEVersion = function () {
    var v = 4,
      div = document.createElement('div'),
      all = div.getElementsByTagName('i');
    while (
      div.innerHTML = '<!--[if gt IE ' + v + ']><i></i><![endif]-->',
        all[0]
      ) {
      v++;
    }
    return v > 4 ? v : false;
  };

  var _extend = function (dst, src) {
    for (var i in src) {
      if (Object.prototype.hasOwnProperty.call(src, i) && src[i]) {
        dst[i] = src[i];
      }
    }
  };

  function OssUpload(config) {
    if (!config) {
      // console.log('需要 config');
      return;
    }
    this._config = {
      chunkSize: 1048576    // 1MB
    };

    if (this._config.chunkSize && this._config.chunkSize < 102400) {
      // console.log('chunkSize 不能小于 100KB');
      return;
    }

    _extend(this._config, config);

    if (!this._config.aliyunCredential && !this._config.stsToken) {
      // console.log('需要 stsToken');
      return;
    }

    if (!this._config.endpoint) {
      // console.log('需要 endpoint');
      return;
    }

    var ALY = window.ALY;
    if (this._config.stsToken) {
      this.oss = new ALY.OSS({
        accessKeyId: this._config.stsToken.Credentials.AccessKeyId,
        secretAccessKey: this._config.stsToken.Credentials.AccessKeySecret,
        securityToken: this._config.stsToken.Credentials.SecurityToken,
        endpoint: this._config.endpoint,
        apiVersion: '2013-10-15'
      });
    }
    else {
      this.oss = new ALY.OSS({
        accessKeyId: this._config.aliyunCredential.accessKeyId,
        secretAccessKey: this._config.aliyunCredential.secretAccessKey,
        endpoint: this._config.endpoint,
        apiVersion: '2013-10-15'
      });
    }

    var arr = this._config.endpoint.split('://');
    if (arr.length < 2) {
      // console.log('endpoint 格式错误');
      return;
    }
    this._config.endpoint = {
      protocol: arr[0],
      host: arr[1]
    }

  }

  OssUpload.prototype.upload = function (options) {
    if (!options) {
      if (typeof options.onerror == 'function') {
        options.onerror('需要 options');
      }
      return;
    }

    if (!options.file) {
      if (typeof options.onerror == 'function') {
        options.onerror('需要 file');
      }
      return;
    }
    var file = options.file;

    if (!options.key) {
      if (typeof options.onerror == 'function') {
        options.onerror('需要 key');
      }
      return;
    }
    // 去掉 key 开头的 /
    options.key.replace(new RegExp("^\/"), '');

    var self = this;

    var readFile = function (callback) {
      var result = {
        chunksHash: {},
        chunks: []
      };
      var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
      var chunkSize = self._config.chunkSize;
      var chunksNum = Math.ceil(file.size / chunkSize);
      var currentChunk = 0;

      var frOnload = function (e) {
        result.chunks[currentChunk] = e.target.result;
        currentChunk++;
        if (currentChunk < chunksNum) {
          loadNext();
        }
        else {
          result.file_size = file.size;
          callback(null, result);
        }
      };
      var frOnerror = function () {
        console.error("读取文件失败");
        if (typeof options.onerror == 'function') {
          options.onerror("读取文件失败");
        }
      };

      function loadNext() {
        var fileReader = new FileReader();
        fileReader.onload = frOnload;
        fileReader.onerror = frOnerror;

        var start = currentChunk * chunkSize,
          end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
        var blobPacket = blobSlice.call(file, start, end);
        fileReader.readAsArrayBuffer(blobPacket);
      }

      loadNext();
    };

    var uploadSingle = function (result, callback) {
      var params = {
        Bucket: self._config.bucket,
        Key: options.key,
        Body: result.chunks[0],
        ContentType: file.type || ''
      };
      _extend(params, options.headers);

      self.oss.putObject(params, callback);
    };

    var uploadMultipart = function (result, callback) {
      var maxUploadTries = options.maxRetry || 3;
      var uploadId;
      var loadedNum = 0;
      var latestUploadNum = -1;
      var concurrency = 0;

      var multipartMap = {
        Parts: []
      };

     ``` javascript 
        //这里使用arguments读取传入的onProgres函数并回调
      if(3===arguments.length){
        var fnProgress = arguments[2];
      }
     ```

      var init = function () {
        var params = {
          Bucket: self._config.bucket,
          Key: options.key,
          ContentType: file.type || ''
        };
        _extend(params, options.headers);

        self.oss.createMultipartUpload(params,
          function (mpErr, res) {
            if (mpErr) {
              // console.log('Error!', mpErr);
              callback(mpErr);
              return;
            }

            // console.log("Got upload ID", res.UploadId);
            uploadId = res.UploadId;

            uploadPart(0);
          });
      };

      var uploadPart = function (partNum) {
        if(partNum >= result.chunks.length) {
          return;
        }

        concurrency++;
        if(latestUploadNum < partNum) {
          latestUploadNum = partNum;
        }
        if(concurrency < self._config.concurrency && (partNum < (result.chunks.length - 1))) {
          uploadPart(partNum + 1);
        }
        var partParams = {
          Body: result.chunks[partNum],
          Bucket: self._config.bucket,
          Key: options.key,
          PartNumber: String(partNum + 1),
          UploadId: uploadId
        };

        var tryNum = 1;

        var doUpload = function () {
          self.oss.uploadPart(partParams, function (multiErr, mData) {
            if (multiErr) {
              // console.log('multiErr, upload part error:', multiErr);
              if (tryNum > maxUploadTries) {
                console.log('上传分片失败: #', partParams.PartNumber);
                callback(multiErr);
              }
              else {
                console.log('重新上传分片: #', partParams.PartNumber);
                tryNum++;
                doUpload();
              }
              return;
            }
            // console.log(mData);
            concurrency--;

            multipartMap.Parts[partNum] = {
              ETag: mData.ETag,
              PartNumber: partNum + 1
            };

            console.log("Completed part", partNum + 1);
            //console.log('mData', mData);
            loadedNum++;
            ``` JavaScript
            //回调upload.js文件中写的onprogress函数,显示进度
            if("function" === typeof fnProgress){
              fnProgress(loadedNum, result.chunks.length);
            }
            ```
            if (loadedNum == result.chunks.length) {
              complete();
            }
            else {
              uploadPart(latestUploadNum + 1);
            }
          });
        };

        doUpload();

      };

      var complete = function () {
        // console.log("Completing upload...");

        var doneParams = {
          Bucket: self._config.bucket,
          Key: options.key,
          CompleteMultipartUpload: multipartMap,
          UploadId: uploadId
        };

        self.oss.completeMultipartUpload(doneParams, callback);
      };

      init();
    };

    readFile(function (err, result) {
      var callback = function (err, res) {
        if (err) {
          if (typeof options.onerror == 'function') {
            options.onerror(err);
          }
          return;
        }

        if (typeof options.oncomplete == 'function') {
          options.oncomplete(res);
        }
      };

      if (result.chunks.length == 1) {
        uploadSingle(result, callback)
      }
      else {
      //若文件大于2MB,使用分块上传,显示进度,若小于2MB,则不显示进度直接上传
        if('function' === typeof options.onprogress){
          uploadMultipart(result, callback, options.onprogress);
        } else {
          uploadMultipart(result, callback);
        }
      }
    });

  };

  window.OssUpload = OssUpload;

})();
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值