
先來個(gè)總結(jié),我之前和將要寫到的文件上傳用到的技術(shù)和場景描述:
| 場景描述 | 使用技術(shù) |
|---|---|
| 圖片上傳前的預(yù)覽 | FileReader 或 createObjectURL |
| 限制用戶上傳的文件格式和大小 | 通過文件對象 File 的 size 和 type 屬性 |
| 強(qiáng)大的原生 Form 表單上傳 |
FileList 對象 |
| 虛擬表單上傳 | FormData |
| ctrl + v 上傳 | paste 事件 |
| 鼠標(biāo)拖拽上傳 | dropover 和 drop 事件, DataTransfer 對象 |
| 大文件 分片上傳 | Blob 的 slice 方法 |
| 大文件 分片下載 | HTTP 的 Range 技術(shù) |
| 體驗(yàn)更好的 斷點(diǎn)續(xù)傳/下載 | 暫存技術(shù) |
| 秒傳 | MD5 等摘要算法加密 |
有幾項(xiàng)涉及到的技術(shù),在之前的博客有提到過,就是下面這兩篇文章,鏈接如下:
本次重點(diǎn)來寫下「分片」和「斷點(diǎn)」這兩個(gè)技術(shù)。
寫完發(fā)現(xiàn)一篇好文章:NodeJS實(shí)現(xiàn)簡單的HTTP文件斷點(diǎn)續(xù)傳下載功能
一、實(shí)現(xiàn)分片上傳和斷點(diǎn)續(xù)傳
分片上傳又叫切片上傳
我們知道使用 <input type="file" name="file" /> 元素選擇一個(gè)文件之后,會(huì)得到 File 對象,而 File 對象 又天生繼承 Blob,正好 Blob 對象有個(gè)方法叫 slice。這個(gè)方法和數(shù)組的 slice 方法使用基本相同,它可以獲取待上傳文件的某一部分,經(jīng)過 slice 方法處理之后得到的結(jié)果也是一個(gè) Blob。
我們先來個(gè)文件上傳案例:
前端代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片上傳</title>
<style>
html, body {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>
</head>
<body>
<script src="./axios.min.js"></script>
<input type="file" name="file" id="ipt" />
<button onclick="upload()">上傳</button>
<script>
function upload() {
const file = ipt.files[0];
const vform = new FormData();
vform.append("vform", file, file.name);
axios.post("/upload", vform).then(res => {
console.log(vform, res);
});
}
</script>
</body>
</html>
后端代碼:
const multiparty = require("multiparty");
const bodyParser = require("body-parser");
const path = require("path");
const express = require("express");
const app = express();
const fs = require("fs");
function resolvePath (dir) {
return path.join(__dirname, dir);
}
app.use(express.static(resolvePath("/public")));
// https://expressjs.com/en/4x/api.html#req.body
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ extended: true }));
app.post("/upload", function (req, res) {
const form = new multiparty.Form({ uploadDir: "public" });
form.parse(req);
form.on("file", function(name, file) {
console.log(name, file)
const { path, originalFilename } = file;
fs.renameSync(path, `public/${originalFilename}`);
res.json({
url: `http://localhost:48488/${originalFilename}`,
message: "發(fā)送成功"
});
})
});
const port = 48488;
app.listen(port, function () {
console.log(`listen port ${port}`);
});
一個(gè)簡單的文件上傳就完成了,現(xiàn)在開始切片上傳功能開發(fā),切片上傳就是把一個(gè)文件切分成很多小文件,本來上傳一個(gè)大文件,現(xiàn)在改成上傳很多小文件。
如何切文件,這就很哲學(xué)了,流行兩種思路:
- 不管上傳文件的大小,切成固定的塊數(shù),然后上傳。
- 不管上傳文件的大小,每次切的塊大小相同,然后上傳。
第一種方法的缺點(diǎn)就是,如果文件過小的話,切成固定的塊數(shù),明顯浪費(fèi) HTPP 請求,如果文件過大,切成固定的塊數(shù),切的每塊可能依然過大,即切片的切片還需要繼續(xù)切片。所以使用這種方法,需要加上限定條件,假如上傳文件的大小為 s,限定條件應(yīng)該這樣寫 n <= s <= m。
第二種方法的缺點(diǎn)就是,如何確定每次上傳文件的大小,定小了,容易出現(xiàn) HTTP 請求過多,定大了,容易出現(xiàn)切片效果不理想,切片的大小真是讓人頭疼。
所以,我們常常需要這兩種辦法結(jié)合使用,上傳文件的代碼邏輯應(yīng)該這樣寫:
- 文件過小,不用切片,可以直接上傳。例如 10kb、190kb、200kb、甚至 1M……。
- 文件三四十兆的這種,就固定切片大小就好。
- 大于一百兆但是小于 1G 的這種,可以分區(qū)間,不同的區(qū)間給不同固定的分包數(shù)量。
- 如果文件再大,可以兩者方法結(jié)合用,先固定分包數(shù)量,然后隨機(jī)包大小。
- 文件超大的那種,應(yīng)該尋求并行上傳方法,簡單點(diǎn)前端可以直接禁止上傳超大文件。
- 重復(fù)文件上傳應(yīng)該有秒傳功能。
以上是一個(gè)非常完善的切片上傳邏輯,項(xiàng)目沒有要求我當(dāng)然不會(huì)實(shí)現(xiàn)的了,畢竟要寫好多的判斷,不過切片上傳的核心功能,我還是得通過代碼來實(shí)現(xiàn)的,一起來看看。
前端代碼:遞歸實(shí)現(xiàn)切片上傳
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片上傳</title>
<style>
html, body {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>
</head>
<body>
<script src="./axios.min.js"></script>
<script src="./spark-md5.min.js"></script>
<input type="file" name="file" id="ipt" />
<button onclick="upload(0)">上傳</button>
<script>
const chunkSize = 1024 * 1024; // 默認(rèn)分片大小為1兆。1kb = 1024byte, 1m = 1024 kb
let fileFingerprint = undefined;
const file = ipt.files[0];
function upload(index) {
const { name, type, size } = file;
// 生成文件指紋
const spark = fileFingerprint || new SparkMD5.ArrayBuffer();
spark.append(file);
const hexHash = spark.end();
const extName = name.substring(name.lastIndexOf("."));
const startIndex = chunkSize * index;
// 文件上傳完,終止遞歸同時(shí)合并文件
if ( startIndex > size ) {
axios.post("/merge", {
fileName: name,
hexHash,
extName
}).then(res => {
fileFingerprint = undefined;
console.log(res);
});
return;
};
const endIndex = startIndex + chunkSize > size ? size : startIndex + chunkSize;
const blobPart = file.slice(startIndex, endIndex, type);
// FormData 直接上傳切片后的文件,文件名默認(rèn)為 blob( filename="blob")
// 這里通過 File 給個(gè)文件名
const blobFile = new File([blobPart], `${hexHash}-${index}${extName}`, { type });
// 創(chuàng)建虛擬表單進(jìn)行文件上傳
const vform = new FormData();
vform.append("vform", blobFile);
axios.post("/upload", vform).then(res => {
// 分片 => 通過遞歸實(shí)現(xiàn)
upload(++index);
});
}
</script>
</body>
</html>
后端代碼:兩個(gè)重要的路由
app.post("/upload", function (req, res) {
const form = new multiparty.Form({ uploadDir: "temp" });
form.parse(req);
form.on("file", function(name, file) {
const { path, originalFilename } = file;
fs.renameSync(path, `temp/${originalFilename}`);
res.json({
code: "200",
message: "發(fā)送成功"
});
})
});
app.post("/merge", function (req, res) {
const { fileName, hexHash, extName } = req.body;
const readDir = fs.readdirSync(resolvePath("./temp"));
readDir.sort((a, b) => a - b).map(chunkPath => {
fs.appendFileSync(
resolvePath(`public/${fileName}`),
fs.readFileSync(resolvePath(`temp/${chunkPath}`))
);
fs.rmSync(resolvePath(`temp/${chunkPath}`));
});
// fs.rmdirSync(resolvePath("./temp"));
res.json({
url: `http://localhost:48488/${fileName}`,
message: "發(fā)送成功"
});
});
一個(gè)牛叉而又簡單的切片上傳就完成了。

你看我們 network 面板里面的 waterfall 你會(huì)發(fā)現(xiàn),接口是串行發(fā)出的(當(dāng)然根據(jù)前端代碼你也能得出來這個(gè)結(jié)論),聰明的你這時(shí)候肯定想到了,這樣是不是有點(diǎn)浪費(fèi) HTTP 請求,而且速度并沒有達(dá)到最快,既然串行的方法不太好,我們就并行上傳。
并行實(shí)現(xiàn)邏輯:略。
這時(shí)候應(yīng)該考慮一個(gè)用戶體驗(yàn)的問題,一如果文件是在太大,文件還沒上傳完,用戶需要暫時(shí)離開,關(guān)上電子設(shè)備。二網(wǎng)絡(luò)過差,甚至差到斷網(wǎng)。這時(shí)候我們應(yīng)該提供 暫停/繼續(xù) 上傳功能。這個(gè)功能后端不需要?jiǎng)哟a邏輯,只需要前端記住切片上傳的位置就行了,這個(gè)很簡單,簡單的加個(gè)變量來控制下就行了,如果想體驗(yàn)更好點(diǎn),甚至要考慮加上取消請求的功能。
這個(gè)暫停/繼續(xù)上傳功能也有局限,就是用戶刷新了頁面,重新再打開,受瀏覽器的限制,我們不能用例如 NodeJS 中 fs 模塊來主動(dòng)獲取文件,只能用戶手動(dòng)上傳,前端來能獲取到文件 File,所以暫停/繼續(xù)上傳功能受頁面不能刷新影響很大。那問題來了,請問怎么解決?
自然而然的,我們想到把文件對象直接存儲(chǔ)在本地不就行了,好主意,那存在哪里呢?存在 localStorage 怎么樣?好像不太行,localStorage 大小就能存約 5M 大小,在如今的網(wǎng)絡(luò)時(shí)代下,這怎么能夠用。那就沒辦法了嗎?非也,還有一個(gè)終極大殺招,那就 IndexDB,我們通過 MD5 來確認(rèn)文件的唯一性,然后把沒有上傳的部分放入 IndexDB 里面,一旦上傳完,就立刻刪除,最大可能的節(jié)省空間。哈哈哈,這下算是徹底的解決問題了。
但是此時(shí)又有一個(gè)問題,就是我在 PC 我上傳文件,但是只是上傳一半就關(guān)閉了網(wǎng)頁,此時(shí)我換設(shè)備了,跑到 iPad 或 手機(jī)再次打開上傳文件頁面。我也想要看到未上傳的文件。這下麻煩大了,但是也有解決辦法。
首先上傳進(jìn)度,肯定是后端記住,而不是前端了。其次對于設(shè)備上沒有此文件的上傳我們只需要簡單的提醒用戶,要么使用原設(shè)備,要么使用此設(shè)備手動(dòng)重新上傳。
如果用戶選擇了重新上傳,后端需要根據(jù)此文件的 MD5 檢索出來文件已經(jīng)上傳的部分,前端續(xù)傳,而不是真正的重新開始。
斷點(diǎn)續(xù)傳和分片上傳,到此完結(jié)撒花??。
二、秒傳
上面提到了,續(xù)傳功能,那就沒有理由不支持秒傳功能,這個(gè)更加簡單了,就是根據(jù)上傳文件的 MD5,在數(shù)據(jù)庫中檢索已經(jīng)上傳文件的 MD5,一旦檢索到直接上傳完成。
三、分片下載和斷點(diǎn)續(xù)載
分片下載又叫切片下載
分片下載和分片上傳的原理那是大大的不同,不過思路都是一致的,就是大化小。與分片上傳利用 File 對象 不同,分片下載用到的技術(shù)是 HTTP 中的知識(shí)。
你先猜猜用到的是 HTTP 中的什么知識(shí)?
猜不出來吧,那你的去補(bǔ)補(bǔ) HTTP 的知識(shí)了,推薦「圖解 HTTP」這本書。答案揭曉其實(shí)用到是 Range Requests 的知識(shí),如果你想更加系統(tǒng)的學(xué)習(xí),請參考 RFC 7233。
這個(gè)技術(shù)可能我們前端不經(jīng)常用到,但是平時(shí)我們接觸還是非常多的。例如像迅雷這樣的多線程下載器,我們平時(shí)看視頻,進(jìn)度條隨意拖拽只加載部分視頻流等等。這么一講你是不是,有種天靈蓋被揭開的柑橘,哦,原來這些功能都是 HTTP 請求的功勞。
好了,廢話不多說,直接上硬菜,先來學(xué)學(xué) HTTP 知識(shí)的內(nèi)容。
Accept-Ranges
我們知道 HTTP 最早是用來傳輸文本的,現(xiàn)在想把一個(gè)文件切開一部分一部分的傳輸,那就需要支持更加底層的傳輸單位,沒錯(cuò)就是 字節(jié)(byte)。根據(jù)規(guī)范,在使用 byte 傳輸?shù)臅r(shí)候,首先驗(yàn)證服務(wù)器是否支持這種傳輸方式。
我們需要在服務(wù)器上通過 Accept-Ranges 頭部表示是否支持 Range,Accept-Ranges 的格式為:
Accept-Ranges = acceptable-ranges
acceptable-ranges 的值有兩個(gè):
- Accept-Ranges: none 不支持 bytes 請求
- Accept-Ranges = bytes 支持 bytes 請求
來看下嗶哩嗶哩網(wǎng)站視頻播放時(shí)其中一個(gè)接口。

