ant-design / ant-design

An enterprise-class UI design language and React UI library
https://ant.design
MIT License
92.31k stars 49.48k forks source link

提供阿里云 oss 和 Upload 一起使用的例子 #15546

Closed afc163 closed 5 years ago

afc163 commented 5 years ago

What problem does this feature solve?

阿里云 oss 在内部和外部使用广泛,对国内用户是一个很实用的案例。

What does the proposed API look like?

一个演示 demo。

DiamondYuan commented 5 years ago

为了安全,上传依赖于后端生成签名。demo 只是贴代码,不需要实际可运行?

Durianicecream commented 5 years ago

之前项目中写过一个组件,代码贴在下面了.这里需要后台配合获取临时STS凭证,然后才可以上传

import React from 'react'
import { Upload, message, Icon, Progress } from 'antd'
import PropTypes from 'prop-types'
import Crypto from 'crypto'
import { fetchJSON } from 'services/ajax'

export default class FileUpload extends React.Component {
    static propTypes = {
        value: PropTypes.shape({
            url: PropTypes.string,
            size: PropTypes.number
        }),
        title: PropTypes.string,
        onChange: PropTypes.func
    }
    static defaultProps = {
        title: '点击此处上传文件',
        onChange: () => {}
    }
    constructor(props) {
        super(props)
        this.state = {
            progress: 0,
            uploadStatus: 'init',
            uploadData: {
                success_action_status: 200,
                policy: new Buffer(
                    JSON.stringify({
                        expiration: '2020-01-01T12:00:00.000Z',
                        conditions: [{ bucket: 'xxx' }]
                    })
                ).toString('base64')
            }, //填写你的bucket
            uploadPath: '',
            uploadHost: 'https://output-mingbo.oss-cn-beijing.aliyuncs.com/',
            uploadName: 'file',
            STS: null,
            isInit: false
        }
    }

    static getDerivedStateFromProps(nextProps, prevState) {
        if (prevState.isInit) {
            return null
        }
        if (nextProps.value && nextProps.value.url) {
            return {
                uploadStatus: 'done',
                isInit: true
            }
        }
        return null
    }

    /**
     * 获取临时STS凭证
     */
    fetchSignature = () => {
        if (this.state.STS) {
            return Promise.resolve()
        } else {
            const localSTS = this.getLocalSTS()
            if (localSTS) {
                this.setState({ STS: localSTS })
                return Promise.resolve()
            } else {
                const url = '/api/upload/osstoken'
                return fetchJSON(url, {}, 'POST').then((res) => {
                    const STS = {
                        Signature: this.signature(
                            this.state.uploadData.policy,
                            res.data.accesskeysecret
                        ),
                        OSSAccessKeyId: res.data.accesskeyid,
                        'x-oss-security-token': res.data.securitytoken
                    }
                    this.setLocalSTS(STS)
                    this.setState({ STS })
                    return true
                })
            }
        }
    }

    getLocalSTS = () => {
        const STSInfo = JSON.parse(sessionStorage.getItem('STS'))
        if (!STSInfo || !STSInfo.expires) return null
        if (new Date(STSInfo.expires) < new Date()) return null
        delete STSInfo.expires
        return STSInfo
    }

    setLocalSTS = (data) => {
        data.expires = new Date(new Date().getTime() + 10 * 60 * 1000)
        sessionStorage.setItem('STS', JSON.stringify(data))
    }

    /**
     * 生成上传签名
     * @param {string} policyText
     * @param {string} accesskey
     * @return siginature
     */
    signature = (policyText, accesskey) => {
        const signature = Crypto.Hmac('sha1', accesskey)
            .update(policyText)
            .digest('base64')
        return signature
    }

    /**
     * 生成MD5签名
     * @param {File} file 文件
     * @return hex 签名
     */
    md5 = (file) => {
        const hash = Crypto.Hash('md5')
        const chunkSize = 2 * 1024 * 1024
        const chunkLen = Math.ceil(file.size / chunkSize)
        const blobSlice =
            File.prototype.mozSlice ||
            File.prototype.webkitSlice ||
            File.prototype.slice
        const fileReader = new FileReader()
        let bs = fileReader.readAsBinaryString
        let currentChunk = 0

        const loadNext = (chunkSize) => {
            let start = currentChunk * chunkSize
            let end = start + chunkSize >= file.size ? file.size : start + chunkSize
            if (bs) fileReader.readAsBinaryString(blobSlice.call(file, start, end))
            else fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
        }

        return new Promise((resolve, reject) => {
            try {
                loadNext(chunkSize)
            } catch (err) {
                reject(err)
            }

            // 文件读取完毕之后的处理
            fileReader.onload = (e) => {
                try {
                    hash.update(e.target.result)
                    currentChunk += 1
                    this.setState({
                        progressPercent: Math.ceil((10 * currentChunk) / chunkLen, 10)
                    })
                    if (currentChunk < chunkLen) {
                        loadNext()
                    } else {
                        resolve(hash.digest('hex'))
                    }
                } catch (err) {
                    reject(err)
                }
            }
        })
    }

