阿里云oss文件上传+React+antd-upload

1,051 阅读5分钟

自定义文件上传没有进度条,折腾了好久解决了。

P.S. 图片预览需要自己有域名,懒得折腾了

antd upLoad 自定义上传 不显示进度条 解决办法 参考了这篇文章,发现进度条只有一开始和完成,没有中间进度。 结合 customRequest.tsx官方例子,在onProgress方法中实时更新file的percent属性,终于实现实时进度条了。

0 准备工作

在阿里云oss 建立子ram账号,并给予权限(STS留给服务端授权用,使用方法1可以忽略) 1.png 建立好bucket之后配置跨域

3.png

1.key和secret写在前端(不安全)

import OSS from "ali-oss";
import React, { useState } from "react";
import { Upload, Spin } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";

import type { UploadProps } from "antd";

interface UploadFileProps extends UploadProps {
  onProgress?: (progress: any, file: any) => void;
}

const UploadFile = (props) => {
  // 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
  // const changeSrc = props.changeSrc;
  const [show, changeShow] = useState(false);
  const [fileList, setFileList] = useState<UploadFile[]>([]);
 
   const client = new OSS({
    region: "oss-cn-hangzhou", //你的oss服务器所在区域
    accessKeyId: "你的ram账号key",
    accessKeySecret: "你的ram账号accessKeySecret",
    bucket: process.env.NODE_ENV == "production" ? "nest-prod" : "nest-devp", // oss上你的bucket名称
 });

  const uploadPath = (path, file) => {
    return `${path}/${file.name.split(".")[0]}-${file.uid}.${
      file.type.split("/")[1]
    }`;
  };

  const OssUpload = async (option) => {
    const { file, onSuccess, onProgress, onError } = option;
    const folder = "test";
    const url = uploadPath(folder, file);
    let data = null;
    try {
      data = await client.multipartUpload(url, file, {
        progress: function (p) {
          console.log("获取进度条的值==>", (p * 100).toFixed(2));
          onProgress({ percent: (p * 100).toFixed(2) }, file);
        },
      });
      onSuccess(
        data,
        file,
      );
    } catch (e) {
      // message.error(e.message);
      onError(e);
    }
  };

  const beforeUpload = (file) => {
    return true;
  };

  const uploadProps:UploadFileProps = {
    showUploadList: true,
    customRequest: OssUpload,
    beforeUpload: beforeUpload,
    fileList: fileList,
    listType: "picture-card",
    onProgress({ percent }, file) {
      const index = fileList.findIndex((item) => item.uid === file.uid);
      fileList[index].percent = percent;
      setFileList([...fileList]);
      // console.log("onProgress", `${percent}%`, file);
    },
    onChange(info: any) {
      console.log("onChange😊===》", info);
      if (info.file.status !== "uploading") {
        console.log(info.file, info.fileList);
      }
      if (info.file.status === "done") {
        // console.log(`${info.file.name} 文件上传成功`);
        message.success(`${info.file.name} 文件上传成功`);
      } else if (info.file.status === "error") {
        info.fileList = info.fileList.filter(
          (item) => item.uid !== info.file.uid,
        );
        message.error(`${info.file.name} 文件上传失败`);
      }
      setFileList([...info.fileList]);
    },
  };

  const uploadButton = (
    <div>
      <PlusOutlined />
      <div className="ant-upload-text">Upload</div>
    </div>
  );

  return (
    <div>
      {show === true ? (
        <Spin style={{ position: "relative", left: "40px" }} />
      ) : (
        <Upload {...uploadProps}>{uploadButton}</Upload>
      )}
      <br />
    </div>
  );
};

export default UploadFile;

2.key和secret接口获取(安全)

2.1 准备工作- 使用STS进行临时授权

  1. 在阿里云oss 建立oss访问权限的角色 2.png

2.2 服务端部分(nest.js):