Range 請求范圍的單位
現(xiàn)在知道服務(wù)器支持 byte 請求了,那怎么表示請求范圍呢?該是 Range 登場的時(shí)候了。來看看 Range 的格式:
Range = byte-ranges-specifier / other-ranges-specifier
現(xiàn)在假設(shè)我們要獲取的文件大小為 2000 bytes,那么我們可以按下面步驟獲取。
- 第 1 個(gè) 500 字節(jié):
bytes=0-499 - 第 2 個(gè) 500 字節(jié)
bytes=500-999 - 第 3 個(gè) 500 字節(jié)
bytes=500-600,601-999也可以有重合部分bytes=500-700,601-999重合的只會(huì)加載一次。注意這種請求叫多重范圍請求,它請求頭的 Content-Type 比較特殊長成這樣Content-Type:multipart/byteranges; boundary=…,有點(diǎn)類似 POST 表單提交的方式。關(guān)于 byteranges 請去 MDN 查看教程。 - 最后 1 個(gè) 500 字節(jié):
bytes=-500或bytes=9500-
看完只會(huì),考你個(gè)問題,如果僅要第 1 個(gè)和最后 1 個(gè)字節(jié),怎么寫?
bytes=0-0,-1
OK,這次我們來看看微博上視頻播放時(shí) Range 是如何寫的。