    /**
     * 获取文件后缀名
     * @param {string} name 文件名
     * @return {string} 文件后缀名
     */
    suffix = (file_name) => {
        const index = file_name.lastIndexOf('.')
        const strtype = file_name.substr(index + 1, file_name.length)
        return strtype.toLowerCase()
    }

    beforeUpload = (file) => {
        // 获取临时凭证
        return this.fetchSignature()
            .then(() => {
                // 生成文件MD5
                this.setState({ status: 'uploading' })
                return this.md5(file)
            })
            .then((md5) => {
                return this.setState({
                    uploadPath: `upload/${md5}.${this.suffix(file.name)}`
                })
            })
    }

    handleFileChange = (event) => {
        const status = event.file.status
        this.setState({ uploadStatus: status })
        if (status === 'error') {
            message.error('服务器处理异常')
        } else if (status === 'done') {
            message.success('上传成功')
            this.setState({
                progressPercent: 100
            })
            const url = `${this.state.uploadHost}/${this.state.uploadPath}`
            this.props.onChange({ url, size: event.file.size })
        } else if (status === 'uploading') {
            this.setState({
                progressPercent: Math.floor(event.file.percent * 0.9 + 10)
            })
        }
    }

    renderFileImage = () => {
        if (this.state.uploadStatus === 'uploading') {
            return (
                <Progress
                    type="circle"
                    percent={this.state.progressPercent}
                    width={100}
                />
            )
        } else if (this.state.uploadStatus === 'init') {
            return (
                <React.Fragment>
                    <p className="ant-upload-drag-icon">
                        <Icon type="inbox" />
                    </p>
                    <p className="ant-upload-text">{this.props.title}</p>
                </React.Fragment>
            )
        } else if (this.state.uploadStatus === 'done') {
            return (
                <React.Fragment>
                    <p className="ant-upload-drag-icon">
                        <Icon type="file" />
                    </p>
                    <p className="ant-upload-text">重新上传</p>
                </React.Fragment>
            )
        } else if (this.state.uploadStatus === 'error') {
            return (
                <Progress
                    type="circle"
                    status="exception"
                    width={100}
                    percent={this.state.progressPercent}
                />
            )
        }
    }

    render() {
        return (
            <div style={{ marginTop: 16, height: 180 }}>
                <Upload.Dragger
                    beforeUpload={this.beforeUpload}
                    onChange={this.handleFileChange}
                    name={this.state.uploadName}
                    action={this.state.uploadHost}
                    data={{
                        ...this.state.uploadData,
                        ...this.state.STS,
                        key: this.state.uploadPath
                    }}
                    showUploadList={false}
                >
                    {this.renderFileImage()}
                </Upload.Dragger>
            </div>
        )
    }
}
Durianicecream commented 5 years ago

这里是用阿里OSS SDK的上传方式, 核心函数就两个,相对来说代码量少一点,不过还是需要后台配合获取STS临时凭证

import AliyunOSS from './aliyun-oss-sdk-5.2.0.min.js'
import AliyunUpload from './aliyun-upload-sdk-1.4.0.min.js'

    /**
     * 初始化上传SDK
     */

    beforeUpload = (file) => {
        if (!this.validateFile(file)) return

        this.file = file
        this.fetchSignature()
            .then(() => {
                const uploader = this.initAliUploader()
                uploader.addFile(file)
                uploader.startUpload()
            })
            .catch((err) => {
                console.log(err)
                Toast.error('服务器处理异常')
            })
    }

         /**
     * 初始化上传SDK
     */
    initAliUploader = () => {
        const uploader = new window.AliyunUpload.Vod({
            //分片大小默认1M,不能小于100K
            partSize: 1048576,
            //并行上传分片个数,默认5
            parallel: 5,
            //网络原因失败时,重新上传次数,默认为3
            retryCount: 3,
            //网络原因失败时,重新上传间隔时间,默认为2秒
            retryDuration: 2,
            // 开始上传
            onUploadstarted: (uploadInfo) => {
                uploader.setUploadAuthAndAddress(
                    uploadInfo,
                    this.STS.uploadAuth,
                    this.STS.uploadAddress,
                    this.STS.videoId
                )
                this.setState({
                    status: 'uploading'
                })
            },
            // 文件上传成功
            onUploadSucceed: (uploadInfo) => {
                Toast.success('上传成功')
                this.setState({ status: 'done', progress: 1 })
                this.props.onChange(this.STS.videoId, this.file)
            },
            // 文件上传失败
            onUploadFailed: (uploadInfo, code, message) => {
                console.log(code, message)
                Toast.error('服务器处理异常')
                this.setState({ status: 'error' })
            },
            // 文件上传进度,单位:字节
            onUploadProgress: (uploadInfo, totalSize, loadedPercent) => {
                this.setState({ progress: loadedPercent })
            }
        })
        return uploader
    }
