上传文件很费时费力?那是你没用对方式

1,315 阅读3分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

嗨,大家好,我是希留,一个被迫致力于全栈开发的老菜鸟。

上一篇文章说到上传文件使用云服务商的对象存储,感兴趣的可以阅读该文章:传送门

发布后有不少伙伴反馈,前后端分离的项目更好的上传方式是使用前端直传的方式。于是我查阅相关文档,连夜把项目里的上传方式改成前端直传了(项目的技术栈是Springboot + Vue),发现上传速度明显提升了。所以这篇文章就来说说,前端直传的方式应该怎么弄呢?

一、前端直传的优点

前端数据直传的方式相比较后端上传的方式有不少的优点。

  • 上传速度快。后端上传的方式是用户数据需先上传到应用服务器,之后再上传到COS。而前端直传的方式,用户数据不用通过应用服务器中转,直传到COS。减少了网络请求响应,速度将大大提升。而且COS采用BGP带宽,能保证各地各运营商之间的传输速度。
  • 扩展性好。后端上传的方式会占用带宽,文件上传一多就会占用服务器大量的带宽,导致其它请求阻塞,甚至无法访问等情况。前端直传的方式节约了后端服务器的带宽和负载。
  • 成本低。服务器真正的成本基本上都在带宽上,提升带宽的费用是很贵的。而使用前端直传的方式可以减少文件上传到服务器的带宽,从而降低带宽成本。

二、实现步骤

2.1、后端方面

后端需要提供一个生成临时密钥的接口,具体步骤如下,可参考文档,由于我的后端服务是Java语言,所以这里举例使用的是 Java SDK。

2.1.1 添加依赖

<!--腾讯云 COS 临时密钥-->
<dependency>
    <groupId>com.qcloud</groupId>
    <artifactId>cos-sts_api</artifactId>
    <version>3.1.1</version>
</dependency>

2.1.2 增加接口

package com.xiliu.common.controller;

import com.tencent.cloud.CosStsClient;
import com.tencent.cloud.Response;
import com.xiliu.common.result.R;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.TreeMap;

/**
 * @author xiliu
 * @description cos存储前端控制器
 * @date 2022/11/13 17:51
 */
@RestController
@RequestMapping("/cos")
public class CosController {

    @Value("${cos.secretId}")
    private String secretId;
    @Value("${cos.secretKey}")
    private String secretKey;
    @Value("${cos.regionName}")
    private String regionName;
    @Value("${cos.bucketName}")
    private String bucketName;

    @ApiOperation(value = "获取cos临时密钥")
    @GetMapping("/temp-key")
    public R getTempKey() {
        TreeMap<String, Object> config = new TreeMap<String, Object>();
        try {
            // 替换为您的云 api 密钥 SecretId
            config.put("secretId", secretId);
            // 替换为您的云 api 密钥 SecretKey
            config.put("secretKey", secretKey);
            // 临时密钥有效时长,单位是秒,默认 1800 秒,目前主账号最长 2 小时(即 7200 秒),子账号最长 36 小时(即 129600)秒
            config.put("durationSeconds", 1800);
            // 换成您的 bucket
            config.put("bucket", bucketName);
            // 换成 bucket 所在地区
            config.put("region", regionName);

            // 只允许用户访问 upload/house 目录下的资源
            config.put("allowPrefixes", new String[] {"upload/house/*"});

            // 密钥的权限列表。必须在这里指定本次临时密钥所需要的权限。
            String[] allowActions = new String[] {
                    // 简单上传
                    "name/cos:PutObject",
                    // 表单上传、小程序上传
                    "name/cos:PostObject",
                    // 分块上传
                    "name/cos:InitiateMultipartUpload",
                    "name/cos:ListMultipartUploads",
                    "name/cos:ListParts",
                    "name/cos:UploadPart",
                    "name/cos:CompleteMultipartUpload"
            };
            config.put("allowActions", allowActions);
            Response response = CosStsClient.getCredential(config);
            return R.ok(response);
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException("no valid secret !");
        }
    }
}

2.1.3 测试接口

由于是测试,接口的安全校验我就去掉了,方便测试是否能正常生成临时密钥。结果如下图所示,正常生成了。

image.png

2.2、前端方面

前端方面,可以参考文档,我使用的前端框架是Vue,具体步骤如下,上传功能可以封装成一个组件,提供给有需要的地方调用。

2.2.1 安装 cos-js-sdk-v5 依赖

npm install --save cos-js-sdk-v5

2.2.2 新建组件

在 components 目录下新建一个 upload 目录,在 upload 目录下新建 multiUpload.vue 组件。

代码示例如下:

```
<template> 
  <div>
    <el-upload
      action=""
      list-type="picture-card"
      :file-list="fileList"
      :before-upload="beforeUpload"
      :on-remove="handleRemove"
      :http-request="handleUploadFile"
      :on-preview="handlePreview"
      :limit="maxCount"
      :on-exceed="handleExceed"
    >
      <i class="el-icon-plus"></i>
    </el-upload>
    <!--进度条-->
    <el-progress v-show="showProgress"  :text-inside="true" :stroke-width="15"
       :percentage="progress" status="success"></el-progress>
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="dialogImageUrl" alt="">
    </el-dialog>
  </div>
</template>
<script>
  import COS from 'cos-js-sdk-v5'
  import { getCosTempKey } from '@/api/upload'

  export default {
    name: 'multiUpload',
    props: {
      //图片属性数组
      value: Array,
      //最大上传图片数量
      maxCount:{
        type:Number,
        default:9
      }
    },
    data() {
      return {
        // 图片预览
        dialogVisible: false,
        // 图片预览地址
        dialogImageUrl:null,
        // COS
        cosData: {},
        // 进度条的显示
        showProgress: false,
        // 进度条数据
        progress: 0,
        // 文件表单
        fileParams: {
          // 上传的文件目录
          folder: '/upload/house/'
        },
      };
    },
    computed: {
      fileList() {
        let fileList=[];
        for(let i=0;i<this.value.length;i++){
          fileList.push({url:this.value[i]});
        }
        return fileList;
      }
    },
    methods: {
      emitInput(fileList) {
        let value=[];
        for(let i=0;i<fileList.length;i++){
          value.push(fileList[i].url);
        }
        this.$emit('input', value)
      },
      // 移除图片
      handleRemove(file, fileList) {
        this.emitInput(fileList);
      },
      // 预览图片
      handlePreview(file) {
        this.dialogVisible = true;
        this.dialogImageUrl = file.url;
      },
      // 上传预处理
      beforeUpload(file) {
        return new Promise((resolve, reject) => {
          const isImage = file.type.indexOf("image/") != -1;
          const isLt2M = file.size / 1024 / 1024 < 10;

          if (!isImage) {
            this.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。");
            reject(false)
          }
          if (!isLt2M) {
            this.$message.error("上传头像图片大小不能超过 10MB!");
            reject(false)
          }
          // 获取cos临时密钥
          getCosTempKey().then(response => {
            this.cosData = response.data;
            resolve(true)
          }).catch(error => {
            this.$message.error('获取cos临时密钥失败!msg:' + error)
            reject(false)
          })
        })
      },
      // 前端直传文件
      handleUploadFile(file) {
        let that = this;
        // 获取COS实例
        const cos = new COS({
          // 必选参数
          getAuthorization: (options, callback) => {
            const obj = {
              TmpSecretId: that.cosData.credentials.tmpSecretId,
              TmpSecretKey: that.cosData.credentials.tmpSecretKey,
              XCosSecurityToken: that.cosData.credentials.sessionToken,
              // 时间戳,单位秒,如:1580000000
              StartTime: that.cosData.startTime,
              // 时间戳,单位秒,如:1580000900
              ExpiredTime: that.cosData.expiredTime
            }
            callback(obj)
          }
        });
        // 文件路径和文件名
        let cloudFilePath = this.fileParams.folder + `${+new Date()}` + '_' + file.file.name

        // 执行上传服务
        cos.putObject({
          // 你的存储桶名称
          Bucket: 'xiliu-1259663924',
          // 你的存储桶地址
          Region: 'ap-guangzhou',
          // key加上路径写法可以生成文件夹
          Key: cloudFilePath,
          StorageClass: 'STANDARD',
          // 上传文件对象
          Body: file.file,
          onProgress: progressData => {
            if (progressData) {
              that.showProgress = true
              that.progress = Math.floor(progressData.percent * 100)
            }
          }
        },(err, data) => {
          if (data && data.statusCode === 200) {
            let uploadResult = `https://${data.Location}`
            that.showProgress = false
            that.$message({message: '上传成功', type: 'success'})
            this.fileList.push({name: file.name,url:uploadResult});
            this.emitInput(this.fileList);
          } else {
            that.$message.error("上传失败,请稍后重试!")
          }
        })
      },
      // 文件超出个数限制
      handleExceed(files, fileList) {
        this.$message({
          message: '最多只能上传'+this.maxCount+'张图片',
          type: 'warning',
          duration:1000
        });
      },
    }
  }
</script>
<style>

</style>

2.2.3 使用组件

代码示例如下:

```
<el-form-item label="房屋图片/视频:">
  <multi-upload v-model="selectHousePics"></multi-upload>
</el-form-item>

效果如下图所示:

image.png

总结

以上就是本文的全部内容了,感谢大家的阅读。

如果觉得文章对你有帮助,还不忘帮忙点赞、收藏、关注、评论哟,您的支持就是我创作最大的动力!