Khi phát triển các ứng dụng doanh nghiệp, yêu cầu phổ biến là cho phép người dùng xuất dữ liệu dạng bảng biểu sang định dạng Excel hoặc PDF. Mặc dù phần lớn quy trình chuyển đổi và tạo tập tin được thực hiện bởi máy chủ, phía Client vẫn cần nắm vững kỹ thuật gọi API và xử lý luồng dữ liệu trả về để đảm bảo tính toàn vẹn của tệp.
Trong một trường hợp cụ thể, tôi đã gặp tình huống khi truy vấn API thành công nhưng kết quả tải về lại là một tập tin Excel trống hoặc báo lỗi hỏng hóc nội dung. Dưới đây là quy trình phân tích và cách khắc phục sự cố này.
Mô tả phương pháp tiếp cận ban đầu
Đầu tiên, giả sử chúng ta có hàm gọi API để lấy dữ liệu xuất khẩu. Theo thói quen mặc định khi làm việc với thư viện HTTP, nhiều lập trình viên thường xử lý phản hồi dưới dạng JSON hoặc văn bản thông thường mà không chú ý đến kiểu dữ liệu nhị phân:
// Hàm khởi tạo yêu cầu cũ (chưa tối ưu cho file nhị phân)
function fetchExportData(payload) {
return apiInstance.get('/api/v1/report/export', { params: payload });
}
// Xử lý tải xuống
fetchExportData(queryParams).then(response => {
// Truy cập header để lấy tên tệp
const disposition = response.headers['content-disposition'];
// Phân tách chuỗi để trích xuất filename
const fileNameMatch = disposition ? disposition.split('filename=')[1].replace(/"/g, '') : 'report.xlsx';
// Sử dụng hàm tiện ích để kích hoạt trình tải xuống
triggerFileDownload(response.data, decodeURIComponent(fileNameMatch));
});
Hàm hỗ trợ triggerFileDownload lúc này được viết đơn giản dựa trên đối tượng Blob:
function triggerFileDownload(content, fileName) {
// Tạo Blob từ dữ liệu nhận được
const fileBlob = new Blob([content], { type: '' });
// Tạo URL tạm thời cho Blob
const url = window.URL.createObjectURL(fileBlob);
// Tạo thẻ anchor ẩn để click tự động
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
// Dọn dẹp tài nguyên
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 0);
}
Vấn đề xảy ra ở đây là dữ liệu trả về từ Server là một luồng byte (stream) thực sự, nhưng thư viện AJAX mặc định có thể giải mã nó sai cách (ví dụ như ép thành chuỗi UTF-8), dẫn đến việc nội dung tệp bị nhiễu hoặc mất đi cấu trúc nhị phân của Excel.
Giai đoạn sửa lỗi và cải tiến
Nguồn gốc của lỗi nằm ở việc cấu hình tham số responseType. Để giữ nguyên vẹn dữ liệu gốc dưới dạng mảng byte, chúng ta bắt buộc phải chỉ định định dạng nhận về là arraybuffer ngay tại thời điểm gửi yêu cầu. Sau đó, khi xây dựng Blob, cần đảm bảo truyền đúng khối lượng bộ nhớ thô vào.
Dưới đây là phiên bản đã được tối ưu hóa:
// Cấu hình lại hàm gọi API
async function requestExportFile(params) {
const response = await apiInstance.get('/api/v1/report/export', {
params: params,
// BẮT BUỘC: Chỉ định nhận dữ liệu dạng mảng byte
responseType: 'arraybuffer'
});
// Trích xuất tên file an toàn hơn bằng Regex
const contentDisposition = response.headers['content-disposition'];
let fileName = 'export_data.xlsx';
if (contentDisposition) {
const match = /filename\*=UTF-8''(.+)/i.exec(contentDisposition) ||
/filename=([^;]+)/i.exec(contentDisposition);
if (match && match[1]) {
fileName = decodeURIComponent(match[1]);
}
}
// Gọi hàm tải xuống với dữ liệu ArrayBuffer
saveFileFromBytes(response.data, fileName);
}
Hàm xử lý lưu trữ cũng cần điều chỉnh để tương thích với dữ liệu đầu vào là mảng byte thay vì đối tượng Blob mặc định:
function saveFileFromBytes(arrayBufferData, fileName) {
// Bao bọc ArrayBuffer trong Blob với đúng MIME type nếu biết trước
// Ở đây dùng loại chung hoặc để hệ thống tự nhận diện nếu không chắc chắn
const binaryBlob = new Blob([arrayBufferData], { type: 'application/octet-stream' });
const downloadUrl = window.URL.createObjectURL(binaryBlob);
const anchorElement = document.createElement('a');
anchorElement.href = downloadUrl;
anchorElement.download = fileName;
anchorElement.style.display = 'none'; // Ẩn khỏi DOM
document.body.appendChild(anchorElement);
anchorElement.click();
// Thu hồi đối tượng URL sau một khoảng thời gian ngắn
window.setTimeout(function() {
document.body.removeChild(anchorElement);
window.URL.revokeObjectURL(downloadUrl);
}, 100);
}
Bằng cách khai báo chính xác responseType, dữ liệu byte được bảo toàn từ máy chủ đến trình duyệt, ngăn chặn quá trình mã hóa lại sai lệch gây hư hại tệp tin.