import { Injectable } from '@nestjs/common';
import { CreateUploadDto } from './dto/create-upload.dto';
import { UpdateUploadDto } from './dto/update-upload.dto';
import { STS } from 'ali-oss';
import { ConfigService } from '@nestjs/config';
import { ConfigEnum } from '../enum/config.enum';
import * as CryptoJS from 'crypto-js';
@Injectable()
export class UploadService {
  private readonly client: STS;
  constructor(private configService: ConfigService) {
    this.client = new STS({
      accessKeyId: this.configService.get(ConfigEnum.OSS_KEY),
      accessKeySecret: this.configService.get(ConfigEnum.OSS_SECRET),
    });
  }
 
  async findAll() {
  // roleArn填写角色ARN。
  // policy填写自定义权限策略。
  // expiration用于设置临时访问凭证有效时间单位为秒,最小值为900,最大值以当前角色设定的最大会话时间为准。
  // sessionName用于自定义角色会话名称,用来区分不同的令牌,例如填写为SessionTest。
    const result = await this.client.assumeRole(
      '这里填写ARN,就是上面图标箭头指出的部分',
      '{"Statement": [{"Action": ["*"],"Effect": "Allow","Resource": ["*"]}],"Version":"1"}',
      3600,
      'child-ram-oss',
    );
    /**
     * 使用CryptoJS对AccessKeySecret进行加密,key为OSS_KEY
     */
    const key = this.configService.get(ConfigEnum.OSS_KEY);
    const AccessKeySecret = CryptoJS.AES.encrypt(
      result.credentials.AccessKeySecret,
      key,
    ).toString();
    return {
      AccessKeyId: result.credentials.AccessKeyId,
      AccessKeySecret: AccessKeySecret,
      SecurityToken: result.credentials.SecurityToken,
      Expiration: result.credentials.Expiration,
    };
  }

2.3 前端部分

1部分的区别在于accessKeyId,accessKeySecret是从接口获取的,还多了stsToken属性。 为了安全,accessKeySecret之前在服务端进行了加密,这里需要解密。key写在.env文件中

import OSS from "ali-oss";
import React, { useState } from "react";
import { Upload, Spin } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";
import { GET_STS } from "@/api/upload";
import * as CryptoJS from "crypto-js";
 
import type { UploadProps } from "antd";

interface UploadFileProps extends UploadProps {
  onProgress?: (progress: any, file: any) => void;
}
 
const UploadFile = (props) => {
  // 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
  // const changeSrc = props.changeSrc;
  const [show, changeShow] = useState(false);
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  
  const uploadPath = (path, file) => {
    return `${path}/${file.name.split(".")[0]}-${file.uid}.${
      file.type.split("/")[1]
    }`;
  };
  const OssUpload = async (option) => {
     /**
      * 获取accessKeyId,accessKeySecret等信息
      */
    const STS = await GET_STS();
    /**
     * 解密AccessKeySecret
     */
    const decryptedSecret = await CryptoJS.AES.decrypt(
      STS.data?.AccessKeySecret,
      import.meta.env.VITE_OSS_CRYPTO_KEY,
    );
    const AccessKeySecret = decryptedSecret.toString(CryptoJS.enc.Utf8);
    const client = new OSS({
      region: "oss-cn-hangzhou",
      accessKeyId: STS.data?.AccessKeyId,
      accessKeySecret: AccessKeySecret,
      stsToken: STS.data?.SecurityToken,
      bucket: process.env.NODE_ENV == "production" ? "nest-prod" : "nest-devp", // oss上你的bucket名称
    });
    const { file, onSuccess, onProgress, onError } = option;
    const folder = "test";
    const url = uploadPath(folder, file);
    let data = null;
    try {
      data = await client.multipartUpload(url, file, {
        progress: function (p) {
          console.log("获取进度条的值==>", (p * 100).toFixed(2));
          onProgress({ percent: (p * 100).toFixed(2) }, file);
        },
      });
      onSuccess(
        data,
        file,
      );
    } catch (e) {
      // message.error(e.message);
      onError(e);
    }
  };

  const beforeUpload = (file) => {
    return true;
  };

  const uploadProps:UploadFileProps = {
    showUploadList: true,
    customRequest: OssUpload,
    beforeUpload: beforeUpload,
    fileList: fileList,
    listType: "picture-card",
    onProgress({ percent }, file) {
      const index = fileList.findIndex((item) => item.uid === file.uid);
      fileList[index].percent = percent;
      setFileList([...fileList]);
      // console.log("onProgress", `${percent}%`, file);
    },
    onChange(info: any) {
      // console.log("onChange😊===》", info);
      if (info.file.status !== "uploading") {
        console.log(info.file, info.fileList);
      }
      if (info.file.status === "done") {
        // console.log(`${info.file.name} 文件上传成功`);
        message.success(`${info.file.name} 文件上传成功`);
      } else if (info.file.status === "error") {
        info.fileList = info.fileList.filter(
          (item) => item.uid !== info.file.uid,
        );
        message.error(`${info.file.name} 文件上传失败`);
      }
      setFileList([...info.fileList]);
    },
  };

  const uploadButton = (
    <div>
      <PlusOutlined />
      <div className="ant-upload-text">Upload</div>
    </div>
  );

  return (
    <div>
      {show === true ? (
        <Spin style={{ position: "relative", left: "40px" }} />
      ) : (
        <Upload {...uploadProps}>{uploadButton}</Upload>
      )}
      <br />
    </div>
  );
};

export default UploadFile;

3.进一步优化

3.1 把client和key,secret放到状态管理zustand中

//store/module/oss.ts
import { create } from "@/store/index";
import OSS from "ali-oss";
import { GET_STS } from "@/api/upload";
import type { StsInterface } from "@/api/upload";
import * as CryptoJS from "crypto-js";

export const useOSS = create<{
  expires: number;
  client: OSS;
  STSData: StsInterface;
  initOSS: () => Promise<{
    client: OSS;
    STSData: StsInterface;
  }>;
}>()((set, get) => ({
  expires: 0,
  client: OSS, // 你可能需要提供适当的默认值
  STSData: {} as StsInterface, // 你可能需要提供适当的默认值
  initOSS: async (): Promise<{
    client: OSS;
    STSData: StsInterface;
  }> => {
    /**
     * 判断STS是否过期
     */
    if (get().expires < new Date().getTime()) {
      const STS = await GET_STS();
      /**
       * 解密AccessKeySecret
       */
      const decryptedSecret = await CryptoJS.AES.decrypt(
        STS.data?.AccessKeySecret,
        import.meta.env.VITE_OSS_CRYPTO_KEY,
      );
      const AccessKeySecret = decryptedSecret.toString(CryptoJS.enc.Utf8);
      const client = new OSS({
        region: STS.data?.OSS_REGION, // 你的oss地址
        accessKeyId: STS.data?.AccessKeyId, // 你的accessKeyId
        accessKeySecret: AccessKeySecret, // 你的accessKeySecret
        stsToken: STS.data?.SecurityToken, // 你的stsToken
        bucket: STS.data?.OSS_BUCKET, // 你的bucket
        timeout: 60000, // 上传超时时间,1分钟
      });
      const STSData = STS!.data as StsInterface;
      /**
       * 转换expires时间为时间戳
       */
      const expires = new Date(STS.data!.Expiration).getTime();
      set({ expires, client, STSData: STSData });
      return { client: client, STSData: STSData! };
    }
    return { client: get().client, STSData: get().STSData };
  },
}));

上传组件

import React, { useEffect, useState } from "react";
import { Upload, Spin, Modal, Button } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";
import type { UploadProps } from "antd";
import type { RcFile } from "antd/es/upload";
import { useOSS } from "@/store";

interface UploadFileProps extends UploadProps {
  onProgress?: (progress: any, file: any) => void;
}

const StoreUploadFile = (props) => {
  // 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
  // const changeSrc = props.changeSrc;
  const [show, changeShow] = useState(false);
  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewImage, setPreviewImage] = useState("");
  const [previewTitle, setPreviewTitle] = useState("");
  const [previewUrl, setPreviewUrl] = useState("");

