技术前端开发一文彻底弄懂文件上传

前言

文件上传是前端开发中常见的功能。最近为了本地化部署,需要使用 minio(一个开源的 S3 兼容的对象存储)来替代一直使用的 阿里云 OSS, 研究了一下关于文件上传的实现,记录一下踩的坑以及整个文件上传的过程。

文件上传的实现

file-upload

出于安全考虑,标准的文件上传流程通常包含以下四个步骤:

  1. 前端向自己的服务端发起请求,获取文件上传所需的授权信息
  2. 服务端进行身份验证,验证通过后生成一个带有过期时间的预签名 URL
  3. 前端获取预签名 URL 后,直接通过该 URL 将文件上传至对象存储服务
  4. 文件上传成功后,对象存储服务可以调用预先配置的回调接口,执行回调函数

其中,阿里云 oss 与 minio 的实现略有不同,回调也略有差异,下面分别介绍。

上传方法差异

阿里云 oss

antdupload 组件默认是使用 post 方法也就是表单上传来上传文件的。

form-file-upload.png

在使用这种方式上传文件时,有一个重要的约定:file必须为最后一个表单域,后面如果还有字段会直接丢弃不解析

const formData = new FormData();
formData.append('name',filename);
formData.append('policy', data.policy);
formData.append('OSSAccessKeyId', data.ossAccessKeyId);
formData.append('success_action_status', '200');
formData.append('signature', data.signature);
formData.append('key', data.dir + filename);
// file必须为最后一个表单域,除file以外的其他表单域无顺序要求。
formData.append('file', file);

这是因为使用 PostObject 进行表单上传时, Content-Length 并不是必须的。

也就是说 oss 并不知道你这个文件的大小,没办法提前给你把空间分配出来让你写入。它只能一小段一小段的数据接收,收完一段存一份元数据,最后汇总。

所以,当拿到 file 字段后,就认为后面全都是数据,就会去走数据存储的流程。

minio

minio 作为一个 S3 兼容的对象存储,它支持 S3API。为了以后迁移方便,最好使用 @aws-sdk/client-s3 来上传文件。

在这个 sdk 中,关于对象上传的操作只有

  • PutObjectPart: 上传分段
  • putObject: 普通上传

所以我们在使用 @aws-sdk/client-s3 生成预签名 url 时,只能通过 PutObjectCommand 来生成。

下面是一个生成预签名 url 的示例:

import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
 
const s3Client = new S3Client({
  endpoint: "your-endpoint",
  region: "your-region",
  credentials: {
    accessKeyId: "your-access-key-id",
    secretAccessKey: "your-secret-access-key"
  },
  forcePathStyle: true
});
 
// 生成包含时间戳和文件ID的JWT令牌
const verificationToken = sign(
   {
     fileId,
     userId: user?.id,
     timestamp: Date.now(),
   },
    "your-jwt-secret-key",
   {
     expiresIn: '2h', // 令牌2小时后过期
     algorithm: 'RS256',
   }
);
 
const command = new PutObjectCommand({
   Bucket: "your-bucket-name",
   Key: key,
   ContentType: 'application/octet-stream',
   Metadata: {
   'x:user_id': user?.id,
   'x:verification_token': verificationToken,
   },
});
 
const url = await getSignedUrl(s3Client, command, {
  expiresIn: 60 * 3 // 3分钟
});

前端拿到 url 后,直接上传文件即可,如果使用的 antdUpload 组件,需要修改以下地方

  • 接口是 put 请求,所以需要修改 methodPUT
  • 拿到 url 后,不需要通过 FormData 上传,直接通过 PUT 请求把 file 放到 body 里即可
customRequest: async (options) => {
  const response = await fetch(options.action, {
   headers: options.headers,
   method: options.method,
   body: options.file
  });
  options?.onSuccess?.({ ...response, file_id: options.data?.key })
} 

上传回调差异

阿里云的上传回调是一个很好用功能,在执行完上传文件后,oss 会执行回调接口,并直接把回调返回的结果返回给前端,可以有效降低 前端的逻辑复杂度和网络消耗。

upload-callback

但是这个不是一个通用方案,如果你想在 minio 上使用上传回调功能,你需要配置对应的 webhook 功能

minio-webhook

最后再在你的 bucket 上订阅你这个 webhook 即可

minio-webhook-subscribe