在瀏覽器中異步下載文件,其實(shí)就是把服務(wù)器響應(yīng)的文件先保存在內(nèi)存中。然后再一次下載到磁盤。第二次下載過(guò)程,就是把內(nèi)存的數(shù)據(jù)IO到磁盤,沒(méi)有網(wǎng)絡(luò)開(kāi)銷。速度極快。
之所以要先保存在內(nèi)存,主要是可以在下載開(kāi)始之前和下載結(jié)束后可以做一些業(yè)務(wù)邏輯(例如:校驗(yàn),判斷),還可以監(jiān)聽(tīng)下載的進(jìn)度。
演示
這里演示一個(gè)Demo,在點(diǎn)擊下載摁鈕后,彈出加loading框。在讀取到服務(wù)器的響應(yīng)的文件后。關(guān)閉loading框。并且在控制臺(tái)中輸出下載的進(jìn)度。
有點(diǎn)像是監(jiān)聽(tīng)文件下載完畢的意思,也只能是像。從內(nèi)存IO到磁盤的這個(gè)過(guò)程,JS代碼,再也無(wú)法染指過(guò)程。更談不上監(jiān)聽(tīng)了。
Controller
服務(wù)端的下載實(shí)現(xiàn)
import JAVA.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMApping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/download")
public class DownloadController {
@GetMapping
public void download (HttpServletRequest request,
HttpServletResponse response, @RequestParam("file") String file) throws IOException {
Path path = Paths.get(file);
if (Files.notExists(path) || Files.isDirectory(path)) {
// 文件不存在,或者它是一個(gè)目錄
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return ;
}
String contentType = request.getServletContext().getMimeType(file);
if (contentType == null) {
// 如果沒(méi)讀取到ContentType,則設(shè)置為默認(rèn)的二進(jìn)制文件類型
contentType = "application/octet-stream";
}
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(Files.newInputStream(path))){
response.setContentType(contentType);
response.setHeader("Content-Disposition", "attachment; filename=" + new String(path.getFileName().toString().getBytes("GBK"), "ISO-8859-1"));
// 關(guān)鍵點(diǎn),給客戶端響應(yīng)Content-Length頭,客戶端需要用此來(lái)計(jì)算下載進(jìn)度
response.setContentLengthLong(Files.size(path));
OutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[8192];
int len = 0;
while ((len = bufferedInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
} catch (IOException e) {
}
}
}
Index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>異步下載</title>
</head>
<body>
<input name="name" value="D:\eclipse-jee-2019-12-R-win32-x86_64.zip" placeholder="輸入你要下載的文件路徑" id="file" />
<button id="button" onclick="downlod();">開(kāi)始下載</button>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<script src="/layer/layer.js"></script>
<script type="text/JavaScript">
function downlod(){
const file = document.querySelector('#file').value;
if (!file){
alert('請(qǐng)輸入合法的文件地址');
}
// 打開(kāi)加載動(dòng)畫(huà)
const index = layer.load(1, {
shade: [0.1,'#fff']
});
const xhr = new XMLHttpRequest();
xhr.open('GET', '/download?file=' + encodeURIComponent(file));
xhr.send(null);
// 設(shè)置服務(wù)端的響應(yīng)類型
xhr.responseType = "blob";
// 監(jiān)聽(tīng)下載
xhr.addEventListener('progress', event => {
// 計(jì)算出百分比
const percent = ((event.loaded / event.total) * 100).toFixed(2);
console.log(`下載進(jìn)度:${percent}`);
}, false);
xhr.onreadystatechange = event => {
if(xhr.readyState == 4){
if (xhr.status == 200){
// 獲取ContentType
const contentType = xhr.getResponseHeader('Content-Type');
// 文件名稱
const fileName = xhr.getResponseHeader('Content-Disposition').split(';')[1].split('=')[1];
// 創(chuàng)建一個(gè)a標(biāo)簽用于下載
const donwLoadLink = document.createElement('a');
donwLoadLink.download = fileName;
donwLoadLink.href = URL.createObjectURL(xhr.response);
// 觸發(fā)下載事件,IO到磁盤
donwLoadLink.click();
// 釋放內(nèi)存中的資源
URL.revokeObjectURL(donwLoadLink.href);
// 關(guān)閉加載動(dòng)畫(huà)
layer.close(index);
} else if (response.status == 404){
alert(`文件:${file} 不存在`);
} else if (response.status == 500){
alert('系統(tǒng)異常');
}
}
}
}
</script>
</html>
現(xiàn)在的ajax請(qǐng)求,幾乎都是用ES6的fetch,支持異步,而且代碼也更優(yōu)雅。API設(shè)計(jì)得更合理。但是目前為止,好像fetch并沒(méi)有progress事件,也就說(shuō)它不支持監(jiān)聽(tīng)上傳下載的進(jìn)度。所以沒(méi)轍,還是得用XMLHttpRequest。
最后
這種方式弊端也是顯而易見(jiàn),如果文件過(guò)大。那么內(nèi)存就炸了。我覺(jué)得瀏覽器應(yīng)該暴露一個(gè)js的接口。允許通過(guò)異步的方式直接下載文件IO到磁盤,通過(guò)回調(diào)給出下載的進(jìn)度,IO的進(jìn)度。
原文:https://springboot.io/t/topic/2734