  const oss = useOSS();
  useEffect(() => {
    oss.initOSS();
  }, []);

  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const getBase64 = (file: RcFile): Promise<string> =>
    new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = (error) => reject(error);
    });
  const handleCancel = () => setPreviewOpen(false);

  const handlePreview = async (file: UploadFile) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj as RcFile);
    }
    setPreviewImage(file.url || (file.preview as string));
    setPreviewOpen(true);
    setPreviewTitle(
      file.name || file.url!.substring(file.url!.lastIndexOf("/") + 1),
    );
  };
  const uploadPath = (path, file) => {
    return `${path}/${file.name.split(".")[0]}-${file.uid}.${
      file.type.split("/")[1]
    }`;
  };

  const OssUpload = async (option) => {
    const { client, STSData } = await oss.initOSS();
    const { file, onSuccess, onProgress, onError } = option;
    const folder = STSData!.OSS_DIR;
    const url = uploadPath(folder, file);
    let data = null;
    try {
      data = await client.multipartUpload(url, file, {
        progress: function (p) {
          console.log("获取进度条的值==>", (p * 100).toFixed(2));
          onProgress({ percent: (p * 100).toFixed(2) }, file);
        },
      });
      onSuccess(data, file);
    } catch (e) {
      // message.error(e.message);
      onError(e);
    }
  };
  const OSS_DOWNLOAD = async (url): Promise<void> => {
    const { client } = await oss.initOSS();
    try {
      const result = await client.signatureUrl(url, {
        expires: 3600, // 有效时间 3600 秒
        method: "GET",
      });
      console.log("OSS_DOWNLOAD result:", result);
      setPreviewUrl(result);
      // return result;
    } catch (error) {
      console.error("OSS_DOWNLOAD error:", error);
      throw error;
    }
  };
  const OSS_DELETE = async (url): Promise<void> => {
    const { client } = await oss.initOSS();
    try {
      const result = await client.delete(url);
      console.log("OSS_DELETE result:", result);
      if (result.res.status === 204) {
        message.success("删除成功");
      }
      // return result;
    } catch (error) {
      console.error("OSS_DELETE error:", error);
      throw error;
    }
  };

  const beforeUpload = (file) => {
    return true;
  };

  const uploadProps: UploadFileProps = {
    showUploadList: true,
    customRequest: OssUpload,
    beforeUpload: beforeUpload,
    fileList: fileList,
    listType: "picture-card",
    onPreview: handlePreview,
    onProgress({ percent }, file) {
      const index = fileList.findIndex((item) => item.uid === file.uid);
      fileList[index].percent = percent;
      setFileList([...fileList]);
      // console.log("onProgress", `${percent}%`, file);
    },
    onChange(info: any) {
      console.log("onChange😊===》", info);
      if (info.file.status !== "uploading") {
        console.log(info.file, info.fileList);
      }
      if (info.file.status === "done") {
        // console.log(`${info.file.name} 文件上传成功`);
        message.success(`${info.file.name} 文件上传成功`);
      } else if (info.file.status === "error") {
        info.fileList = info.fileList.filter(
          (item) => item.uid !== info.file.uid,
        );
        message.error(`${info.file.name} 文件上传失败`);
      }
      setFileList([...info.fileList]);
    },
  };

  const uploadButton = (
    <div>
      <PlusOutlined />
      <div className="ant-upload-text">上传</div>
    </div>
  );

  return (
    <div>
      {show === true ? (
        <Spin style={{ position: "relative", left: "40px" }} />
      ) : (
        <Upload {...uploadProps}>{uploadButton}</Upload>
      )}
      <Modal
        open={previewOpen}
        title={previewTitle}
        footer={null}
        onCancel={handleCancel}
      >
        <img alt="example" style={{ width: "100%" }} src={previewImage} />
      </Modal>
      <img
        alt="example"
        style={{ width: "200px", height: "200px" }}
        src={previewUrl}
      />
      {/*<span>{previewUrl}</span>*/}
      <Button
        onClick={() => {
          OSS_DOWNLOAD("test/113027753_p0-rc-upload-1704442853470-3.png");
        }}
      >
        下载
      </Button>
      <Button
        onClick={() => {
          OSS_DELETE("test/113027753_p0-rc-upload-1704436150951-3.png");
        }}
      >
        删除
      </Button>
      <br />
    </div>
  );
};

export default StoreUploadFile;