看完,不知道你發(fā)現(xiàn)沒,微博和嗶哩嗶哩請求頭響應(yīng)頭的定義有點(diǎn)不同,微博第一個(gè)單詞都是大寫的,嗶哩嗶哩都是小寫的,本人更喜歡微博的做法,嚴(yán)格遵守了 RFC 規(guī)定。
Content-Range
服務(wù)器收到瀏覽器的請求了,那么服務(wù)器如何返回資源呢?沒錯(cuò)就是 Content-Range 了。給個(gè)例子來看看它的語法格式:
Content-Range: bytes start-end/total
上面表示一次 HTTP 請求,服務(wù)端返回的結(jié)果為 start-end 區(qū)間,這次請求的資源總大小為 total??磦€(gè)例子:

還沒完,看到我上面標(biāo)出來的狀態(tài)碼了吧,我們知道一個(gè)正常的 HTTP 請求完成之后我們會(huì)收到 200 狀態(tài)碼,但是我們用 Range 請求服務(wù)器的時(shí)候,有所不同,分為以下幾種情況:
- 服務(wù)器不支持 Range 請求時(shí),則以 200 返回完整的響應(yīng)包體
- 服務(wù)器支持 Range 請求的時(shí)候,一次正常請求結(jié)束返回 206 Partial Content
- 服務(wù)器支持 Range 請求的時(shí)候,當(dāng)請求范圍不滿足實(shí)際資源的大小,返回狀態(tài)碼 416 Range Not Satisfiable ,同時(shí) Content-Range 中的 complete- length 顯示完整響應(yīng)的長度,例如 :
Content-Range: bytes */1234
HTTP 的條件請求
平時(shí)涉及到 HTTP 條件請求的頭部有以下五個(gè)。
- If-Match = "*" / 1#entity-tag
- If-None-Match = "*" / 1#entity-tag
- If-Modified-Since = HTTP-date
- If-Unmodified-Since = HTTP-date
- If-Range = entity-tag / HTTP-date
If-None-Match 和 If-Modified-Since 這兩個(gè)請求頭相信你非常熟悉了,屬于協(xié)商緩存的內(nèi)容,相信這時(shí)候你馬上能想到一個(gè)非常經(jīng)典的前端面試題——說說瀏覽器的緩存機(jī)制。,不懂這個(gè)面試題的可以去看看這個(gè)博客 圖解 HTTP 緩存。
剩下幾個(gè)我相信你就不太懂了,我們一起來學(xué)習(xí)下,If-None-Match 和 If-Modified-Since 分別取反就是 If-Match 和 If-Unmodified-Since。取反的 If-Match 和 If-Unmodified-Since 就是給我們前面講的 Accept-Ranges 使用的,當(dāng)我們一點(diǎn)點(diǎn)的從服務(wù)器獲取數(shù)據(jù)的時(shí)候,突然此時(shí)已經(jīng)獲取的數(shù)據(jù)發(fā)生了變化,這時(shí)我們肯定不能接著獲取數(shù)據(jù)了,而是要從新獲取數(shù)據(jù)。這是我們現(xiàn)在遇到的問題,那怎么解決這個(gè)問題呢?
兩個(gè)方法:
-
If-Match和If-Unmodified-Since
通過請求頭攜帶的 If-Match 或 If-Unmodified-Since 來判斷文件是否被修改,如果文件被修改,直接返回 412 (Precondition Failed)來告訴瀏覽器,正在請求的資源發(fā)生了改變,請重新發(fā)送請求進(jìn)行獲取。
使用 NodeJS 來模擬下資源被更新返回 412 狀態(tài)碼這個(gè)過程,不過先插播一段知識(shí),關(guān)于 NodeJS 獲取請求頭的方面的,即 express req.headers 大小寫問題。
express 中通過 req.query 來獲取客戶端 Query 參數(shù),客戶端上傳的參數(shù)是嚴(yán)格遵守大小寫的,但是 req.headers 來獲取請求頭時(shí),接收到的全是小寫。搞的人很郁悶,為什么這么奇怪呢,原來是和 HTTP 協(xié)議有關(guān)。詳情見:express request.headers 大小寫問題,坑!,我是受不了大小寫混亂,所以使用
req.get()來獲取請求頭,因?yàn)檫@個(gè) API 是忽略大小寫的。
我們的 NodeJS 后端代碼如下:
app.get("/download", function (req, res) {
const Range = req.get("Range");
const clientMatch = req.get("If-Match");
const clientmodifiedSince = req.get("If-Unmodified-Since");
const readPath = resolvePath("./test.js");
const md5 = crypto.createHash("md5");
md5.update(fs.readFileSync(readPath));
const serverMatch = md5.digest("hex");
const { mtime } = fs.statSync(readPath);
const timeStamp = mtime.getTime();
const lastModifiedStringDate = new Date(clientmodifiedSince || 0).getTime();
if ((clientMatch && clientMatch !== serverMatch) || (clientmodifiedSince && lastModifiedStringDate !== timeStamp)) {
res.sendStatus(412);
} else {
const rangeBytes = Range.substring(Range.lastIndexOf("=") + 1);
const [ start, end ] = rangeBytes.split("-");
res.setHeader("ETag", serverMatch);
res.setHeader("Last-Modified", timeStamp);
fs.createReadStream(readPath, { start: +start, end: +end }).pipe(res);
};
});
我們要讀取的文件 test.js 的內(nèi)容為:
CondorHero
然后我們采用 CURL 命令工具進(jìn)行調(diào)試,先看看如何獲取一段數(shù)據(jù):
curl http://localhost:48488/download -H "Range: bytes=0-5" // Condor
再來獲取下請求頭,為 HTTP 條件請求做準(zhǔn)備:
curl http://localhost:48488/download -H "Range: bytes=0-5" -I
HTTP/1.1 200 OK
X-Powered-By: Express
ETag: 532462711215f93a3206e236e45f894e
LastModified: 1611933312687
Date: Sat, 30 Jan 2021 08:01:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5
然后利用條件請求 If-Match 或 If-Unmodified-Since 來做一個(gè)正常請求,這個(gè)我選擇了 If-Match。
curl http://localhost:48488/download -H "Range: bytes=0-5" -H "If-Match: 532462711215f93a3206e236e45f894e" // Condor
依然正常輸出。隨便更改下 If-Match 的值,再次發(fā)送請求:
curl http://localhost:48488/download -H "Range: bytes=0-5" -H "If-Match: 哈哈哈哈" // 402 Precondition Failed
模擬完成。
上面這個(gè)方法有個(gè)缺點(diǎn),那就是數(shù)據(jù)一旦被改變,瀏覽需要先獲取 412 狀態(tài)碼,然后瀏覽器再準(zhǔn)備發(fā)送請求,我們發(fā)現(xiàn)多了一個(gè)請求來回,如果服務(wù)器對比完,發(fā)現(xiàn)資源被改變,能直接完整返回最新資源就完美了,省去了一次 HTTP 請求。沒錯(cuò) If-Range 就是用來干這個(gè)的。
- If-Range
稍微修改下后端的代碼:
app.get("/download", function (req, res) {
const Range = req.get("Range");
const clientMatch = req.get("If-Range");
const clientmodifiedSince = req.get("If-Unmodified-Since");
const readPath = resolvePath("./test.js");
const md5 = crypto.createHash("md5");
md5.update(fs.readFileSync(readPath));
const serverMatch = md5.digest("hex");
const { mtime } = fs.statSync(readPath);
const timeStamp = mtime.getTime();
const lastModifiedStringDate = new Date(clientmodifiedSince || 0).getTime();
if ((clientMatch && clientMatch !== serverMatch) || (clientmodifiedSince && lastModifiedStringDate !== timeStamp)) {
res.setHeader("ETag", serverMatch);
res.setHeader("Last-Modified", timeStamp);
fs.createReadStream(readPath).pipe(res);
} else {
const rangeBytes = Range.substring(Range.lastIndexOf("=") + 1);
const [ start, end ] = rangeBytes.split("-");
res.setHeader("ETag", serverMatch);
res.setHeader("Last-Modified", timeStamp);
fs.createReadStream(readPath, { start: +start, end: +end }).pipe(res);
};
});
當(dāng)我們再次發(fā)送請求的時(shí)候:
curl http://localhost:48488/download -H "Range: bytes=0-5" -H "If-Range: 哈哈哈哈哈"
// 返回結(jié)果為 CondorHero
// 沒有返回 412 而是直接返回全部結(jié)果
條件請求我們就講完了,現(xiàn)在直接開始做分片下載的 Demo 好了。
分片下載
現(xiàn)在分片實(shí)現(xiàn)最重要的兩點(diǎn)就是:
- 前端 => 遞歸
- 后端 => createReadStream 的用法
然后我們后端現(xiàn)在幾乎都不要改動(dòng)什么,簡單的加點(diǎn)東西就行了,看下接口。
app.get("/download", function (req, res) {
const Range = req.get("Range");
const clientMatch = req.get("If-Range");
const clientmodifiedSince = req.get("If-Unmodified-Since");
const readPath = resolvePath("./test.txt");
const md5 = crypto.createHash("md5");
md5.update(fs.readFileSync(readPath));
const serverMatch = md5.digest("hex");
const { mtime, blksize } = fs.statSync(readPath);
const timeStamp = mtime.getTime();
const lastModifiedStringDate = new Date(clientmodifiedSince || 0).getTime();
if ((clientMatch && clientMatch !== serverMatch) || (clientmodifiedSince && lastModifiedStringDate !== timeStamp)) {
res.setHeader("ETag", serverMatch);
res.setHeader("Last-Modified", timeStamp);
fs.createReadStream(readPath).pipe(res);
} else {
const rangeBytes = Range.substring(Range.lastIndexOf("=") + 1);
const [ start, end ] = rangeBytes.split("-");
res.setHeader("Accetp-Ranges", "bytes");
res.setHeader("ETag", serverMatch);
res.setHeader("Last-Modified", timeStamp);
res.setHeader("Total-Size", blksize);
res.setHeader("Content-Range", `bytes ${start}-${end}/${blksize}`);
res.setHeader("fileName", encodeURIComponent("浣溪沙-晏殊.txt"));
fs.createReadStream(readPath, { start: +start, end: +end }).pipe(res);
};
});
我們要接收的文件它長成這樣:
浣溪沙·小閣重簾有燕過
晏殊〔宋代〕
小閣重簾有燕過。晚花紅片落庭莎。曲闌干影入涼波。
一霎好風(fēng)生翠幕,幾回疏雨滴圓荷。酒醒人散得愁多。
前端就很好寫了,遞歸分片:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片下載</title>
</head>
<body>
<script src="./axios.min.js"></script>
<button onclick="download(0, 1000)">下載</button>
<script>
let downloadText = "", totalSize = 0, fileName;
function download(startRang, endRang) {
// 下載
if ( totalSize && totalSize <= startRang) {
const url = window.URL.createObjectURL(new Blob([downloadText]));
const link = document.createElement("a");
link.style.display = "none";
link.href = url;
link.setAttribute("download", decodeURIComponent(fileName));
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
totalSize=0;
downloadText="";
fileName="fileName";
return;
}
// totalSize 4096
const ajaxConfig = {
headers: {
responseType: "arraybuffer",
Range: `bytes=${startRang}-${endRang}`
}
};
const downRes = axios.get("/download", ajaxConfig);
downRes.then(res => {
!totalSize && (totalSize = res.headers["total-size"]);
!fileName && (fileName = res.headers["filename"]);
const data = res.data
downloadText += data;
startRang = endRang;
if (totalSize - endRang > 1000) {
endRang += 1000;
} else {
endRang = totalSize;
};
// 分片 => 遞歸
download(startRang, endRang);
});
}
</script>
</body>
</html>
把網(wǎng)絡(luò)調(diào)慢點(diǎn),我們看下分片下載的演示效果:

這里送你一張分片請求鏈接的圖:

斷點(diǎn)下載思路,參考上面斷點(diǎn)上傳。
完~
四、最后
代碼我都是很簡單的略寫實(shí)現(xiàn),并沒有太過深入的精心實(shí)現(xiàn)。這是因?yàn)槲覀兦岸擞龅酱蠖鄶?shù)的項(xiàng)目,就是一個(gè)簡單的文件上傳,頂多文件過大加點(diǎn)分片上傳。像百度云網(wǎng)頁那種完美的實(shí)現(xiàn),應(yīng)該很少有公司有這種業(yè)務(wù)場景。所以,我們只要大概的了解個(gè)原理,妥妥的應(yīng)付面試就行了。
當(dāng)前時(shí)間 Saturday, January 30, 2021 19:29:40 北京辦公室