Closed afc163 closed 5 years ago
为了安全,上传依赖于后端生成签名。demo 只是贴代码,不需要实际可运行?
之前项目中写过一个组件,代码贴在下面了.这里需要后台配合获取临时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>
)
}
}
这里是用阿里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
}
所以你们的Upload 都不和 Form组件一起用的吗? 用getFieldDecorator 的方式 并且用的是localstorage的token,需要每次手动axios自己带token上传,有谁能提供一个例子?
@ouzhou 你说的问题,和这里的业务场景不太一样. 如何你的后台支持图片上传,那么直接用Upload组件上传即可 token的话Upload组件支持headers设置的
这里的业务场景应该大致是这样的,需要图片直传OSS
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>
需要在表单中上传不同格式的文件, 作为项目的附件, 比如 .doc , .PDF, .ppt; 查看了阿里云的文档,发现 aliyun 存储不同格式的文件 需要不同的Content-Type; 可以提供例子🌰咩? 非常感谢
首先从服务端获取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,
});
多选时onChange和beforeUpload会触发两次怎么解决??
What problem does this feature solve?
阿里云 oss 在内部和外部使用广泛,对国内用户是一个很实用的案例。
What does the proposed API look like?
一个演示 demo。