ouzhou commented 5 years ago

所以你们的Upload 都不和 Form组件一起用的吗? 用getFieldDecorator 的方式 并且用的是localstorage的token,需要每次手动axios自己带token上传,有谁能提供一个例子?

Durianicecream commented 5 years ago

@ouzhou 你说的问题,和这里的业务场景不太一样. 如何你的后台支持图片上传,那么直接用Upload组件上传即可 token的话Upload组件支持headers设置的

Durianicecream commented 5 years ago

这里的业务场景应该大致是这样的,需要图片直传OSS 企业微信截图_15537428814211

kanweiwei commented 5 years ago

首先从服务端获取oss令牌,用的browser版本sdk的ali-oss,根据oss相关信息创建ossClient,然后使用类似ossClient.put(path, file)的方法上传,并更新Upload里的fileList。

ossClientCreator

const OSS =  require("ali-oss");

export interface IOssConfig {
    accessKeyId: string;
    accessKeySecret: string;
    securityToken: string;
    region: string;
    bucket: string;
    endpoint: string;
    [propName: string]: any;
}

export default (ossConfig: IOssConfig) => {
    return new OSS({
        accessKeyId: ossConfig.accessKeyId,
        accessKeySecret: ossConfig.accessKeySecret,
        stsToken: ossConfig.securityToken,
        region: ossConfig.region,
        bucket: ossConfig.bucket
    });
};

ossService.js

import http from '../httpService';
import { IOssConfig } from 'src/utils/ossClientCreator';

export async function getOssConfig(): Promise<IOssConfig> {
  let res = await http.post('/api/services/app/Util/OssToken');
  return res.data.result;
}

export async function multipartUpload(ossClient: any, path: string, file: File) {
  let ossRes = await ossClient.multipartUpload(path, file);
  let url: string = '';
  if (ossRes && ossRes.name) {
    url = `http://${ossClient.options.bucket}.${ossClient.options.endpoint.host}/${ossRes.name}`;
  } else {
    throw new Error('上传失败');
  }
  ossClient = null;
  return url;
}

export async function put(ossClient: any, path: string, file: File) {
  let ossRes = await ossClient.put(path, file);

  let url: string = '';
  if (ossRes && ossRes.name) {
    url = `http://${ossClient.options.bucket}.${ossClient.options.endpoint.host}/${ossRes.name}`;
  } else {
    throw new Error('上传失败');
  }
  ossClient = null;
  return url;
}

export async function upload(ossClient: any, path: string, file: File) {
  let size = file.size / 1024;
  if (size > 100 * 1024) {
   throw new Error( '附件大小不能超过100M');
  }
  if (size > 50 * 1024) {
    return await multipartUpload(ossClient, path, file);
  }
  let result = await ossClient.put(path, file);
  // console.log(result, '上传成功');
  let url: string = '';
  if (result && result.name) {
    url = `http://${ossClient.options.bucket}.${ossClient.options.endpoint.host}/${result.name}`;
  } else {
    throw new Error('上传失败');
  }
  ossClient = null;
  return url;
}

export default {
  getOssConfig,
  multipartUpload,
  put,
  upload,
};
async componentDidMount() {
    const config = await ossService.getOssConfig();
    this.client = ossClientCreator(config);
  }
     <Upload
          customRequest={(e: any) => {
            if (!this.client) return;
            ossService
              .put(this.client, 'upload/' + e.file.uid.replace(/-/g, '') + '.' + e.file.type.match(/image\/(\w*)/)[1], e.file)
              .then((url: any) => {
                let index = this.state.fileList.findIndex((n: UploadFile) => n.uid === e.file.uid);
                if (index > -1) {
                  this.state.fileList[index].url = url;
                  this.state.fileList[index].status = 'done';
                } else {
                  this.state.fileList.push({
                    ...e.file,
                    status: 'done',
                    url: url,
                  });
                }
                const { onUpdate } = this.props;
                if (onUpdate) {
                  onUpdate(this.state.fileList);
                }
                this.setState({
                  fileList: this.state.fileList
                });
              });
          }}
          listType="picture-card"
          fileList={fileList}
          onPreview={this.handlePreview}
          onChange={this.handleChange}
          onRemove={this.handleRemove}
          multiple={true}
        >
          {uploadButton}
     </Upload>
