axios.js實現文件下載功能

原文連接javascript

在開發中遇到了須要實現文件下載的功能,起初覺得只用<a>標籤就能搞定,<a>標籤確實可以搞定常見的場景。可是像導出或者在header裏面添加了特殊字段的時候,使用<a>標籤就搞不定了,又不想去使用原生XMLHttpRequest,由於又一堆的兼容性需求(技術能力不夠ε=ε=ε=┏(゜ロ゜;)┛,有現成的兼容方案爲啥要本身造輪子呢,說不定還爆胎>逃666),因此萌生基於Axios封裝。前端

Ajax沒法下載文件的緣由

瀏覽器的GET(frame、a)和POST(form)請求具備以下特色:java

  • response會交由瀏覽器處理
  • response內容能夠爲二進制文件、字符串等

Ajax請求具備以下特色:ios

  • response會交由Javascript處理
  • response內容僅能夠爲字符串

Ajax自己設計的目標就是用來獲取文本數據的,而不是用來搞二進制的。git

最近看文檔發現, xhr在老的瀏覽器裏面也是能夠發送二進制數據的,用到一個冷門的api XMLHttpRequest#overrideMimeType,能夠看這裏在老的瀏覽器中接受二進制數據注意兼容性github

XMLHttpRequest 2.0新增的數據類型Blob

看張老師的文章 理解DOMString、Document、FormData、Blob、File、ArrayBuffer數據類型 json

有了Blob類型以後,JavaScript處理二進制進一步加強,能夠說之後想怎樣就怎樣(廢話)。axios

文件下載實現

response header前提條件

服務端返回的頭部須要設置api

Content-Disposition: "attachment; filename=xxxx.docx;"瀏覽器

<a>標籤的直接下載

// Downloader.ts
import qs from 'qs';
/** * downloadByUrl * @param config - 配置參數 * @param config.url - 地址 * @param config.params - querystring參數. * @param filename 文件名稱,包括擴展名部分(不必定生效) * * @description * 原理是使用<a>的href和download屬性,因此filename不必定會生效, 瀏覽器機制問題. * * @see https://zhuanlan.zhihu.com/p/58888918 * @see https://github.com/kennethjiang/js-file-download */
export function downloadByUrl( config: { url: string; params: any; }, filename = '' ): void {
  var tempLink = document.createElement('a');
  tempLink.style.display = 'none';
  tempLink.href =
    config.url + qs.stringify(config.params, { addQueryPrefix: true });
  tempLink.setAttribute('download', filename);
  if (typeof tempLink.download === 'undefined') {
    tempLink.setAttribute('target', '_blank');
  }

  document.body.appendChild(tempLink);
  tempLink.click();
  document.body.removeChild(tempLink);
}
複製代碼

主要是使用了js-file-download的代碼,進行了簡單的封裝,並且去除了對Blob的依賴,主要爲了兼容低版本的瀏覽器。同時使用了qsquerystring參數進行了簡單的處理。

基於axios的實現

// Downloader.ts
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
import fileDownload from 'js-file-download';
import logger from 'js-logger';


/** * 提取文件名. * @param response axios的response * @description 從reponse header的content-disposition中提取文件名. */
const extractFilenameFromResponseHeader = (response: AxiosResponse): string => {
  // content-disposition: "attachment; filename=xxxx.docx;"
  const contentDisposition = response.headers['content-disposition'];
  const patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*');
  const result = patt.exec(contentDisposition) as RegExpExecArray;
  let filename = '';

  if (result) {
    filename = result.length > 0 ? result[1] : '';
  }
  // 解碼以前嘗試去除空格和雙引號
  // content-disposition: "attachment; filename=\"xxxx.docx\";"
  return decodeURIComponent(filename.trim().replace(new RegExp('"', 'g'), ''));
};


const axiosInstance = axios.create({/* 能夠傳遞公共默認的axios配置,可是注意reponse interceptor中默認把reponse.data做爲JSON解析的狀況 */});

// https://www.zhihu.com/question/263323250
// https://github.com/axios/axios/issues/815#issuecomment-340972365
const downloadByAxios = async function ( config: AxiosRequestConfig, filename = '' ): Promise<any | AxiosResponse<any>> {
  let response = await axiosInstance({
    ...config,
    responseType: 'blob', // 指定類型
  });

  let resBlob = response.data; // <--- store the blob if it is
  let respData = null;

  // 若是肯定接口response.data是二進制,因此請求失敗時是JSON.
  // 這裏只對response.data作JSON的嘗試解析
  try {
    let respText = await new Promise((resolve, reject) => {
      let reader = new FileReader();
      reader.addEventListener('abort', reject);
      reader.addEventListener('error', reject);
      reader.addEventListener('loadend', () => {
        resolve(reader.result as string);
      });
      reader.readAsText(resBlob);
    });
    respData = JSON.parse(respText as string); // <--- try to parse as json evantually
  } catch (err) {
    // ignore
  }
  // 若是response.data可以肯定是二進制,則respData = null說明請求成功
  // 不然 respData !== null說明請求失敗
  if (respData as ResponseData) {
    logger.error(respData);
      
    // 方便調用者有進一步的 then().catch()處理
    return Promise.reject({
      ...respData,
    });
  } else {
    // 觸發瀏覽器下載
    // 若是沒有傳遞filename嘗試從Content-Disposition提取
    fileDownload(resBlob, filename || extractFilenameFromResponseHeader(
      response
    ));
    // 方便調用者有進一步的 then().catch()處理
    return Promise.resolve({
      ...response,
    });
  }
};

複製代碼

代碼大部分都是參考這個issue實現的,只有少部分的我的代碼。

基於axios實現的功能:

  1. 可使用axios的全部參數,無論請求是GET或者POST
  2. 解決了在header中添加額外的參數的需求
  3. 能夠指定filename,若是服務端沒有設置content-disposition的狀況
  4. 返回Promise方便調用者進一步處理請求

缺點:

  1. 只能使用獨立的axios實例,不能公用一個axios

    原本想把下載功能使用axios interceptor攔截器實現,可是返回的response.dataBlob二進制,可是其它的response interceptor默認前提都是把response.data看成JSON處理,致使所有出現異常,因此把下載功能獨立出來,更方便維護。

  2. 使用獨立的axios實例,因此項目中的axios默認配置須要從新配置一遍

參考連接

Content-Disposition

axios.js實現下載功能

axios.js #815實現

StreamSaver

FileSaver

js-file-download 理解DOMString、Document、FormData、Blob、File、ArrayBuffer數據類型

歡迎加入羣聊

若是入羣失敗,添加我的微信,拉你入羣,驗證消息:前端交流

關注微信公衆號,發現更多精彩內容。

微信公衆號
相關文章
相關標籤/搜索