在瀏覽器中異步下載文件,其實就是把服務器響應的文件先保存在內存中。然后再一次下載到磁盤。第二次下載過程,就是把內存的數據IO到磁盤,沒有網絡開銷。速度極快。
之所以要先保存在內存,主要是可以在下載開始之前和下載結束后可以做一些業務邏輯(例如:校驗,判斷),還可以監聽下載的進度。
演示
這里演示一個Demo,在點擊下載摁鈕后,彈出加loading框。在讀取到服務器的響應的文件后。關閉loading框。并且在控制臺中輸出下載的進度。
有點像是監聽文件下載完畢的意思,也只能是像。從內存IO到磁盤的這個過程,JS代碼,再也無法染指過程。更談不上監聽了。
Controller
服務端的下載實現
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)) {
// 文件不存在,或者它是一個目錄
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return ;
}
String contentType = request.getServletContext().getMimeType(file);
if (contentType == null) {
// 如果沒讀取到ContentType,則設置為默認的二進制文件類型
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"));
// 關鍵點,給客戶端響應Content-Length頭,客戶端需要用此來計算下載進度
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();">開始下載</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('請輸入合法的文件地址');
}
// 打開加載動畫
const index = layer.load(1, {
shade: [0.1,'#fff']
});
const xhr = new XMLHttpRequest();
xhr.open('GET', '/download?file=' + encodeURIComponent(file));
xhr.send(null);
// 設置服務端的響應類型
xhr.responseType = "blob";
// 監聽下載
xhr.addEventListener('progress', event => {
// 計算出百分比
const percent = ((event.loaded / event.total) * 100).toFixed(2);
console.log(`下載進度:${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];
// 創建一個a標簽用于下載
const donwLoadLink = document.createElement('a');
donwLoadLink.download = fileName;
donwLoadLink.href = URL.createObjectURL(xhr.response);
// 觸發下載事件,IO到磁盤
donwLoadLink.click();
// 釋放內存中的資源
URL.revokeObjectURL(donwLoadLink.href);
// 關閉加載動畫
layer.close(index);
} else if (response.status == 404){
alert(`文件:${file} 不存在`);
} else if (response.status == 500){
alert('系統異常');
}
}
}
}
</script>
</html>
現在的ajax請求,幾乎都是用ES6的fetch,支持異步,而且代碼也更優雅。API設計得更合理。但是目前為止,好像fetch并沒有progress事件,也就說它不支持監聽上傳下載的進度。所以沒轍,還是得用XMLHttpRequest。
最后
這種方式弊端也是顯而易見,如果文件過大。那么內存就炸了。我覺得瀏覽器應該暴露一個js的接口。允許通過異步的方式直接下載文件IO到磁盤,通過回調給出下載的進度,IO的進度。
原文:https://springboot.io/t/topic/2734