目錄
前言
基礎(chǔ)下載功能
進(jìn)階下載功能
單片下載
多片下載?
瀏覽器發(fā)送預(yù)檢(preflight)請求
express 不支持多段 range
multipart/** 搭配 boundary=**
分片下載功能
“只讀的” ArrayBuffer 對象
DataView 子類?Uint8Array?操作二進(jìn)制數(shù)據(jù)
Blob + createObjectURL 創(chuàng)建 url
全部代碼
通用的文件分片下載工具
拿到文件的大小 接口
分片函數(shù)
mergeArrayBuffer 函數(shù)
全部代碼
總結(jié)
前言
技術(shù)都寫在文檔里了。有用的文章,值得看第二遍
基礎(chǔ)下載功能
下載文件是一個(gè)常見的需求,只要服務(wù)端設(shè)置 Content-Disposition 為 attachment 就可以。
比如這樣:
const express = require('express');
const app = express();
app.get('/aaa',(req, res, next) => {
res.setHeader('Content-Disposition','attachment; filename="guang.txt"')
res.end('guangguang');
})
app.listen(3000, () => {
console.log(`server is running at port 3000`)
})
設(shè)置 Cotent-Disposition 為 attachment,指定 filename。
然后 html 里加一個(gè) a 標(biāo)簽:
<!DOCTYPE html>
<html lang="en">
<body>
<a href="http://localhost:3000/aaa">download</a>
</body>
</html>
跑起靜態(tài)服務(wù)器:
點(diǎn)擊鏈接就可以下載:
????????如果文件比較大,比如 500M,當(dāng)你下載了 499M 的時(shí)候突然斷網(wǎng)了,這時(shí)候下載就失敗了,你就要從頭再重新下載一次。體驗(yàn)就很不爽。
進(jìn)階下載功能
單片下載
????????能不能基于上次下載的地方接著下載,也就是斷點(diǎn)續(xù)傳呢?可以的,HTTP 里有這部分協(xié)議,就是 range 相關(guān)的 header。看下 MDN 對 range 的解釋:
Range
The Range 是一個(gè)請求首部,告知服務(wù)器返回文件的哪一部分。在一個(gè) Range 首部中,可以一次性請求多個(gè)部分,服務(wù)器會(huì)以 multipart 文件的形式將其返回。如果服務(wù)器返回的是范圍響應(yīng),需要使用 206 Partial Content 狀態(tài)碼。假如所請求的范圍不合法,那么服務(wù)器會(huì)返回 416 Range Not Satisfiable 狀態(tài)碼,表示客戶端錯(cuò)誤。服務(wù)器允許忽略 Range 首部,從而返回整個(gè)文件,狀態(tài)碼用 200 。
就是說你可以通過 Range 的 header 告訴服務(wù)端下載哪一部分內(nèi)容。
比如這樣:
Range: bytes=200-1000
????????就是下載 200-1000 字節(jié)的內(nèi)容(兩邊都是閉區(qū)間),服務(wù)端返回 206 的狀態(tài)碼,并帶上這部分內(nèi)容。
可以省略右邊部分,代表一直到結(jié)束:
Range: bytes=200-
也可以省略左邊部分,代表從頭開始:
Range: bytes=-1000
而且可以請求多段 range,服務(wù)端會(huì)返回多段內(nèi)容:
Range: bytes=200-1000, 2000-6576, 19000-
知道了 Range header 的格式,我們來試一下吧!
添加這樣一個(gè)路由:
app.get('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.download('index.txt', {
acceptRanges: true
})
})
設(shè)置允許跨域請求。res.download 是讀取文件內(nèi)容返回,acceptRanges 選項(xiàng)為 true 就是會(huì)處理 range 請求(其實(shí)默認(rèn)就是 true)。文件 index.txt 的內(nèi)容是這樣的:
然后在 html 里訪問一下這個(gè)接口:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<script>
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=0-4',
}
}).then((res) => {
console.log(res.data);
}).catch((err) => {
console.log(err);
})
</script>
</body>
</html>
訪問頁面,可以看到返回的是 206 的狀態(tài)碼!
????????這時(shí)候 Content-Length 就代表返回的內(nèi)容的長度。還有個(gè) Content-Range 代表當(dāng)前 range 的長度以及總長度。
當(dāng)然,你也可以訪問 5 以后的內(nèi)容
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<script>
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=5-',
}
}).then((res) => {
console.log(res.data);
}).catch((err) => {
console.log(err);
})
</script>
</body>
</html>
返回的是這樣的:
????????這倆連接起來不就是整個(gè)文件的內(nèi)容么?這樣就實(shí)現(xiàn)了斷點(diǎn)續(xù)傳!我們再來試試如果超出 range 會(huì)怎么樣:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<script>
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=500-600',
}
}).then((res) => {
console.log(res.data);
}).catch((err) => {
console.log(err);
})
</script>
</body>
</html>
請求 500-600 字節(jié)的內(nèi)容,這時(shí)候響應(yīng)是這樣的:
多片下載?
????????返回的是 416 狀態(tài)碼,代表 range 不合法。Range 不是還可以設(shè)置多段么?多段內(nèi)容是怎么返回的呢?我們來試一下:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<script>
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=0-2,4-5,7-',
}
}).then((res) => {
console.log(res.data);
}).catch((err) => {
console.log(err);
})
</script>
</body>
</html>
????????我分了 0-2, 4-5, 7- 這三段 range。重新訪問一下,這時(shí)候報(bào)了一個(gè)跨域的錯(cuò)誤,說是發(fā)送預(yù)檢請求失敗。
瀏覽器發(fā)送預(yù)檢(preflight)請求
瀏覽器會(huì)在三種情況下發(fā)送預(yù)檢(preflight)請求:
-
·用到了非 GET、POST 的請求方法,比如 PUT、DELETE 等,會(huì)發(fā)預(yù)檢請求看看服務(wù)端是否支持
-
·用到了一些非常規(guī)請求頭,比如用到了 Content-Type,會(huì)發(fā)預(yù)檢請求看看服務(wù)端是否支持
-
·用到了自定義 header,會(huì)發(fā)預(yù)檢請求
????????為啥 Range 頭單個(gè) range 不會(huì)觸發(fā)預(yù)檢請求,而多個(gè) range 就觸發(fā)了呢?因?yàn)槎鄠€(gè) range 的時(shí)候返回的 Content-Type 是不一樣的,是 multipart/byteranges 類型,比較特殊。預(yù)檢請求是 options 請求,那我們就支持一下:
app.options('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Range')
res.end('');
});
然后重新訪問:這時(shí)候你會(huì)發(fā)現(xiàn)雖然是 206 狀態(tài)碼,但返回的是整個(gè)內(nèi)容!
express 不支持多段 range
????????這是因?yàn)?express 只做了單 range 的支持,多段 range 可能它覺得沒必要支持吧。畢竟你發(fā)多個(gè)單 range 請求就能達(dá)到一樣的效果。MDN 官網(wǎng)的圖片是支持多 range 請求的,我們用那個(gè)看看:
multipart/** 搭配 boundary=**
????????請求 3 個(gè) range 的內(nèi)容??梢钥吹椒祷氐?Content-Type 確實(shí)是 multipart/byteranges,然后指定了 boundary 邊界線:
響應(yīng)內(nèi)容是這樣的:
????????以這些 boundary 分界線隔開的每一段內(nèi)容都包含 Content-Type、Content-Range 基于具體的那段 range 的內(nèi)容。這就是 multipart/byteranges 的格式,和我們常用的 multipart/form-data 很類似,只不過具體每段包含的 header 不同:
????????話說回來,其實(shí) express 只支持單段 range 問題也不大,不就是多發(fā)幾個(gè)請求就能達(dá)到一樣的效果。
分片下載功能
????????下面我們就用 range 來實(shí)現(xiàn)下文件的分片下載,最終合并成一個(gè)文件的功能。我們來下載一個(gè)圖片吧,分成兩塊下載,然后下載完合并起來。就用這個(gè)圖片好了:
app.get('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.download('guangguang.png', {
acceptRanges: true
})
})
????????我們寫下分片下載的代碼,就分兩段:這個(gè)圖片是 626k,也就是 626000 字節(jié),那我們就分成 0-300000 和 300001- 兩段:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<script>
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=0-300000',
}
}).then((res) => {
}).catch((err) => {
console.log(err);
})
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=300001-',
}
}).then((res) => {
}).catch((err) => {
console.log(err);
})
</script>
</body>
</html>
試一下,兩個(gè)響應(yīng)分別是這樣的:
第一個(gè)響應(yīng)還能看到圖片的預(yù)覽,確實(shí)只有一部分:
“只讀的” ArrayBuffer 對象
然后我們要把兩段給拼起來,怎么拼呢?操作二進(jìn)制數(shù)據(jù)要用 JS 的 ArrayBuffer api 了:
ArrayBuffer
ArrayBuffer 對象用來表示通用的、固定長度的原始二進(jìn)制數(shù)據(jù)緩沖區(qū)。
它是一個(gè)字節(jié)數(shù)組,通常在其他語言中稱為 “byte array”。你不能直接操作 ArrayBuffer 中的內(nèi)容;而是要通過類型化數(shù)組對象或 DataView 對象來操作,它們會(huì)將緩沖區(qū)中的數(shù)據(jù)表示為特定的格式,并通過這些格式來讀寫緩沖區(qū)的內(nèi)容。
????????瀏覽器還有個(gè)特有的 Blob api 也是用于操作二進(jìn)制數(shù)據(jù)的。我們指定下響應(yīng)的類型為 arraybuffer:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<script>
const p1 = new Promise((resolve, reject) => {
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=0-300000',
},
responseType: 'arraybuffer'
}).then((res) => {
resolve(res.data)
}).catch((err) => {
reject(err)
})
})
const p2 = new Promise((resolve, reject) => {
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=300001-',
},
responseType: 'arraybuffer'
}).then((res) => {
resolve(res.data)
}).catch((err) => {
reject(err)
})
})
Promise.all([p1, p2]).then(res => {
const [buffer1, buffer2] = res;
console.log(buffer1, buffeer2)
})
</script>
</body>
</html>
DataView 子類?Uint8Array?操作二進(jìn)制數(shù)據(jù)
????????兩個(gè) ArrayBuffer 怎么合并呢?ArrayBuffer 本身只是存儲(chǔ)二進(jìn)制數(shù)據(jù)的,要操作二進(jìn)制數(shù)據(jù)要使用具體的 DataView 的子類。比如我們想以字節(jié)的方式操作,那就是 Uint8Array 的方式(Uint 是 unsigned integer,無符號整數(shù)):
Promise.all([p1, p2]).then(res => {
const [buffer1, buffer2] = res;
const arr = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
const arr1 = new Uint8Array(buffer1);
arr.set(arr1, 0);
const arr2 = new Uint8Array(buffer2);
arr.set(arr2, arr1.byteLength);
console.log(arr.buffer)
})
????????每個(gè) arraybuffer 都創(chuàng)建一個(gè)對應(yīng)的 Uint8Array 對象,然后創(chuàng)建一個(gè)長度為兩者之和的 Uint8Array 對象,把兩個(gè) Uint8Array 設(shè)置到不同位置。最后輸出合并的 Uint8Array 對象的 arraybuffer。這樣就完成了合并:
合并之后就是整個(gè)圖片了。那自然可以作為圖片展示,也可以下載。
我添加一個(gè) img 標(biāo)簽:
<img id="img"/>
Blob + createObjectURL 創(chuàng)建 url
然后把 ArrayBuffer 轉(zhuǎn)成 Blob 設(shè)置以對象形式設(shè)置為 img 的 url
const blob = new Blob([arr.buffer]);
const url = URL.createObjectURL(blob);
img.src =url;
現(xiàn)在就可以看到完整的圖片了:
現(xiàn)在我們就實(shí)現(xiàn)了文件的分片下載再合并!甚至,你還可以再做一步下載:
const link = document.createElement('a');
link.href = url;
link.download = 'image.png';
document.body.appendChild(link);
link.click();
link.addEventListener('click', () => {
link.remove();
});
全部代碼
現(xiàn)在的全部代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<img id="img"/>
<script>
const p1 = new Promise((resolve, reject) => {
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=0-300000',
},
responseType: 'arraybuffer'
}).then((res) => {
resolve(res.data)
}).catch((err) => {
reject(err)
})
})
const p2 = new Promise((resolve, reject) => {
axios.get('http://localhost:3000', {
headers: {
Range: 'bytes=300001-',
},
responseType: 'arraybuffer'
}).then((res) => {
resolve(res.data)
}).catch((err) => {
reject(err)
})
})
Promise.all([p1, p2]).then(res => {
const [buffer1, buffer2] = res;
const arr = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
const arr1 = new Uint8Array(buffer1);
arr.set(arr1, 0);
const arr2 = new Uint8Array(buffer2);
arr.set(arr2, arr1.byteLength);
const blob = new Blob([arr.buffer]);
const url = URL.createObjectURL(blob);
img.src = url;
const link = document.createElement('a');
link.href = url;
link.download = 'image.jpg';
document.body.appendChild(link);
link.click();
link.addEventListener('click', () => {
link.remove();
});
})
</script>
</body>
</html>
通用的文件分片下載工具
拿到文件的大小 接口
????????當(dāng)然,一般不會(huì)這么寫死來用,我們可以封裝一個(gè)通用的文件分片下載工具。但分片之前需要拿到文件的大小,所以要增加一個(gè)接口:
app.get('/length',(req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.end('' + fs.statSync('./guangguang.png').size);
})
請求這個(gè)接口,返回文件大?。?/p>
分片函數(shù)
然后我們來做分片:
async function concurrencyDownload(path, size, chunkSize) {
let chunkNum = Math.ceil(size / chunkSize);
const downloadTask = [];
for(let i = 1; i <= chunkNum; i++) {
const rangeStart = chunkSize * (i - 1);
const rangeEnd = chunkSize * i - 1;
downloadTask.push(axios.get(path, {
headers: {
Range: `bytes=${rangeStart}-${rangeEnd}`,
},
responseType: 'arraybuffer'
}))
}
const arrayBuffers = await Promise.all(downloadTask.map(task => {
return task.then(res => res.data)
}))
return mergeArrayBuffer(arrayBuffers);
}
這部分代碼不難理解:
? ? ? ? ·首先根據(jù) chunk 大小來計(jì)算一共幾個(gè) chunk,通過 Math.ceil 向上取整。
? ? ? ? ·然后計(jì)算每個(gè) chunk 的 range,構(gòu)造下載任務(wù)的 promise。
? ? ? ? ·Promise.all 等待所有下載完成,之后合并 arraybuffer。
mergeArrayBuffer 函數(shù)
這里 arraybuffer 合并也封裝了一個(gè) mergeArrayBuffer 的方法:
function mergeArrayBuffer(arrays) {
let totalLen = 0;
for (let arr of arrays) {
totalLen += arr.byteLength;
}
let res = new Uint8Array(totalLen)
let offset = 0
for (let arr of arrays) {
let uint8Arr = new Uint8Array(arr)
res.set(uint8Arr, offset)
offset += arr.byteLength
}
return res.buffer
}
????????就是計(jì)算總長度,創(chuàng)建一個(gè)大的 Uint8Array,然后把每個(gè) arraybuffer 轉(zhuǎn)成 Uint8Array 設(shè)置到對應(yīng)的位置,之后再轉(zhuǎn)為 arraybuffer 就好了。
我們來測試下:
(async function() {
const { data: len } = await axios.get('http://localhost:3000/length');
const res = await concurrencyDownload('http://localhost:3000', len, 300000);
console.log(res)
const blob = new Blob([res]);
const url = URL.createObjectURL(blob);
img.src =url;
})();
調(diào)用分片下載的方法,每 300000 字節(jié)分一片,應(yīng)該是可以分 3 片,我們看下結(jié)果:
確實(shí),3 個(gè) range 都對了,最后合并的結(jié)果也是對的:
至此,我們就實(shí)現(xiàn)了通用的分片下載功能!
全部代碼
全部前端代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.unpkg.com/axios@1.3.5/dist/axios.min.js"></script>
</head>
<body>
<img id="img"/>
<script>
async function concurrencyDownload(path, size, chunkSize) {
let chunkNum = Math.ceil(size / chunkSize);
const downloadTask = [];
for(let i = 1; i <= chunkNum; i++) {
const rangeStart = chunkSize * (i - 1);
const rangeEnd = chunkSize * i - 1;
downloadTask.push(axios.get(path, {
headers: {
Range: `bytes=${rangeStart}-${rangeEnd}`,
},
responseType: 'arraybuffer'
}))
}
const arrayBuffers = await Promise.all(downloadTask.map(task => {
return task.then(res => res.data)
}))
return mergeArrayBuffer(arrayBuffers);
}
function mergeArrayBuffer(arrays) {
let totalLen = 0;
for (let arr of arrays) {
totalLen += arr.byteLength;
}
let res = new Uint8Array(totalLen)
let offset = 0
for (let arr of arrays) {
let uint8Arr = new Uint8Array(arr)
res.set(uint8Arr, offset)
offset += arr.byteLength
}
return res.buffer
}
(async function() {
const { data: len } = await axios.get('http://localhost:3000/length');
const res = await concurrencyDownload('http://localhost:3000', len, 300000);
console.log(res)
const blob = new Blob([res]);
const url = URL.createObjectURL(blob);
img.src =url;
})();
</script>
</body>
</html>
全部后端代碼如下:
const express = require('express');
const fs = require('fs');
const app = express();
app.get('/length',(req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.end('' + fs.statSync('./guangguang.png').size);
})
app.options('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Range')
res.end('');
});
app.get('/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.download('guangguang.png', {
acceptRanges: true
})
})
app.listen(3000, () => {
console.log(`server is running at port 3000`)
})
總結(jié)
·文件下載是常見需求,只要設(shè)置 Content-Disposition 為 attachment 就可以。
·但大文件的時(shí)候,下載中斷了再重新傳體驗(yàn)不好,或者想實(shí)現(xiàn)分片下載再合并的功能,這時(shí)候就可以用 Range 了。
·在請求上帶上 Range 的范圍,如果服務(wù)器不支持,就會(huì)返回 200 加全部內(nèi)容。
·如果服務(wù)器支持 Range,會(huì)返回 206 的狀態(tài)碼和 Content-Range 的 header,表示這段內(nèi)容的范圍和全部內(nèi)容的總長度。
·如果 Range 超出了,會(huì)返回 416 的 狀態(tài)碼。
·多段 Range 的時(shí)候,會(huì)返回 multipart/byteranges 的格式,類似 multipart/form-data 一樣,都是通過 boundary 分割的,每一段都包含 Content-Range 和內(nèi)容。
·express 不支持多段 range,但我們可以通過發(fā)多個(gè)請求達(dá)到一樣的效果。
·我們基于 Range 實(shí)現(xiàn)了文件的分片下載,瀏覽器通過 ArrayBuffer 接收。
·ArrayBuffer 只讀,想要操作要通過 Uint8Array 來合并,之后再轉(zhuǎn)為 ArrayBuffer。
·這樣就可以通過 URL.createObjectURL 設(shè)置為 img 的 src 或者通過 a 標(biāo)簽的 download 屬性實(shí)現(xiàn)下載了。文章來源:http://www.zghlxwxcb.cn/news/detail-498531.html
·其實(shí)分片下載用的還是挺多的,比如看視頻的時(shí)候,是不是也是一段一段下載的呢?文章來源地址http://www.zghlxwxcb.cn/news/detail-498531.html
到了這里,關(guān)于基于 HTTP Range 實(shí)現(xiàn)文件分片并發(fā)下載!的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!