本文出自perkinjavascript
之前實習的時候有作過大文件上傳的需求,當時咱們團隊用的是網宿科技的存儲服務,天然而然用的也是他們上傳的js-sdk,不論是網宿科技仍是七牛等提供存儲服務的公司,他們的文件上傳底層使用的基本上都是plupload庫。除了這個,百度FEX團隊開源的webuploader也是鼎鼎大名的,固然,對於文件操做的庫有許多許多,本文不作過多介紹。html
對於一箇中小型企業的小項目或者我的項目來講,使用第三方的存儲服務也許昂貴了點,且若是上傳的文件涉及到隱私的話也是不安全的(各類方案都是因項目而異的)。本文主要講解在不使用WebUploader,plupload等庫的狀況下,使用html5的File API來解決大文件上傳的問題(本文主要指前端部分)。固然,因爲是對內的項目,本文並無過多考慮瀏覽器兼容性的問題,畢竟對於IE低版本瀏覽器來講,Flash多是最適合的。前端
本文主要使用了antd爲UI組件,搭建了以下系統。html5
下圖爲文件預加載時的動圖,考慮到gif時間的限制,拿了個30多M文件測試。java
下圖爲上傳中的過程react
其實之因此不使用WebUploader等庫來實現,也是由於後端的需求跟通常的大文件上傳有一點不一樣,因此前端乾脆不使用庫來寫。先後端重點考慮的點,是使用分片上傳,且每一個分片都須要生成md5值,以便後端去校驗。所以,每一次分片上傳,都須要上傳該片斷的file,以及chunkMd5,和整個文件的fileMd5。同時,先後端採用arrayBuffer的blob格式來進行文件傳輸。git
以下爲先後端聯調的步驟github
本文Demo主要是對UI組件進行描述,因此沒有考慮數據層,讀者能夠本身配合dva或者redux。下文爲主要的代碼結構。web
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Upload, Icon, Button, Progress,Checkbox, Modal, Spin, Radio, message } from 'antd'
import request from 'superagent'
import SparkMD5 from 'spark-md5'
const confirm = Modal.confirm
const Dragger = Upload.Dragger
class FileUpload extends Component {
constructor(props) {
super(props)
this.state = {
preUploading:false, //預處理
chunksSize:0, // 上傳文件分塊的總個數
currentChunks:0, // 當前上傳的隊列個數 當前還剩下多少個分片沒上傳
uploadPercent:-1, // 上傳率
preUploadPercent:-1, // 預處理率
uploadRequest:false, // 上傳請求,即進行第一個過程當中
uploaded:false, // 表示文件是否上傳成功
uploading:false, // 上傳中狀態
}
}
showConfirm = () => {
const _this = this
confirm({
title: '是否提交上傳?',
content: '點擊確認進行提交',
onOk() {
_this.preUpload()
},
onCancel() { },
})
}
preUpload = ()=>{
// requestUrl,返回能夠上傳的分片隊列
//...
}
handlePartUpload = (uploadList)=>{
// 分片上傳
// ...
}
render() {
const {preUploading,uploadPercent,preUploadPercent,uploadRequest,uploaded,uploading} = this.state
const _this = this
const uploadProp = {
onRemove: (file) => {
// ...
},
beforeUpload: (file) => {
// ...對文件的預處理
},
fileList: this.state.fileList,
}
return (
<div className="content-inner">
<Spin tip={
<div >
<h3 style={{margin:'10px auto',color:'#1890ff'}}>文件預處理中...</h3>
<Progress width={80} percent={preUploadPercent} type="circle" status="active" />
</div>
}
spinning={preUploading}
style={{ height: 350 }}>
<div style={{ marginTop: 16, height: 250 }}>
<Dragger {...uploadProp}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">點擊或者拖拽文件進行上傳</p>
<p className="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>
</Dragger>
{uploadPercent>=0&&!!uploading&&<div style={{marginTop:20,width:'95%'}}>
<Progress percent={uploadPercent} status="active" />
<h4>文件上傳中,請勿關閉窗口</h4>
</div>}
{!!uploadRequest&&<h4 style={{color:'#1890ff'}}>上傳請求中...</h4>}
{!!uploaded&&<h4 style={{color:'#52c41a'}}>文件上傳成功</h4>}
<Button type="primary" onClick={this.showConfirm} disabled={!!(this.state.preUploadPercent <100)}>
<Icon type="upload" />提交上傳
</Button>
</div>
</Spin>
</div>
)
}
}
FileUpload.propTypes = {
//...
}
export default FileUpload
複製代碼
使用Html5 的File API是如今主流的處理文件上傳的方案。在使用FileReader API以前,應該瞭解一下Blob對象,Blob對象表示不可變的相似文件對象的原始數據。File接口就是基於Blob,繼承了blob的功能並將其擴展使其支持用戶系統上的文件。json
本文先後端約束採用二進制的ArrayBuffer 對象格式來傳輸文件,類型話數組(ArrayBuffer)能夠直接操做內存,接口之間徹底能夠用二進制數據通訊。
使用FileReader來讀取文件,主要有5個方法:
方法名 | 參數 | 描述 |
---|---|---|
abort | none | 中斷讀取 |
readAsBinaryString | file | 將文件讀取爲二進制碼 |
readAsDataURL | file | 將文件讀取爲DataURL |
readAsText | file,[encoding] | 將文件讀取爲文本 |
readAsArrayBuffer | file | 將文件讀取爲ArrayBuffer |
const uploadProp = {
onRemove: (file) => {
this.setState(({ fileList }) => {
const index = fileList.indexOf(file)
const newFileList = fileList.slice()
newFileList.splice(index, 1)
return {
fileList: newFileList,
}
})
},
beforeUpload: (file) => {
// 首先清除一下各類上傳的狀態
this.setState({
uploaded:false, // 上傳成功
uploading:false, // 上傳中
uploadRequest:false // 上傳預處理
})
// 兼容性的處理
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
chunkSize = 1024*1024*5, // 切片每次5M
chunks = Math.ceil(file.size / chunkSize),
currentChunk = 0, // 當前上傳的chunk
spark = new SparkMD5.ArrayBuffer(),
// 對arrayBuffer數據進行md5加密,產生一個md5字符串
chunkFileReader = new FileReader(), // 用於計算出每一個chunkMd5
totalFileReader = new FileReader() // 用於計算出總文件的fileMd5
let params = {chunks: [], file: {}}, // 用於上傳全部分片的md5信息
arrayBufferData = [] // 用於存儲每一個chunk的arrayBuffer對象,用於分片上傳使用
params.file.fileName = file.name
params.file.fileSize = file.size
totalFileReader.readAsArrayBuffer(file)
totalFileReader.onload = function(e){
// 對整個totalFile生成md5
spark.append(e.target.result)
params.file.fileMd5 = spark.end() // 計算整個文件的fileMd5
}
chunkFileReader.onload = function (e) {
// 對每一片分片進行md5加密
spark.append(e.target.result)
// 每個分片須要包含的信息
let obj = {
chunk:currentChunk + 1,
start:currentChunk * chunkSize, // 計算分片的起始位置
end:((currentChunk * chunkSize + chunkSize) >= file.size) ? file.size : currentChunk * chunkSize + chunkSize, // 計算分片的結束位置
chunkMd5:spark.end(),
chunks
}
// 每一次分片onload,currentChunk都須要增長,以便來計算分片的次數
currentChunk++;
params.chunks.push(obj)
// 將每一塊分片的arrayBuffer存儲起來,用來partUpload
let tmp = {
chunk:obj.chunk,
currentBuffer:e.target.result
}
arrayBufferData.push(tmp)
if (currentChunk < chunks) {
// 當前切片總數沒有達到總數時
loadNext()
// 計算預處理進度
_this.setState({
preUploading:true,
preUploadPercent:Number((currentChunk / chunks * 100).toFixed(2))
})
} else {
//記錄全部chunks的長度
params.file.fileChunks = params.chunks.length
// 表示預處理結束,將上傳的參數,arrayBuffer的數據存儲起來
_this.setState({
preUploading:false,
uploadParams:params,
arrayBufferData,
chunksSize:chunks,
preUploadPercent:100
})
}
}
fileReader.onerror = function () {
console.warn('oops, something went wrong.');
};
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext()
// 只容許一份文件上傳
this.setState({
fileList: [file],
file: file
})
return false
},
fileList: this.state.fileList,
}
複製代碼
{
file:{
fileChunks:119,
fileMd5:"f5aeec69076483585f4f112223265c0c",
fileName:"xxxx.test",
fileSize:6205952600
},
chunks:[{
chunk:1,
chunkMd5:"8770f43dc59effdc8b995e4aacc8a26c",
chunks:119,
end:5242880,
start:0
},
...
]
}
複製代碼
{
Chunks:[
{
chunk: 1,
chunkMd5:"8770f43dc59effdc8b995e4aacc8a26c",
fileMd5:"f5aeec69076483585f4f672223265c0c",
end: 5242880,
start:0,
status:"pending"
},
…
],
Code:200,
FileMd5:"f5aeec69076483585f4f672223265c0c"
MaxThreads:1,
Message:"OK",
Total:119,
Uploaded:0
}
複製代碼
let uploadList = res.body.Chunks.filter((value)=>{
return value.status === 'Pending'
})
// 從返回結果中獲取當前還有多少個分片沒傳
let currentChunks = res.body.Total - res.body.Uploaded
// 得到上傳進度
let uploadPercent = Number(((this.state.chunksSize - currentChunks) /this.state.chunksSize * 100).toFixed(2))
// 上傳以前,先判斷文件是否已經上傳成功
if(uploadPercent === 100){
message.success('上傳成功')
this.setState({
uploaded:true, // 讓進度條消失
uploading:false
})
}else{
this.setState({
uploaded:false,
uploading:true
})
}
this.setState({
uploadRequest:false, // 上傳請求成功
currentChunks,
uploadPercent
})
//進行分片上傳
this.handlePartUpload(uploadList)
複製代碼
handlePartUpload = (uploadList)=>{
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
// 遍歷uploadList
uploadList.forEach((value)=>{
// 取出每一個分片裏的基本屬性
let {fileMd5,chunkMd5,chunk,start,end} = value
let formData = new FormData(),
//新建一個Blob對象,將對應分片的arrayBuffer加入Blob中
blob = new Blob([this.state.arrayBufferData[chunk-1].currentBuffer],{type: 'application/octet-stream'}),
// 上傳的參數
params = `fileMd5=${fileMd5}&chunkMd5=${chunkMd5}&chunk=${chunk}&start=${start}&end=${end}&chunks=${this.state.arrayBufferData.length}`
// 將生成blob塞入到formdata中傳入服務端
formData.append('chunk', blob, chunkMd5)
request
.post(`http://X.X.X.X/api/upload_file_part?${params}`)
.send(formData)
.end((err,res)=>{
if(res.body.Code === 200){
let currentChunks = this.state.currentChunks
--currentChunks
// 計算上傳進度
let uploadPercent = Number(((this.state.chunksSize - currentChunks) /this.state.chunksSize * 100).toFixed(2))
this.setState({
currentChunks, // 同步當前所需上傳的chunks
uploadPercent,
uploading:true
})
if(currentChunks ===0){
// 調用驗證api
this.checkUpload()
message.success('上傳成功')
this.setState({
uploading:false, // 讓進度條消失
uploaded:true
})
}
}
})
})
}
複製代碼
以上就是一個簡單的基於react的大文件上傳組件,主要的知識點包括:分片上傳技術,FileReader API,ArrayBuffer數據結構,md5加密技術,Blob對象的應用等 知識點。讀者能夠自行擴展該React組件,能夠跟Dva/Redux結合擴展Model層或者集中的狀態管理等。同時,對於該組件中出現的異步流程是很簡單粗暴的,如何創建合理的異步流程控制,也是須要去思考的。固然,對於大文件來講,文件壓縮也是一個須要去考慮的點,好比使用snappy.js等工具庫。