Yêu cầu
Trước đây, việc tải xuống dữ liệu đều được thực hiện tại từng module nghiệp vụ riêng biệt. Khi gọi API, sử dụng đối tượng Blob để chuyển đổi dữ liệu, sau đó tạo thẻ a để bắt đầu quá trình tải về, cần tự định nghĩa tên và định dạng file.
Bây giờ, tất cả xử lý đều được cấu hình tại interceptor, dựa trên trường Content-Disposition trong response.headers khi backend trả về file stream. Chúng ta sẽ phân tích tên file từ header này, giải mã tên file và thực hiện tải về bằng thẻ a.
Cách tiếp cận trước đây (viết hàm riêng tại từng module nghiệp vụ)
// Xuất dữ liệu
indexExport() {
let statYear = {
statDate: this.form.statDate,
dataType: "1",
};
let infoMsg = this.$notify.info({
title: "Thông báo",
message: "Đang tải xuống tệp, vui lòng không thoát khỏi trang",
duration: 0,
});
gljyjcDataExport(statYear).then((res) => {
infoMsg.close(); // Đóng thông báo khi hoàn thành tải xuống
this.$notify({
title: "Thành công",
message: "Tải xuống hoàn tất",
type: "success",
});
let blob = new Blob([res], {
type: "",
});
let url = window.URL.createObjectURL(blob);
const link = document.createElement("a"); // Tạo thẻ a
link.href = url;
link.download = "Danh sách dữ liệu (" + this.form.statDate + ").xlsx"; // Đặt tên file
link.click();
URL.revokeObjectURL(url); // Giải phóng bộ nhớ
});
},
Cách tiếp cận hiện tại (xử lý uniform trong interceptor của axios)
Chú ý vào phần ghi chú "Tải xuống tệp" – khi backend trả về file stream, header response sẽ chứa trường Content-Disposition. Từ đó lấy ra tên file, tiến hành giải mã tên file.
Cả decodeURIComponent và decodeURI đều có thể dùng để giải mã, nếu bạn chưa hiểu rõ sự khác biệt giữa chúng thì nên tìm hiểu thêm.
// Trong interceptor, chắc chắn rằng bạn đã có request interceptor: axios.interceptors.request
...
axios.interceptors.response.use(
response => {
const res = response.data;
const config = response.config;
console.log(response.headers,"response.headers")// Kiểm tra nội dung response.headers
// Tải xuống tệp (chính là phần quan trọng)
if (response.headers['content-disposition']) {
let downLoadMark = response.headers['content-disposition'].split(';');
if (downLoadMark[0] === 'attachment') {
// Bắt đầu tải xuống
let fileName = downLoadMark[1].split('filename=')[1];
if (fileName) {
//fileName = decodeURIComponent(filename); // Giải mã tên file
fileName = decodeURI(fileName);
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(new Blob([res]), fileName);
} else {
let url = window.URL.createObjectURL(new Blob([res]));
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
return;
}
} else {
return res;
}
}
}
// Xử lý lỗi toàn cục (kiểm tra mã lỗi để thực hiện các thao tác phù hợp)
if (res.code !== CODE_SUCCESS) {
if (res.code == '205') {
Message.error({ message: res.data || "Đăng nhập thất bại" });
store.dispatch("user/logout").then(() => {
window.location.reload();
});
return
}
if (res.code === WARN_TIP) {
Message.warning({
message: res.message
});
}
if (res.code === LOGIN_FAIL) {
Message.error({ message: res.message || "Đăng nhập thất bại" });
}
// Xử lý các mã lỗi khác
return Promise.reject(new Error(res.message || "Lỗi"));
}
return res;
}, error => {
// Ngăn chặn gửi trùng
if (error.message) {
allowRequest(reqList, error.message.url);
}
if (error.response) {
if (error.response.data.code == 600 && !tipCode) {
tipCode = true;
Message.error({ message: 'Phiên đăng nhập đã hết hạn, vui lòng đăng nhập lại!' });
} else if (error.response.status == 500) {
Message.error({ message: 'Hệ thống gặp lỗi' });
}
}
return Promise.reject(error);
}
);<br></br>// Lọc URL nếu cần
const allowRequest = function (reqList, url) {
for (let i = 0; i < reqList.length; i++) {
if (reqList[i].url === url) {
reqList.splice(i, 1);
break;
}
}
};
Trên đây là cách hiển thị tên folder trước khi giải mã. Sau khi giải mã bằng decodeURIComponent hoặc decodeURI, frontend có thể nhận được tên file tiếng Trung do backend trả về.
Sao chép liên kết bài viết và gửi cho back-end
Cho đến thời điểm này, frontend chỉ làm được đến vậy. Một số người hỏi: Nếu response.headers không có Content-Disposition, làm thế nào frontend có thể xử lý?
Để trả lời câu hỏi này, tôi đã yêu cầu back-end cung cấp đoạn mã họ sử dụng. Vì tôi không hiểu rõ các khái niệm như Controller hay Util class nên tôi sẽ giữ nguyên đoạn mã gốc:
Controller (không biết là gì)
@PostMapping(value="/exportAddresses")
public Result exportAddresses(HttpServletResponse response){
String[] titles = new String[] {"id","tableCode","columnName"};
List
Util class (không hiểu là gì)
public static void exportExcel(HttpServletResponse response,String fileName,String[] titles,List
Xử lý tiếng Trung bị lỗi mã hóa (không hiểu gì hết)
String encodedFilename = URLEncoder.encode(fileName, "UTF-8"); // Mã hóa tên file tiếng Trung
response.addHeader("Content-Type","application/octet-stream;charset=utf-8");// Cài đặt cùng kiểu mã hóa
response.setHeader("Content-disposition", "attachment; filename="+encodedFilename);
Nếu bạn có cách thực hiện tốt hơn, hãy chia sẻ nhé!