macc6579 commented 5 years ago

需要在表单中上传不同格式的文件, 作为项目的附件, 比如 .doc , .PDF, .ppt; 查看了阿里云的文档,发现 aliyun 存储不同格式的文件 需要不同的Content-Type; 可以提供例子🌰咩? 非常感谢

afc163 commented 5 years ago

Ant Design 使用Upload组件默认方式上传图片到阿里云OSShttps://juejin.im/post/5d6a96745188256332722bc3

fengyun2 commented 5 years ago

首先从服务端获取oss令牌,用的browser版本sdk的ali-oss,根据oss相关信息创建ossClient,然后使用类似ossClient.put(path, file)的方法上传,并更新Upload里的fileList。

ossClientCreator

const OSS =  require("ali-oss");

export interface IOssConfig {
    accessKeyId: string;
    accessKeySecret: string;
    securityToken: string;
    region: string;
    bucket: string;
    endpoint: string;
    [propName: string]: any;
}

export default (ossConfig: IOssConfig) => {
    return new OSS({
        accessKeyId: ossConfig.accessKeyId,
        accessKeySecret: ossConfig.accessKeySecret,
        stsToken: ossConfig.securityToken,
        region: ossConfig.region,
        bucket: ossConfig.bucket
    });
};

ossService.js

import http from '../httpService';
import { IOssConfig } from 'src/utils/ossClientCreator';

export async function getOssConfig(): Promise<IOssConfig> {
  let res = await http.post('/api/services/app/Util/OssToken');
  return res.data.result;
}

export async function multipartUpload(ossClient: any, path: string, file: File) {
  let ossRes = await ossClient.multipartUpload(path, file);
  let url: string = '';
  if (ossRes && ossRes.name) {
    url = `http://${ossClient.options.bucket}.${ossClient.options.endpoint.host}/${ossRes.name}`;
  } else {
    throw new Error('上传失败');
  }
  ossClient = null;
  return url;
}

export async function put(ossClient: any, path: string, file: File) {
  let ossRes = await ossClient.put(path, file);

  let url: string = '';
  if (ossRes && ossRes.name) {
    url = `http://${ossClient.options.bucket}.${ossClient.options.endpoint.host}/${ossRes.name}`;
  } else {
    throw new Error('上传失败');
  }
  ossClient = null;
  return url;
}

export async function upload(ossClient: any, path: string, file: File) {
  let size = file.size / 1024;
  if (size > 100 * 1024) {
   throw new Error( '附件大小不能超过100M');
  }
  if (size > 50 * 1024) {
    return await multipartUpload(ossClient, path, file);
  }
  let result = await ossClient.put(path, file);
  // console.log(result, '上传成功');
  let url: string = '';
  if (result && result.name) {
    url = `http://${ossClient.options.bucket}.${ossClient.options.endpoint.host}/${result.name}`;
  } else {
    throw new Error('上传失败');
  }
  ossClient = null;
  return url;
}

export default {
  getOssConfig,
  multipartUpload,
  put,
  upload,
};
async componentDidMount() {
    const config = await ossService.getOssConfig();
    this.client = ossClientCreator(config);
  }
     <Upload
          customRequest={(e: any) => {
            if (!this.client) return;
            ossService
              .put(this.client, 'upload/' + e.file.uid.replace(/-/g, '') + '.' + e.file.type.match(/image\/(\w*)/)[1], e.file)
              .then((url: any) => {
                let index = this.state.fileList.findIndex((n: UploadFile) => n.uid === e.file.uid);
                if (index > -1) {
                  this.state.fileList[index].url = url;
                  this.state.fileList[index].status = 'done';
                } else {
                  this.state.fileList.push({
                    ...e.file,
                    status: 'done',
                    url: url,
                  });
                }
                const { onUpdate } = this.props;
                if (onUpdate) {
                  onUpdate(this.state.fileList);
                }
                this.setState({
                  fileList: this.state.fileList
                });
              });
          }}
          listType="picture-card"
          fileList={fileList}
          onPreview={this.handlePreview}
          onChange={this.handleChange}
          onRemove={this.handleRemove}
          multiple={true}
        >
          {uploadButton}
     </Upload>

customRequest方法中解构 e.file 只有 uid 成功,其他属性丢失

this.state.fileList.push({
                    ...e.file,
                    status: 'done',
                    url: url,
                  });
chenyu1990 commented 2 years ago

多选时onChange和beforeUpload会触发两次怎么解决??