.触发下载行为
async function downloadFile(url) { try { // 堆代码 duidaima.com const response = await fetch(url); const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = downloadUrl; a.download = 'file'; // 默认文件名 document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(downloadUrl); document.body.removeChild(a); } catch (error) { console.error('下载失败:', error); } }2.2 存在的问题
.缺乏错误处理
Content-Disposition: attachment; filename="example.pdf"中文名时需编码下:
const filename = 'duidaima.xlsx'; // 设置响应头 ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); const encodedFilename = encodeURIComponent(filename); ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${encodedFilename}`);说明:
// 通过contentDisposition 获取文件名 function getFilenameFromContentDisposition(contentDisposition) { if (!contentDisposition) { return null } const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/ const matches = filenameRegex.exec(contentDisposition) if (matches && matches[1]) { return matches[1].replace(/['"]/g, "").replace("UTF-8", "") } return null }四.处理特殊情况
function decodeFilename(filename) { // 处理UTF-8编码的文件名 if (filename.includes("%")) { try { return decodeURIComponent(filename) } catch (e) { return filename } } // 处理ISO-8859-1编码的情况 if (/=\?/.test(filename)) { try { return decodeURIComponent(escape(filename)) } catch (e) { return filename } } return filename }五.完整示例
/** * 获取 blob * @param {String} url 目标文件地址 * @return {Promise} */ export function getBlob(url) { return new Promise(resolve => { const xhr = new XMLHttpRequest() url = url + `?r=${Math.random()}` xhr.open("GET", url, true) xhr.responseType = "blob" xhr.onload = () => { if (xhr.status === 200) { resolve(xhr.response) } } xhr.send() }) } /** * 保存 * @param {Blob} blob * @param {String} fileName 想要保存的文件名称 * @param {String} type 想要保存的文件名称 */ export function downloadFileByBlob(data, fileName, type) { let options = {} if (type) { options = { type: `application/${type}` } } const blob = new Blob([data], options) if ("download" in document.createElement("a")) { // 非IE下载 const alink = document.createElement("a") alink.download = fileName alink.style.display = "none" alink.href = URL.createObjectURL(blob) document.body.appendChild(alink) alink.click() URL.revokeObjectURL(alink.href) // 释放URL 对象 document.body.removeChild(alink) } else { // IE10+下载 navigator.msSaveBlob(blob, fileName) } } /** * 下载 * @param {String} url 目标文件地址 * @param {String} filename 想要保存的文件名称 */ export function downloadFileByUrl(url, filename, type) { getBlob(url).then(blob => { downloadFileByBlob(blob, filename, type) }) } // 通过contentDisposition 获取文件名 export function getFilenameFromContentDisposition(contentDisposition) { if (!contentDisposition) { return null } const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/ const matches = filenameRegex.exec(contentDisposition) if (matches && matches[1]) { return decodeFilename(matches[1].replace(/['"]/g, "").replace("UTF-8", "")) } return null } function decodeFilename(filename) { // 处理UTF-8编码的文件名 if (filename.includes("%")) { try { return decodeURIComponent(filename) } catch (e) { return filename } } // 处理ISO-8859-1编码的情况 if (/=\?/.test(filename)) { try { return decodeURIComponent(escape(filename)) } catch (e) { return filename } } return filename }5.2 axios.js响应拦截中设置文件名
import { getFilenameFromContentDisposition } from "@/utils/download" ..... // respone拦截器 service.interceptors.response.use( async(response) => { const { data, config, status, request } = response // 如果自定义代码不是200,则判断为错误。 if (status !== 200) { modal.msgError(data.message || "Error") return { ...data, config, responseErr: true } } else { // 二进制数据则直接返回 if ( request.responseType === "blob" || request.responseType === "arraybuffer" ) { // 获取 Content-Disposition 头 const contentDisposition = response.headers["content-disposition"] // 解析文件名 const fileName = getFilenameFromContentDisposition(contentDisposition) // 默认文件名 为空 if (fileName) { // 存储文件名 用于设置和后端一样的文件名 localStorage.setItem("downloadFileName", fileName) } return response } if (data.code === 200) { return { ...data, config } } if (data.code === 401 || data.code === 403) { return { ...data, config, responseErr: true } } // 用户权限变更 if (data.code === 402) { if (isShowingModal) { return { ...data, config, responseErr: true } } isShowingModal = true modal .confirm(data.message, { type: "warning", showClose: false }) .then(async() => { // 刷新去首页吧 window.location.href = window.location.origin + window.location.pathname }) .catch(async(err) => { // 关闭 MessageBox 后,手动移除 不能关闭的遮罩层 const maskModal = document.querySelector(".v-modal") if (maskModal) { maskModal.remove() } await store.dispatch("user/getUserInfo") isShowingModal = false }) return { ...data, config, responseErr: true } } modal.msgError(data.message || "Error") return { ...data, config, responseErr: true } } }, (error) => { // 加入try 否则 会导致 取消重复请求时 因为无config等字段导致报错 导致取消重复请求失效 try { const { config, headers, status } = error.response if (status === 520) { // 导出文件接口报错时与后端约定 返回状态码为 520 // 约定好响应头添加 x-custom-header 且值为原本报错的 code const code = Number(headers["x-custom-header"]) console.log("🚀 ~ code:", code) // 当code为401 或者 403时 触发刷新token接口或者弹框提示过期重新登录 if (code === 401 || code === 403) { return { code, message: "凭证已过期,请重新登录!", config, responseErr: true } } else { modal.msgError(code === 409 ? "无权限" : error.message || "Error") return Promise.reject(error) } } } catch (err) { console.log("🚀 ~ err:", err) } if (error.name !== "CanceledError") { modal.msgError(error.message || "Error") } return Promise.reject(error) } )代码解释,如果返回内容 是blob文件,则获取其响应头设置的文件名,将文件名存储到浏览器缓存。
export function exportUserByQuery(data = {}, config = { preventDuplicateRequestsType: "prevent", responseType: "blob" // blob 或 arraybuffer }) { return axiosRequest.post(`url`, data, config) }5.4 页面中下载
import { downloadFileByBlob } from "@/utils/download" ...... userApi.exportUserByQuery(this.queryParams).then((data) => { // 从缓存中获取文件名 没有则设置 默认文件名 const fileName = localStorage.getItem("downloadFileName") || "用户数据.xlsx" downloadFileByBlob(data, fileName, "octet-stream") localStorage.removeItem("downloadFileName") // 清除缓存 this.$modal.msgSuccess("导出用户数据成功") this.openExportWay = false })总结