前端 JS 實現字串/圖片/excel 檔案下載
編者按:本文轉載自SF專欄,由作者 趙帥強 授權奇舞週刊轉載
在 web 開發中,如果你想讓使用者下載或者匯出一個檔案,應該怎麼做呢?
傳統的做法是在後端儲存或者即時生成一個檔案來提供下載功能,這樣的優勢是可以做許可權控制、資料二次處理,但缺點是需要額外發起請求、增大服務端壓力、下載速度慢。
但隨著 HTML5 的標準釋出,我們已經能夠做到只前端來下載各種檔案了。
後端響應式下載
在常規的HTTP應答中, Content-Disposition 訊息頭指示回覆的內容該以何種形式展示,是以內聯的形式(即網頁或者頁面的一部分),還是以附件的形式下載並儲存到本地。
在 HTTP 場景中,第一個引數或者是 inline (預設值,表示回覆中的訊息體會以頁面的一部分或者整個頁面的形式展示),或者是 attachment (意味著訊息體應該被下載到本地;大多數瀏覽器會呈現一個“儲存為”的對話方塊,將 filename 的值預填為下載後的檔名)。
我們在後端響應頭中只要設定該頭部資訊,即可下載為檔案,而不是請求並展示:
Content-Type: text/html; charset=utf-8
Content - Disposition : attachment ; filename = "cool.html"
但需要注意的是,如果想要用這種方式下載檔案,不能使用 AJAX 的方式,而是應該新建一個 <a> 標籤,模擬點選下載。原因為處於安全性考慮, JavaScript 無法與磁碟進行互動,因此AJAX得到的內容將被保留在記憶體中,而不是磁碟上。
Nginx 新增 header 頭下載
location ~ \.(jpg|jpeg|png|bmp|ico|gif|swf)$ {
add_header Content - Disposition 'attachment; filename="cool.html"' ;
}
和後端一樣的原理,只不過頭部資訊通過 Nginx 統一新增。
前端下載: <a> 標籤的 download 屬性
此屬性指示瀏覽器下載 URL 而不是導航到它,因此將提示使用者將其儲存為本地檔案。如果屬性有一個值,那麼它將作為下載的檔名使用。此屬性對允許的值沒有限制,但是 / 和 \ 會被轉換為下劃線。
-
此屬性僅適用於同源 URLs 。
-
儘管 HTTP URL 需要位於同一源中,但是可以使用 blob: URLs 和 data: URLs ,以方便使用者下載 JavaScript 方式生成的內容(例如使用線上繪圖的 Web 應用建立的照片)。
常規的 <a> 標籤,用於連結的跳轉,如新的頁面,那麼如果我們給 <a> 標籤加上 download 屬性,就能很簡單的讓使用者儲存新的 html 頁面。
<a download="PHP實現併發請求.html" href="https://segmentfault.com/a/1190000016343861">PHP實現併發請求</a>
生成並下載字串檔案
首先我們需要了解一個特殊的資料格式: Blob 。
Blob 資料
Blob(Binary Large Object,二進位制型別的大物件) ,表示一個不可變的原始資料的類檔案物件,我們上傳檔案時常用的 File 物件就繼承於 Blob ,並進行了擴充套件用於支援使用者系統上的檔案。
我們只能通過 Blob() 建構函式來建立一個新的 Blob 物件:
Blob(blobParts[, options])
// 建立一個json型別的Blob物件,支援傳入同類型資料的一個數組
var debug = { hello : "world" } ;
var blob = new Blob ( [ JSON . stringify ( debug , null , 2 ) ] ,
{ type : 'application/json' } ) ;
// 此時blob的值
// Blob(22) {size: 22, type: 'application/json'}
Blob 物件存在兩個只讀屬性:
size : Blob 物件中所包含資料的大小(位元組)。
type
: 一個字串,表明該Blob物件所包含資料的MIME型別。如果型別未知,則該值為空字串。
URL 物件和下載字串檔案
URL 介面是一個用來建立 URLs 的物件,包含兩個靜態方法:
objectURL = URL.createObjectURL(blob)
建立一個 URL(DOMString) ,包含一個唯一的blob連結(該連結協議為以blob:,後跟唯一標識瀏覽器中的物件的掩碼)。這個 URL 的生命週期和建立它的視窗中的 document 繫結。
URL.revokeObjectURL(objectURL)
銷燬之前使用URL.createObjectURL()方法建立的URL例項。瀏覽器會在文件退出的時候自動釋放它們,但是為了獲得最佳效能和記憶體使用狀況,你應該在安全的時機主動釋放掉它們。
var url = URL.createObjectURL(blob);
// 此時url的值,跟document繫結,所以每個頁面建立的字串均不同
此時,我們在頁面中建立一個新 <a> 標籤,點選即可下載我們想要的檔案:
<a href="blob:https://developer.mozilla.org/58702010-433d-4097-990f-e483d84cd02a" download="file.json">下載檔案連結</a>
FileReader 讀取 Blob 資料
想要讀取 Blob 資料的唯一方法是 FileReader 。
FileReader 物件允許Web應用程式非同步讀取儲存在使用者計算機上的檔案(或原始資料緩衝區)的內容,使用 File 或 Blob 物件指定要讀取的檔案或資料。
其中 File 物件可以是來自使用者在一個 <input> 元素上選擇檔案後返回的 FileList 物件,也可以來自拖放操作生成的 DataTransfer 物件,還可以是來自在一個 HTMLCanvasElement 上執行 mozGetAsFile() 方法後返回結果。
該物件包含3個屬性:
FileReader.error
一個DOMException,表示在讀取檔案時發生的錯誤 。
FileReader.readyState
表示FileReader狀態的數字。取值如下:
常量名 值 描述
EMPTY 0 還沒有載入任何資料.
LOADING 1 資料正在被載入.
DONE 2 已完成全部的讀取請求.
FileReader.result
檔案的內容。該屬性僅在讀取操作完成後才有效,資料的格式取決於使用哪個方法來啟動讀取操作。
包含6個事件處理: onabort,onerror,onload,onloadstart,onloadend,onprogress ,這些不再詳細說明,因為 FileReader 繼承自 EventTarget ,所以所有這些事件也可以通過 addEventListener 方法使用。
包含5個方法:
FileReader.abort()
中止讀取操作。在返回時,readyState屬性為DONE。
FileReader.readAsArrayBuffer()
開始讀取指定的 Blob中的內容, 一旦完成, result 屬性中儲存的將是被讀取檔案的 ArrayBuffer 資料物件.
FileReader.readAsBinaryString()
開始讀取指定的Blob中的內容。一旦完成,result屬性中將包含所讀取檔案的原始二進位制資料。
FileReader.readAsDataURL()
開始讀取指定的Blob中的內容。一旦完成,result屬性中將包含一個data: URL格式的字串以表示所讀取檔案的內容。
FileReader.readAsText()
開始讀取指定的Blob中的內容。一旦完成,result屬性中將包含一個字串以表示所讀取的檔案內容。
因此我們可以直接讀取 Blob 物件的資料:
var reader = new FileReader();
reader . addEventListener ( "loadend" , function ( ) {
console . log ( reader . result ) ;
} ) ;
reader . readAsDataURL ( blob ) ;
// 此時result的值
// data:application/json;base64,ewogICJoZWxsbyI6ICJ3b3JsZCIKfQ==
reader . readAsText ( blob ) ;
// 此時result的值
// {
// "hello": "world"
// }
下載圖片
除了下載手動生成的字串或物件,我們還能提供下載圖片的功能,一方面能用於支援 Canvas 繪圖的儲存功能,一方面能提供批量下載圖片等高階功能。
除了瀏覽器自帶的右鍵儲存,我們還可以這麼做來下載圖片:
// 通過src獲取圖片的blob物件
function getImageBlob ( url , cb ) {
var xhr = new XMLHttpRequest ( ) ;
xhr . open ( "get" , url , true ) ;
xhr . responseType = "blob" ;
xhr . onload = function ( ) {
if ( this . status == 200 ) {
cb ( this . response ) ;
}
} ;
xhr . send ( ) ;
}
let reader = new FileReader ( ) ;
reader . addEventListener ( "loadend" , function ( ) {
console . log ( reader . result ) ;
} ) ;
getImageBlob ( 'https://cdn.segmentfault.com/v-5c4ec07f/global/img/user-64.png' , function ( blob ) {
// 讀取來看下下載的內容
reader . readAsDataURL ( blob ) ;
// 最終生成的字串
// data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAA...
// 生成下載用的URL物件
let url = URL . createObjectURL ( blob ) ;
// 生成一個a標籤,並模擬點選,即可下載,批量下載同理
let aDom = aDom = document . createElement ( 'a' ) ;
aDom . href = url ;
aDom . download = 'download.json' ;
aDom . text = '下載檔案' ;
document . getElementsByTagName ( 'body' ) [ 0 ] . appendChild ( aDom ) ;
aDom . click ( ) ;
} ) ;
下載 excel 檔案等
如果你明白了下載的原理,那麼所有的內容都能夠理解,只不過是轉換成對應的格式而已,當然,複雜格式的文件不需要你自己去配置,可以引入第三方庫,在 excel 文件方面我選擇用 tableExport庫(https://tableexport.v5.travismclarke.com/#javascript):
// 引入CDN檔案
'https://cdn.bootcss.com/xlsx/0.14.1/xlsx.core.min.js' ,
'https://cdn.bootcss.com/FileSaver.js/2014-11-29/FileSaver.min.js' ,
'https://cdn.bootcss.com/TableExport/5.2.0/js/tableexport.min.js'
const tableDom = $ ( '#table' ) ;
$ ( '.table-exportBtn' , tableDom ) . on ( 'click' , function ( ) {
const tableExport = tableDom . tableExport ( {
formats : [ 'xlsx' , 'txt' ] ,
filename : '表格下載' ,
exportButtons : false
} ) ;
const type = $ ( this ) . data ( ) . type ;
const exportData = tableExport . getExportData ( ) [ tableDom [ 0 ] . id ] [ type ] ;
const { data , mimeType , filename , fileExtension , merges , RTL , sheetname } = exportData ;
// 原始碼裡才能看到完整引數,官方文件沒有寫全,導致下載的檔案格式錯誤
tableExport . export2file ( data , mimeType , filename , fileExtension , merges , RTL , sheetname ) ;
} ) ;
預設的方法會自動生成下載按鈕,但如果你想自定義下載功能,參考 exportButtons: false 設定(https://tableexport.v3.travismclarke.com/examples/exportButtons.html) 一節,但這個文件有問題, export2file 引數不完整,導致下載的 xlsx 檔案一直格式錯誤,通過檢視原始碼,需要寫全引數才可以,上面的示例裡已經寫出。
tableExport 庫原始碼
我們可以看下 tableExport 匯出檔案的核心程式碼,其匯出為 excel 格式比較複雜,由 xlsx.core.min.js 來完成:
/**
/**
* Exports and downloads the file
* @memberof TableExport.prototype
* @param data {String}
* @param mime {String} mime type
* @param name {String} filename
* @param extension {String} file extension
* @param merges {Object[]}
* @param RTL {Boolean}
*/
export2file : function ( data , mime , name , extension , merges , RTL , sheetname ) {
var format = extension . slice ( 1 ) ;
data = this . getRawData ( data , extension , name , merges , RTL , sheetname ) ;
if ( _isMobile && ( format === _FORMAT . CSV || format === _FORMAT . TXT ) ) {
// 拼湊指定格式的data:型別 URI
var dataURI = "data:" + mime + ";" + this . charset + "," + data ;
this . downloadDataURI ( dataURI , name , extension ) ;
} else {
// TODO: error and fallback when `saveAs` not available
saveAs ( new Blob ( [ data ] , { type : mime + ";" + this . charset } ) , name + extension , true ) ;
}
} ,
// 先建立<a>標籤,然後提供href和download屬性,並模擬點選
downloadDataURI : function ( dataURI , name , extension ) {
var encodedUri = encodeURI ( dataURI ) ;
var link = document . createElement ( "a" ) ;
link . setAttribute ( "href" , encodedUri ) ;
link . setAttribute ( "download" , name + extension ) ;
document . body . appendChild ( link ) ;
link . click ( ) ;
} ,
xlsx 檔案匯出匯出
還沒有仔細研究,感興趣的可以檢視其js-xlsx Github專案(https://github.com/SheetJS/js-xlsx/blob/master/README.md)
第三方庫
上面我們主要講了下載背後的原理,你可以自己封裝,也可以使用現成的第三方庫,如 download.js(https://github.com/rndme/download) ,這個能提供大部分常用資料的下載;但如果你是要下載表格資料為excel格式,還是推薦 tableExport.js(https://tableexport.v5.travismclarke.com/#javascript) 及其依賴元件。
參考資料
-
MDN-a: (https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a)
-
MDN-blob: (https://developer.mozilla.org/zh-CN/docs/Web/API/Blob)
-
掘金-細說Web API中的Blob:(https://juejin.im/post/59e35d0e6fb9a045030f1f35)
-
MDN-URL: (https://developer.mozilla.org/zh-CN/docs/Web/API/URL)
-
MDN-FileReader: (https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader)
-
部落格園-js 獲取圖片url的Blob值並預覽:(https://www.cnblogs.com/tujia/p/6483255.html)
-
tableExport文件:(https://tableexport.v5.travismclarke.com/#javascript)
-
感謝 @Oliveryoung 提供的其他解決方案
-
MDN-Content-Disposition: (https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition)
-
Ajax請求無法下載檔案的原因: (https://blog.csdn.net/w405722907/article/details/77366843)
-
Github-download.js:(https://github.com/rndme/download)
關於奇舞週刊
《奇舞週刊》是360公司專業前端團隊「 奇舞團 」運營的前端技術社群。關注公眾號後,直接傳送連結到後臺即可給我們投稿。