| 場景 | 瀏覽器行為 | 關鍵點 |
|---|---|---|
地址欄輸入 URL / <a href> | 發起 **導航請求**,收到 Content-Disposition: attachment 直接彈出「另存為」對話框 | 瀏覽器原生處理下載流 |
JS $.ajax / fetch / Angular $http | 發起 **XHR/Fetch 請求**,響應體進入 JS 變量,**不會自動觸發下載** | 需手動創建 Blob + ObjectURL + 點擊隱藏 <a> |
瀏覽器安全策略:腳本發起的請求不允許直接彈出文件保存對話框,必須由用戶交互(點擊 <a download>)觸發。
// Controller 返回 byte[] 或 ResponseEntity<byte[]>
@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {
byte[] data = fileService.getFileBytes(id);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", "export.xlsx");
return new ResponseEntity<>(data, headers, HttpStatus.OK);
}
// 前端 AngularJS / jQuery / 原生 fetch 統一模式
function downloadFile(url, filename) {
fetch(url, { method: 'GET', credentials: 'include' })
.then(resp => resp.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
});
}
// 按鈕綁定
<button ng-click="downloadFile('/api/download/123', 'report.xlsx')">下載</button>
window.open / <a href> 但帶參數?// 構建帶參數 URL,瀏覽器原生導航自動下載
const params = new URLSearchParams({ token: getToken(), id: 123 });
window.open(`/api/download?${params}`, '_blank');
// 或隱藏 form 提交
const form = document.createElement('form');
form.method = 'GET';
form.action = '/api/download';
form.target = '_blank';
Object.entries({ token: getToken(), id: 123 }).forEach(([k,v]) => {
const i = document.createElement('input');
i.type = 'hidden'; i.name = k; i.value = v;
form.appendChild(i);
});
document.body.appendChild(form);
form.submit();
form.remove();
Content-Disposition: attachment; filename="..." 是否存在AJAX 無法直接下載文件。選項二擇一:
window.open / 隱藏 form.submit() 觸發瀏覽器原生導航下載createObjectURL + <a download>.click() 模擬下載推薦方案 2,體驗更可控(進度條、錯誤提示、不阻塞頁面)。