文件下載
,在我以前曾經發過一篇文件上傳的文章(
一文了解文件上傳全過程(1.8w字深度解析,進階必備
),反響還不錯,時隔多日,因爲最近有研究一些媒體相關的工做,所以打算對下載作一個整理,
所以他的兄弟篇誕生了,帶你領略文件下載的奧祕。本文會花費你較長的時間閱讀,建議先收藏/點贊,而後查看你感興趣的部分,平時也能夠充當當作字典的效果來查詢。
:) 不整不知道,一整,竟然整出這麼多狀況,我只是想簡單地作個頁面仔。html
前言
一圖覽全文,能夠先看看大綱適不適合本身,若是你喜歡則繼續往下閱讀。前端
這一節呢,主要介紹一些前置知識,對一些基礎知識的介紹,若是你以爲你是這個。⬇️⬇️⬇️,你能夠跳過前言。node
前端的文件下載主要是經過 <a>
,再加上 download
屬性,有了它們讓咱們的下載變得簡單。ios
download
此屬性指示瀏覽器下載 URL 而不是導航到它,所以將提示用戶將其保存爲本地文件。若是屬性有一個值,那麼此值將在下載保存過程當中做爲預填充的文件名(若是用戶須要,仍然能夠更改文件名)。此屬性對容許的值沒有限制,可是 /
和 \
會被轉換爲下劃線。大多數文件系統限制了文件名中的標點符號,故此,瀏覽器將相應地調整建議的文件名。( 摘自 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a)nginx
注意:git
此屬性僅適用於同源 URL。 儘管 HTTP URL 須要位於同一源中,可是可使用 blob:
URL 和data:
URL ,以方便用戶下載使用 JavaScript 生成的內容(例如使用在線繪圖 Web 應用程序建立的照片)。
所以下載 url 主要有三種方式。(本文大部分以 blob 的方式進行演示)github
兼容性web
能夠看到它的兼容性也很是的可觀(https://www.caniuse.com/#search=download)chrome
爲了不不少代碼的重複性,由於我抽離出了幾個公共函數。(該部分可跳過,名字都比較可讀,以後如果遇到不明白則能夠在這裏尋找)element-ui
export function downloadDirect(url) {
const aTag = document.createElement('a');
aTag.download = url.split('/').pop();
aTag.href = url;
aTag.click()
}
export function downloadByContent(content, filename, type) {
const aTag = document.createElement('a');
aTag.download = filename;
const blob = new Blob([content], { type });
const blobUrl = URL.createObjectURL(blob);
aTag.href = blobUrl;
aTag.click();
URL.revokeObjectURL(blob);
}
export function downloadByDataURL(content, filename, type) {
const aTag = document.createElement('a');
aTag.download = filename;
const dataUrl = `data:${type};base64,${window.btoa(unescape(encodeURIComponent(content)))}`;
aTag.href = dataUrl;
aTag.click();
}
export function downloadByBlob(blob, filename) {
const aTag = document.createElement('a');
aTag.download = filename;
const blobUrl = URL.createObjectURL(blob);
aTag.href = blobUrl;
aTag.click();
URL.revokeObjectURL(blob);
}
export function base64ToBlob(base64, type) {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const buffer = Uint8Array.from(byteNumbers);
const blob = new Blob([buffer], { type });
return blob;
}
🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅🚅
(手動給不看以上內容的大佬畫分割線)
🇨🇳
全部示例Github地址: https://github.com/hua1995116/node-demo/tree/master/file-download
在線Demo: https://qiufeng.blue/demo/file-download/index.html
後端
本文後端全部示例均以 koa / 原生 js 實現。
後端返回文件流
這種狀況很是簡單,咱們只須要直接將後端返回的文件流以新的窗口打開,便可直接下載了。
// 前端代碼
<button id="oBtnDownload">點擊下載</button>
<script>
oBtnDownload.onclick = function(){
window.open('http://localhost:8888/api/download?filename=1597375650384.jpg', '_blank')
}
</script>
// 後端代碼
router.get('/api/download', async (ctx) => {
const { filename } = ctx.query;
const fStats = fs.statSync(path.join(__dirname, './static/', filename));
ctx.set({
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename=${filename}`,
'Content-Length': fStats.size
});
ctx.body = fs.readFileSync(path.join(__dirname, './static/', filename));
})
可以讓瀏覽器自動下載文件,主要有兩種狀況:
一種爲使用了Content-Disposition
屬性。
咱們來看看該字段的描述。
在常規的HTTP應答中,
Content-Disposition
響應頭指示回覆的內容該以何種形式展現,是以內聯的形式(即網頁或者頁面的一部分),仍是以附件的形式下載並保存到本地 --- 來源 MDN(https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition)
再來看看它的語法
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"
很簡單,只要設置成最後一種形態我就能成功讓文件從後端進行下載了。
另外一種爲瀏覽器沒法識別的類型
例如輸入 http://localhost:8888/static/demo.sh,瀏覽器沒法識別該類型,就會自動下載。
不知道小夥伴們有沒有遇到過這樣的一個狀況,咱們輸入一個正確的靜態 js 地址,沒有配置Content-Disposition
,可是卻會被意外的下載。
例如像如下的狀況。
這極可能是因爲你的 nginx
少了這一行配置.
include mime.types;
致使默認走了 application/octet-stream
,瀏覽器沒法識別就下載了文件。
後端返回靜態站點地址
經過靜態站點下載,這裏要分爲兩種狀況,一種爲可能該服務自帶靜態目錄,即爲同源狀況,第二種狀況爲適用了第三方靜態存儲平臺,例如阿里雲、騰訊雲之類的進行託管,即非同源(固然也有些平臺直接會返回)。
同源
同源狀況下是很是簡單,先上代碼,直接調用一下函數就能輕鬆實現下載。
import {downloadDirect} from '../js/utils.js';
axios.get('http://localhost:8888/api/downloadUrl').then(res => {
if(res.data.code === 0) {
downloadDirect(res.data.data.url);
}
})
非同源
咱們也能夠從 MDN 上看到,雖然 download 限制了非同源的狀況,可是!!可是!!可是可使用 blob:
URL 和 data:
URL ,所以咱們只要將文件內容進行下載轉化成 blob
就能夠了。
整個過程以下
<button id="oBtnDownload">點擊下載</button>
<script type="module">
import {downloadByBlob} from '../js/utils.js';
function download(url) {
axios({
method: 'get',
url,
responseType: 'blob'
}).then(res => {
downloadByBlob(res.data, url.split('/').pop());
})
}
oBtnDownload.onclick = function(){
axios.get('http://localhost:8888/api/downloadUrl').then(res => {
if(res.data.code === 0) {
download(res.data.data.url);
}
})
}
</script>
如今非同源的也能夠愉快地下載啦。
後端返回字符串(base64)
有時候咱們也會遇到一些新手後端返回字符串的狀況,這種狀況不多見,可是來了咱們也不慌,順即可以向後端小哥秀一波操做,無論啥數據,咱都能給你下載下來。
ps: 前提是安全無污染的資源 :) , 正經文章的招牌閃閃發光。
這種狀況下,我須要模擬下後端小哥的騷操做,所以有後端代碼。
核心過程
// node 端
router.get('/api/base64', async (ctx) => {
const { filename } = ctx.query;
const content = fs.readFileSync(path.join(__dirname, './static/', filename));
const fStats = fs.statSync(path.join(__dirname, './static/', filename));
console.log(fStats);
ctx.body = {
code: 0,
data: {
base64: content.toString('base64'),
filename,
type: mime.getType(filename)
}
}
})
// 前端
<button id="oBtnDownload">點擊下載</button>
<script type="module">
import {base64ToBlob, downloadByBlob} from '../js/utils.js';
function download({ base64, filename, type }) {
const blob = base64ToBlob(blob, type);
downloadByBlob(blob, filename);
}
oBtnDownload.onclick = function(){
axios.get('http://localhost:8888/api/base64?filename=1597375650384.jpg').then(res => {
if(res.data.code === 0) {
download(res.data.data);
}
})
}
</script>
思路其實仍是利用了咱們上面說的 <a>
標籤。可是在這個步驟前,多了一個步驟就是,須要將咱們的 base64
字符串轉化爲二進制流,這個東西,在個人前一篇文件上傳中也經常提到,畢竟文件就是以二進制流的形式存在。不過也很簡單,js 擁有內置函數 atob
。極大地提升了咱們轉換的效率。
純前端
上面介紹藉助後端來完成文件下載的相關方法,接下來咱們來介紹介紹純前端來完成文件下載的一些方法。
方法一: blob:
URL
方法二: data:
URL
因爲 data:URL 會有長度的限制,所以下面的全部例子都會採用 blob 的方式來進行演示。
json/text
下載text和json很是的簡單,能夠直接構造一個 Blob。
Blob(blobParts[, options])
返回一個新建立的 Blob 對象,其內容由參數中給定的數組串聯組成。
// html
<textarea name="" id="text" cols="30" rows="10"></textarea>
<button id="textBtn">下載文本</button>
<p></p>
<textarea name="" id="json" cols="30" rows="10" disabled>
{
"name": "秋風的筆記"
}
</textarea>
<button id="jsonBtn">下載JSON</button>
//js
import {downloadByContent, downloadByDataURL} from '../js/utils.js';
textBtn.onclick = () => {
const value = text.value;
downloadByContent(value, 'hello.txt', 'text/plain');
// downloadByDataURL(value, 'hello.txt', 'text/plain');
}
jsonBtn.onclick = () => {
const value = json.value;
downloadByContent(value, 'hello.json', 'application/json');
// downloadByDataURL(value, 'hello.json', 'application/json');
}
效果圖
註釋代碼爲 data:URL 的展現部分,因爲是第一個例子,所以我講展現代碼,後面都省略了,可是你也能夠經過調用 downloadByDataURL
方法,找不到該方法的定義請滑到文章開頭哦~
excel
excel 能夠說是咱們部分前端打交道很深的一個場景,什麼數據中臺,每天須要導出各類報表。之前都是前端請求後端,來獲取一個 excel 文件地址。如今讓咱們來展現下純前端是如何實現下載excel。
簡單excel
表格長這個模樣,比較簡陋的形式
const template = '<html xmlns:o="urn:schemas-microsoft-com:office:office" '
+'xmlns:x="urn:schemas-microsoft-com:office:excel" '
+'xmlns="http://www.w3.org/TR/REC-html40">'
+'<head>'
+'</head>'
+'<body><table border="1" style="width:60%; text-align: center;">{table}</table><\/body>'
+'<\/html>';
const context = template.replace('{table}', document.getElementById('excel').innerHTML);
downloadByContent(context, 'qiufengblue.xls', 'application/vnd.ms-excel');
可是編寫並不複雜,依舊是和咱們以前同樣,經過構造出 excel
的格式,轉化成 blob 來進行下載。
最終導出的效果
element-ui 導出表格
沒錯,這個就是 element-ui
官方table
的例子。
導出效果以下,能夠說很是完美。
這裏咱們用到了一個插件 https://github.com/SheetJS/sheetjs
使用起來很是簡單。
<template>
<el-table id="ele" border :data="tableData" style="width: 100%">
<el-table-column prop="date" label="日期" width="180">
</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column>
</el-table>
<button @click="exportExcel">導出excel</button>
</template>
<script>
...
methods: {
exportExcel() {
let wb = XLSX.utils.table_to_book(document.getElementById('ele'));
XLSX.writeFile(wb, 'qiufeng.blue.xlsx');
}
}
...
</script>
word
講完了 excel
咱們再來說講 word
這但是 office 三劍客另一大利器。這裏咱們依舊是利用上述的 blob 的方法進行下載。
簡單示例
代碼展現
exportWord.onclick = () => {
const template = '<html xmlns:o="urn:schemas-microsoft-com:office:office" '
+'xmlns:x="urn:schemas-microsoft-com:office:word" '
+'xmlns="http://www.w3.org/TR/REC-html40">'
+'<head>'
+'</head>'
+'<body>{table}<\/body>'
+'<\/html>';
const context = template.replace('{table}', document.getElementById('word').innerHTML);
downloadByContent(context, 'qiufeng.blue.doc', 'application/msword');
}
效果展現
使用 docx.js
插件
若是你想有更高級的用法,可使用 docx.js
這個庫。固然用上述方法也是能夠高級定製的。
代碼
<button type="button" onclick="generate()">下載word</button>
<script>
async function generate() {
const res = await axios({
method: 'get',
url: 'http://localhost:8888/static/1597375650384.jpg',
responseType: 'blob'
})
const doc = new docx.Document();
const image1 = docx.Media.addImage(doc, res.data, 300, 400)
doc.addSection({
properties: {},
children: [
new docx.Paragraph({
children: [
new docx.TextRun("歡迎關注[秋風的筆記]公衆號").break(),
new docx.TextRun("").break(),
new docx.TextRun("按期發送優質文章").break(),
new docx.TextRun("").break(),
new docx.TextRun("美團點評2020校招-內推").break(),
],
}),
new docx.Paragraph(image1),
],
});
docx.Packer.toBlob(doc).then(blob => {
console.log(blob);
saveAs(blob, "qiufeng.blue.docx");
console.log("Document created successfully");
});
}
</script>
效果(沒有打廣告...隨便找了張圖,強行不認可系列)
zip下載
前端壓縮仍是很是有用的,在必定的場景下,能夠節省流量。而這個場景比較使用於,例如前端打包圖片下載、前端打包下載圖標。
一開始我覺得我 https://tinypng.com/ 就是用了這個,結果我發現我錯了...仔細一想,由於它壓縮好的圖片是存在後端的,若是使用前端打包的話,反而要去請求全部壓縮的圖片從而來獲取圖片流。若是用後端壓縮話,能夠有效節省流量。嗯。。。失敗例子了結。
後來又覺得https://www.iconfont.cn/打包下載圖標的時候,使用了這個方案....發現....我又錯了...可是咱們分析一下.
它官網都是 svg 渲染的圖標,對於 svg 下載的時候,徹底可使用前端打包下載。可是,它還支持 font 以及 jpg 格式,因此爲了統一,採用了後端下載,可以理解。那咱們就來實現這個它未完成的功能,固然咱們還須要用到一個插件,就是 jszip。
這裏我從以上找了兩個 svg 的圖標。
實現代碼
download.onclick = () => {
const zip = new JSZip();
const svgList = [{
id: 'demo1',
}, {
id: 'demo2',
}]
svgList.map(item => {
zip.file(item.id + '.svg', document.getElementById(item.id).outerHTML);
})
zip.generateAsync({
type: 'blob'
}).then(function(content) {
// 下載的文件名
var filename = 'svg' + '.zip';
// 建立隱藏的可下載連接
var eleLink = document.createElement('a');
eleLink.download = filename;
// 下載內容轉變成blob地址
eleLink.href = URL.createObjectURL(content);
// 觸發點擊
eleLink.click();
// 而後移除
});
}
查看文件夾目錄,已經將 SVG 打包下載完畢。
瀏覽器文件系統(實驗性)
在我電腦上都有這麼一個瀏覽器,用來學習和調試 chrome
的最新新特性, 若是你的電腦沒有,建議你安裝一個。
玩這個特性須要打開 chrome 的實驗特性 chrome://flags
=> #native-file-system-api
=> enable
, 由於實驗特性都會伴隨一些安全或者影響本來的渲染的行爲,所以我再次強烈建議,下載一個金絲雀版本的 chrome 來進行玩耍。
<textarea name="" id="textarea" cols="30" rows="10"></textarea>
<p><button id="btn">下載</button></p>
<script>
btn.onclick = async () => {
const handler = await window.chooseFileSystemEntries({
type: 'save-file',
accepts: [{
description: 'Text file',
extensions: ['txt'],
mimeTypes: ['text/plain'],
}],
});
const writer = await handler.createWritable();
await writer.write(textarea.value);
await writer.close();
}
</script>
實現起來很是簡單。卻飛通常的感受。
其餘場景
H5文件下載
通常在 h5 下載比較多的是 pdf 或者是 apk 的下載。
Android
在安卓瀏覽器中,瀏覽器直接下載文件。
ios
因爲ios的限制,沒法進行下載,所以,可使用複製 url ,來代替下載。
import {downloadDirect} from '../js/utils.js';
const btn = document.querySelector('#download-ios');
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
const clipboard = new ClipboardJS(btn);
clipboard.on('success', function () {
alert('已複製連接,打開瀏覽器粘貼連接下載');
});
clipboard.on('error', function (e) {
alert('系統版本太低,複製連接失敗');
});
} else {
btn.onclick = () => {
downloadDirect(btn.dataset.clipboardText)
}
}
更多
對於 apk 等下載包可使用這個包(本人暫時沒有試驗,接觸很少,回頭熟悉了再回來補充。)
https://github.com/jawidx/web-launch-app
大文件的分片下載
最近在開發媒體流相關的工做的時候,發如今加載 mp4 文件的時候,發現了一個比較有意思的現象,視頻流並不須要將整個 mp4 下載完才進行播放,而且伴隨了不少狀態碼爲 206 的請求,乍一看有點像流媒體(HLS等)的韻味。
以爲這個現象很是的有意思,他可以分片地加載資源,這對於體驗或者是流量的節省都是很是大的幫助。最終發現它帶了一個名爲 Range 的頭。咱們來看看 MDN 的解釋。
The
Range
是一個請求首部,告知服務器返回文件的哪一部分。在一個Range
首部中,能夠一次性請求多個部分,服務器會以 multipart 文件的形式將其返回。若是服務器返回的是範圍響應,須要使用206
Partial Content
狀態碼。 摘自 MDN
語法
Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
Node實現
既然咱們知道了它的原理,就來本身實現一下。
router.get('/api/rangeFile', async(ctx) => {
const { filename } = ctx.query;
const { size } = fs.statSync(path.join(__dirname, './static/', filename));
const range = ctx.headers['range'];
if (!range) {
ctx.set('Accept-Ranges', 'bytes');
ctx.body = fs.readFileSync(path.join(__dirname, './static/', filename));
return;
}
const { start, end } = getRange(range);
if (start >= size || end >= size) {
ctx.response.status = 416;
ctx.set('Content-Range', `bytes */${size}`);
ctx.body = '';
return;
}
ctx.response.status = 206;
ctx.set('Accept-Ranges', 'bytes');
ctx.set('Content-Range', `bytes ${start}-${end ? end : size - 1}/${size}`);
ctx.body = fs.createReadStream(path.join(__dirname, './static/', filename), { start, end });
})
Nginx實現
發現 nginx 不須要寫任何代碼就默認支持了 range 頭,想着我必定知道它究竟是支持,仍是加入了什麼模塊,或者是我默認開啓了什麼配置,找了半天沒有找到什麼額外的配置。
正當我準備放棄的時候,靈光一現,去看看源碼吧,說不定會有發現,去查了 nginx 源碼相關的內容,用了慣用的反推方式,才發現原來是max_ranges
這個字段。
https://github.com/nginx/nginx/blob/release-1.13.6/src/http/modules/ngx_http_range_filter_module.c#L166
這也怪我一開始文檔閱讀不夠仔細,浪費了大量的時間。
:) 其實我對 nginx 源碼也不熟悉,這裏能夠用個小技巧,直接在源碼庫 搜索 206 而後 發現了一個宏命令
#define NGX_HTTP_PARTIAL_CONTENT 206
而後順藤摸瓜,直接找到這個宏命令NGX_HTTP_PARTIAL_CONTENT
用到的地方,這樣一步一步就慢慢能找到咱們想要的。
默認 nginx 是自動開啓 range 頭的, 若是不須要配置,則配置 max_range: 0;
Nginx 配置文檔 http://nginx.org/en/docs/http/ngx_http_core_module.html#max_ranges
總結
咱們能夠來總結一下,其實全文主要講了(xbb)兩個核心的知識,一個是 blob
一個a
標籤,另外還要注意對於大文件,服務器的優化策略,能夠經過 Range
來分片加載。
參考資料
https://github.com/dolanmiu/docx
https://github.com/SheetJS/sheetjs
https://juejin.im/post/6844903763359039501
本文分享自微信公衆號 - 前端日誌(gh_12dcc43e6039)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。