歡迎光臨我的博客拓跋的前端客棧,這個是原文地址卿捎。如果您發(fā)現(xiàn)我文章中存在錯誤,請盡情向我吐槽径密,大家一起學(xué)習(xí)一起進步φ(>ω<*)
最近做了一個需求午阵,需要上傳鏡像的tar包,小的3享扔、5G底桂,大的可能會達到20多G,而要求在瀏覽器中上傳惧眠,因此普通的上傳方式肯定無法滿足需求籽懦,必須要使用到分片上傳,前臺分片后臺就需要合并氛魁,一系列做完以后踩了很多坑暮顺,在這邊總結(jié)記錄一下厅篓。
前臺分片上傳
所謂上傳,實際上就是一個把文件通過客戶端傳給服務(wù)端的過程捶码,也就是通過前端傳給后臺的過程羽氮。在這個過程中,如果要傳輸?shù)膬?nèi)容太過龐大惫恼,在傳輸過程中就容易遇到各種各樣的問題档押。為了把出現(xiàn)問題的概率控制在最低,我們往往需要把大文件分成一份一份的小文件來依次上傳祈纯,這個過程就是我們所說分片上傳令宿。
我前臺使用的是webuploader上傳插件,參考webuploader的切片方法:
function CuteFile( file, chunkSize ) {
var pending = [],
blob = file.source,
total = blob.size,
chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1,
start = 0,
index = 0,
len, api;
api = {
file: file,
has: function() {
return !!pending.length;
},
shift: function() {
return pending.shift();
},
unshift: function( block ) {
pending.unshift( block );
}
};
while ( index < chunks ) {
len = Math.min( chunkSize, total - start );
pending.push({
file: file,
start: start,
end: chunkSize ? (start + len) : total,
total: total,
chunks: chunks,
chunk: index++,
cuted: api
});
start += len;
}
file.blocks = pending.concat();
file.remaning = pending.length;
return api;
}
其中盆繁,chunkSize表示每片大小掀淘,chunks表示切片的數(shù)目,具體切分方法也很簡單油昂,看代碼就好了。
具體在使用webuploader的過程中倾贰,只需要在創(chuàng)建webuploader實例的過程中如下配置一下:
const uploader = WebUploader.create({
// 文件接收服務(wù)端冕碟。
server: './ftp/images',
// 選擇文件的按鈕〈艺悖可選安寺。
// 內(nèi)部根據(jù)當(dāng)前運行是創(chuàng)建,可能是input元素首尼,也可能是flash.
pick: '#picker',
chunked: true,//開啟分片上傳
chunkSize: 50*1024*1024,//每片大小50M
chunkRetry: 1,//失敗后重試次數(shù)
threads: 1,//上傳并發(fā)數(shù)目
fileNumLimit: 1,//驗證文件總數(shù)量易猫,超出則不允許加入隊列
});
后臺合并
對于分片上傳上來的文件遣臼,后臺肯定要再次合并起來,重新生成跟原文件一模一樣的文件。后臺合并的方法有很多吠勘,以Node.js為例,可以使用以下方式:
buffer合并
按照慣例铜幽,先貼代碼:
// 合并分片
function mergeChunks(fileName, chunks, callback) {
console.log('chunks:' + chunks);
let chunkPaths = chunks.map(function (name) {
return path.join(process.env.IMAGESDIR, name)
});
// 采用Buffer方式合并
const readStream = function (chunkArray, cb) {
let buffers = [];
chunkPaths.forEach(function (path) {
let buffer = fs.readFileSync(path);
buffers.push(buffer);
});
let concatBuffer = Buffer.concat(buffers);
let concatFilePath = path.join(process.env.IMAGESDIR, fileName);
fs.writeFileSync(concatFilePath, concatBuffer);
chunkPaths.forEach(function (path) {
fs.unlinkSync(path)
})
cb();
};
readStream(chunkPaths, callback);
}
buffer方式合并是一種常見的文件合并方式驴剔,方法是將各個分片文件分別用fs.readFile()方式讀取,然后通過Buffer.concat()進行合并跋核。
這種方法簡單易理解岖瑰,但有個最大的缺點,就是你讀取的文件有多大砂代,合并的過程占用的內(nèi)存就有多大蹋订,因為我們相當(dāng)于把這個大文件的全部內(nèi)容都一次性載入到內(nèi)存中了,這是非常低效的刻伊。同時露戒,Node默認(rèn)的緩沖區(qū)大小的上限是2GB椒功,一旦我們上傳的大文件超出2GB,那使用這種方法就會失敗玫锋。雖然可以通過修改緩沖區(qū)大小上限的方法來規(guī)避這個問題蛾茉,但是鑒于這種合并方式極吃內(nèi)存,我不建議您這么做撩鹿。
那么谦炬,有更好的方式嗎?那是當(dāng)然节沦,下面介紹一種stream合并方式键思。
stream合并
顯然,stream(流甫贯,下面都用‘流’來表示stream)就是這種更好的方式吼鳞。
按照慣例,先貼代碼:
// 合并分片
function mergeChunks(fileName, chunks, callback) {
console.log('chunks:' + chunks);
let chunkPaths = chunks.map(function (name) {
return path.join(process.env.IMAGESDIR, name)
});
// 采用Stream方式合并
let targetStream = fs.createWriteStream(path.join(process.env.IMAGESDIR, fileName));
const readStream = function (chunkArray, cb) {
let path = chunkArray.shift();
let originStream = fs.createReadStream(path);
originStream.pipe(targetStream, {end: false});
originStream.on("end", function () {
// 刪除文件
fs.unlinkSync(path);
if (chunkArray.length > 0) {
readStream(chunkArray, callback)
} else {
cb()
}
});
};
readStream(chunkPaths, callback);
}
為什么說流更好呢叫搁?流到底是什么呢赔桌?
流是數(shù)據(jù)的集合 —— 就像數(shù)組或字符串一樣。區(qū)別在于流中的數(shù)據(jù)可能不會立刻就全部可用渴逻,并且你無需一次性的把這些數(shù)據(jù)全部放入內(nèi)存疾党。這使得流在操作大量數(shù)據(jù)或是數(shù)據(jù)從外部來源逐段發(fā)送過來的時候變得非常有用。
換句話說惨奕,當(dāng)你使用buffer方式來處理一個2GB的文件雪位,占用的內(nèi)存可能是2GB以上,而當(dāng)你使用流來處理這個文件梨撞,可能只會占用幾十個M雹洗。這就是我們?yōu)槭裁催x擇流的原因所在。
在Node.js中卧波,有4種基本類型的流时肿,分別是可讀流,可寫流幽勒,雙向流以及變換流嗜侮。
- 可讀流是對一個可以讀取數(shù)據(jù)的源的抽象。fs.createReadStream 方法是一個可讀流的例子啥容。
- 可寫流是對一個可以寫入數(shù)據(jù)的目標(biāo)的抽象锈颗。fs.createWriteStream 方法是一個可寫流的例子。
- 雙向流既是可讀的咪惠,又是可寫的击吱。TCP socket 就屬于這種。
- 變換流是一種特殊的雙向流遥昧,它會基于寫入的數(shù)據(jù)生成可供讀取的數(shù)據(jù)覆醇。
所有的流都是EventEmitter的實例朵纷。它們發(fā)出可用于讀取或?qū)懭霐?shù)據(jù)的事件。然而永脓,我們可以利用pipe方法以一種更簡單的方式使用流中的數(shù)據(jù)袍辞。
在上面那段代碼中,我們首先通過fs.createWriteStream()創(chuàng)建了一個可寫流常摧,用來存放最終合并的文件搅吁。然后使用fs.createReadStream()分別讀取各個分片后的文件,再通過pipe()方式將讀取的數(shù)據(jù)像倒水一樣“倒”到可寫流中落午,到監(jiān)控到一杯水倒完后谎懦,馬上接著倒下一杯,直到全部倒完為止溃斋。此時界拦,全部文件合并完畢。
追加文件方式合并
追加文件方式合并指的是使用fs.appendFile()的方式來進行合并梗劫。fs.appendFile()的作用是異步地追加數(shù)據(jù)到一個文件享甸,如果文件不存在則創(chuàng)建文件。data可以是一個字符串或buffer梳侨。例:
fs.appendFile('message.txt', 'data to append', (err) => {
if (err) throw err;
console.log('The "data to append" was appended to file!');
});
使用這種方法也可以將文件合并枪萄,雖然我沒有在項目中實驗,但通過在網(wǎng)上查的資料來看猫妙,性能強過buffer合并方式,但不及流合并方式聚凹。
小結(jié)
這篇文章雖然是談前臺分片和后臺合并割坠,但是由于前臺分片主要是使用webUploader實現(xiàn)的,因此側(cè)重點都在后臺合并上妒牙。
后臺合并主要有3種方式:buffer合并彼哼、流合并、追加文件方式合并湘今。三種方式各有各的特點敢朱,但是在大文件合并上,我推薦使用流方式合并摩瞎,流合并占內(nèi)存最少拴签,效率最高,是處理大文件的最佳選擇旗们。