Xử lý tải xuống file stream trong Vue với tên file từ response.headers

#### Tải xuống file stream thông qua interceptor trong axios - Yêu cầu - Cách tiếp cận trước đây (viết hàm riêng tại từng module nghiệp vụ) - Cách tiếp cận hiện tại (xử lý统一 trong interceptor của axios) - Sao chép liên kết bài viết và gửi cho back-end để xem xét.

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ả decodeURIComponentdecodeURI đề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> objList = new ArrayList<>();
        DownLoadFileController addressService;
        List<NpColumns> npColumnsList = npColumnsService.findByTableCode("APP_TASK_CASE_INFO");
        for(NpColumns item : npColumnsList){
            Map tempMap = new HashMap<>();
            tempMap.put("id", item.getId());
            tempMap.put("tableCode", item.getTableCode());
            tempMap.put("columnName", item.getColumnName());
            objList.add(tempMap);
        }
        try {
            FileUtils.exportExcel(response,"Địa chỉ cây",titles,objList);
            return ResultGenerator.genSuccessResult("Xuất thành công!");
        }catch (Exception e){
            e.printStackTrace();
            return ResultGenerator.genFailResult("Xuất thất bại!");
        }
    }
Util class (không hiểu là gì)
public static void exportExcel(HttpServletResponse response,String fileName,String[] titles,List> result){
        HSSFWorkbook wb;
        OutputStream output = null;
        String tempName = fileName;
        try {
            Date date = new Date();
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
            fileName +="_"+df.format(date)+".xls";
            String encodedFilename = URLEncoder.encode(fileName, "UTF-8");
            wb= new HSSFWorkbook();
            HSSFSheet sh = wb.createSheet();
            // Cài đặt chiều rộng cột
            for(int i = 0; i < titles.length-1; i++){
                sh.setColumnWidth( i, 256*15+184);
            }
            // Hàng tiêu đề đầu tiên, CellRangeAddress: hàng đầu, hàng cuối, cột đầu, cột cuối
            HSSFRow row = sh.createRow(0);
            HSSFCell cell = row.createCell(0);
            cell.setCellValue(new HSSFRichTextString(tempName));
            //cell.setCellStyle(fontStyle(wb));
            sh.addMergedRegion(new CellRangeAddress(0, 0, 0,titles.length-1));
            // Hàng thứ hai
            HSSFRow row3 = sh.createRow(1);
            // Các cột hàng thứ hai
            for(int i=0; i < titles.length; i++){
                cell = row3.createCell(i);
                cell.setCellValue(new HSSFRichTextString(titles[i]));
                //cell.setCellStyle(fontStyle(wb));
            }
            // Điền dữ liệu, i là hàng, z là số lượng bản ghi, dùng để lặp
            int i = 2, z = 0;
            while (z < result.size()) {
                row = sh.createRow(i);
                Map map = result.get(z);
                for(int j=0;j < titles.length;j++) {
                    cell = row.createCell(j);
                    if(map.get(titles[j]) !=null) {
                        cell.setCellValue(map.get(titles[j]).toString());
                    }else {
                        cell.setCellValue("");
                    }
                }
                i++;
                z++;
            }
            output = response.getOutputStream();
            response.reset();
            response.addHeader("Content-Type","application/octet-stream;charset=utf-8");
            response.setHeader("Content-disposition", "attachment; filename="+encodedFilename);
            response.setContentType("application/msexcel");
            wb.write(output);
            output.flush();
            output.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
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é!

Thẻ: Vue.js Axios file-download Content-Disposition decodeURIComponent

Đăng vào ngày 14 tháng 9 lúc 03:50