JavaScript實(shí)現(xiàn)大文件分片上傳處理
很多時(shí)候我們?cè)谔幚砦募蟼鲿r(shí),如視頻文件,小則幾十M,大則 1G+,以一般的HTTP請(qǐng)求發(fā)送數(shù)據(jù)的方式的話,會(huì)遇到的問(wèn)題:
1、文件過(guò)大,超出服務(wù)端的請(qǐng)求大小限制;
2、請(qǐng)求時(shí)間過(guò)長(zhǎng),請(qǐng)求超時(shí);
3、傳輸中斷,必須重新上傳導(dǎo)致前功盡棄
這些問(wèn)題很影響用戶的體驗(yàn)感,所以下面介紹一種基于原生JavaScript進(jìn)行文件分片處理上傳的方案,具體實(shí)現(xiàn)過(guò)程如下:
1、通過(guò)dom獲取文件對(duì)象,并且對(duì)文件進(jìn)行MD5加密(文件內(nèi)容+文件標(biāo)題形式),采用SparkMD5進(jìn)行文件加密;
2、進(jìn)行分片設(shè)置,文件File基于Blob, 繼承了Blob的功能,可以把File當(dāng)成Blob的子類(lèi),利于Blob的slice方法進(jìn)行文件分片處理,并且依次進(jìn)行上傳
3、分片文件上傳完成后,請(qǐng)求合并接口后端進(jìn)行文件合并處理即可
1. 上傳文件頁(yè)面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>文件上傳</title>
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script>
<style>
/* 自定義進(jìn)度條樣式 */
.precent input[type=range] {
-webkit-appearance: none;
/*清除系統(tǒng)默認(rèn)樣式*/
width: 7.8rem;
/* background: -webkit-linear-gradient(#ddd, #ddd) no-repeat, #ddd; */
/*設(shè)置左邊顏色為#61bd12,右邊顏色為#ddd*/
background-size: 75% 100%;
/*設(shè)置左右寬度比例*/
height: 0.6rem;
/*橫條的高度*/
border-radius: 0.4rem;
border: 1px solid #ddd;
box-shadow: 0 0 10px rgba(0,0,0,.125) inset ;
}
/*拖動(dòng)塊的樣式*/
.precent input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
/*清除系統(tǒng)默認(rèn)樣式*/
height: .9rem;
/*拖動(dòng)塊高度*/
width: .9rem;
/*拖動(dòng)塊寬度*/
background: #fff;
/*拖動(dòng)塊背景*/
border-radius: 50%;
/*外觀設(shè)置為圓形*/
border: solid 1px #ddd;
/*設(shè)置邊框*/
}
</style>
</head>
<body>
<h1>大文件分片上傳測(cè)試</h1>
<div>
<input id="file" type="file" name="avatar" />
<div style="padding: 10px 0;">
<input id="submitBtn" type="button" value="提交" />
<input id="pauseBtn" type="button" value="暫停" />
</div>
<div class="precent">
<input type="range" value="0" /><span id="precentVal">0%</span>
</div>
</div>
<script type="text/javascript" src="./js/index.js"></script>
</body>
</html>
2. 大文件分片上傳處理
$(document).ready(() => {
const submitBtn = $('#submitBtn'); //提交按鈕
const precentDom = $(".precent input")[0]; // 進(jìn)度條
const precentVal = $("#precentVal"); // 進(jìn)度條值對(duì)應(yīng)dom
const pauseBtn = $('#pauseBtn'); // 暫停按鈕
// 每個(gè)chunk的大小,設(shè)置為1兆
const chunkSize = 1 * 1024 * 1024;
// 獲取slice方法,做兼容處理
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
// 對(duì)文件進(jìn)行MD5加密(文件內(nèi)容+文件標(biāo)題形式)
const hashFile = (file) => {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
function loadNext() {
const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
fileReader.onload = e => {
spark.append(e.target.result); // Append array buffer
currentChunk += 1;
if (currentChunk < chunks) {
loadNext();
} else {
console.log('finished loading');
const result = spark.end();
// 通過(guò)內(nèi)容和文件名稱進(jìn)行md5加密
const sparkMd5 = new SparkMD5();
sparkMd5.append(result);
sparkMd5.append(file.name);
const hexHash = sparkMd5.end();
resolve(hexHash);
}
};
fileReader.onerror = () => {
console.warn('文件讀取失??!');
};
loadNext();
}).catch(err => {
console.log(err);
});
}
// 提交
submitBtn.on('click', async () => {
var pauseStatus = false;
var nowUploadNums = 0
// 1.讀取文件
const fileDom = $('#file')[0];
const files = fileDom.files;
const file = files[0];
if (!file) {
alert('沒(méi)有獲取文件');
return;
}
// 2.設(shè)置分片參數(shù)屬性、獲取文件MD5值
const hash = await hashFile(file); //文件 hash
const blockCount = Math.ceil(file.size / chunkSize); // 分片總數(shù)
const axiosPromiseArray = []; // axiosPromise數(shù)組
// 文件上傳
const uploadFile = () => {
const start = nowUploadNums * chunkSize;
const end = Math.min(file.size, start + chunkSize);
// 構(gòu)建表單
const form = new FormData();
// blobSlice.call(file, start, end)方法是用于進(jìn)行文件分片
form.append('file', blobSlice.call(file, start, end));
form.append('index', nowUploadNums);
form.append('hash', hash);
// ajax提交 分片,此時(shí) content-type 為 multipart/form-data
const axiosOptions = {
onUploadProgress: e => {
nowUploadNums++;
// 判斷分片是否上傳完成
if (nowUploadNums < blockCount) {
setPrecent(nowUploadNums, blockCount);
uploadFile(nowUploadNums)
} else {
// 4.所有分片上傳后,請(qǐng)求合并分片文件
axios.all(axiosPromiseArray).then(() => {
setPrecent(blockCount, blockCount); // 全部上傳完成
axios.post('/file/merge_chunks', {
name: file.name,
total: blockCount,
hash
}).then(res => {
console.log(res.data, file);
pauseStatus = false;
alert('上傳成功');
}).catch(err => {
console.log(err);
});
});
}
},
};
// 加入到 Promise 數(shù)組中
if (!pauseStatus) {
axiosPromiseArray.push(axios.post('/file/upload', form, axiosOptions));
}
}
// 設(shè)置進(jìn)度條
function setPrecent(now, total) {
var prencentValue = ((now / total) * 100).toFixed(2)
precentDom.value = prencentValue
precentVal.text(prencentValue + '%')
precentDom.style.cssText = `background:-webkit-linear-gradient(top, #059CFA, #059CFA) 0% 0% / ${prencentValue}% 100% no-repeat`
}
// 暫停
pauseBtn.on('click', (e) => {
pauseStatus = !pauseStatus;
e.currentTarget.value = pauseStatus ? '開(kāi)始' : '暫停'
if (!pauseStatus) {
uploadFile(nowUploadNums)
}
})
uploadFile();
});
})
3. 文件上傳和合并分片文件接口(node)
const Router = require('koa-router');
const multer = require('koa-multer');
const fs = require('fs-extra');
const path = require('path');
const router = new Router();
const { mkdirsSync } = require('../utils/dir');
const uploadPath = path.join(__dirname, 'upload');
const chunkUploadPath = path.join(uploadPath, 'temp');
const upload = multer({ dest: chunkUploadPath });
// 文件上傳接口
router.post('/file/upload', upload.single('file'), async (ctx, next) => {
const { index, hash } = ctx.req.body;
const chunksPath = path.join(chunkUploadPath, hash, '/');
if(!fs.existsSync(chunksPath)) mkdirsSync(chunksPath);
fs.renameSync(ctx.req.file.path, chunksPath + hash + '-' + index);
ctx.status = 200;
ctx.res.end('Success');
})
// 合并分片文件接口
router.post('/file/merge_chunks', async (ctx, next) => {
const { name, total, hash } = ctx.request.body;
const chunksPath = path.join(chunkUploadPath, hash, '/');
const filePath = path.join(uploadPath, name);
// 讀取所有的chunks
const chunks = fs.readdirSync(chunksPath);
// 創(chuàng)建存儲(chǔ)文件
fs.writeFileSync(filePath, '');
if(chunks.length !== total || chunks.length === 0) {
ctx.status = 200;
ctx.res.end('切片文件數(shù)量不符合');
return;
}
for (let i = 0; i < total; i++) {
// 追加寫(xiě)入到文件中
fs.appendFileSync(filePath, fs.readFileSync(chunksPath + hash + '-' +i));
// 刪除本次使用的chunk
fs.unlinkSync(chunksPath + hash + '-' +i);
}
fs.rmdirSync(chunksPath);
// 文件合并成功,可以把文件信息進(jìn)行入庫(kù)。
ctx.status = 200;
ctx.res.end('Success');
})
以上就是文件分片上傳的基本過(guò)程,過(guò)程中加入了上傳進(jìn)度條、暫停和開(kāi)始上傳操作,見(jiàn)詳細(xì)代碼
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
用js實(shí)現(xiàn)預(yù)覽待上傳的本地圖片
用js實(shí)現(xiàn)預(yù)覽待上傳的本地圖片...2007-03-03
紅黑樹(shù)的插入詳解及Javascript實(shí)現(xiàn)方法示例
這篇文章主要給大家介紹了關(guān)于紅黑樹(shù)的插入的相關(guān)資料,以及Javascript實(shí)現(xiàn)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起看看吧。2018-03-03
OpenLayers3實(shí)現(xiàn)鼠標(biāo)移動(dòng)顯示坐標(biāo)
這篇文章主要為大家詳細(xì)介紹了OpenLayers3實(shí)現(xiàn)鼠標(biāo)移動(dòng)顯示坐標(biāo),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
靜態(tài)的動(dòng)態(tài)續(xù)篇之來(lái)點(diǎn)XML
靜態(tài)的動(dòng)態(tài)續(xù)篇之來(lái)點(diǎn)XML...2006-08-08
基于Bootstrap模態(tài)對(duì)話框只加載一次 remote 數(shù)據(jù)的解決方法
下面小編就為大家?guī)?lái)一篇基于Bootstrap模態(tài)對(duì)話框只加載一次 remote 數(shù)據(jù)的解決方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
外部web端訪問(wèn)微信小程序云數(shù)據(jù)庫(kù)的三種方法總結(jié)
最近在研究微信小程序的云開(kāi)發(fā)功能,下面這篇文章主要給大家介紹了關(guān)于外部web端訪問(wèn)微信小程序云數(shù)據(jù)庫(kù)的三種方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04
JavaScript 閉包在封裝函數(shù)時(shí)的簡(jiǎn)單分析
近才開(kāi)始系統(tǒng)的研究js,對(duì)js的興趣源于對(duì)JQuery的應(yīng)用。之前只會(huì)用js做簡(jiǎn)單的計(jì)算函數(shù),后來(lái)由于需要做特效,故接觸JQ,看著API,基本的特效都能完成,但相反,如果用js去實(shí)現(xiàn),估計(jì)自己很難寫(xiě)得出來(lái),所以下定決心系統(tǒng)的看看js。2009-11-11

