# 前端文件下载

先看结论

前端文件下载主要有两种方式。

# 直接下载

下载静态资源或者 api 返回文件流,对于异常处理没有什么要求的,都可以使用直接下载的方式。但是仅支持浏览器无法识别的文件。如果是浏览器支持的文件格式(如:txt、jpg、png、mp3),就不会触发下载,而是直接用浏览器打开文件。

  1. 使用 <a> 标签下载,download 同源可以指定文件名(IE 不支持)。
<a href="/static/example.json" download="A.json">
1
  1. 使用 js 直接下载,主要有两种方式 window.location.hrefwindow.open。都不能指定文件名。这两种方式默认情况下表现形式基本相同。
const URL = '/static/example.json'

// 方式1
window.location.href = URL;

// 方式2
window.open(URL)
1
2
3
4
5
6
7
  1. 使用 iframe 下载文件。缺点没法处理错误情况。

需要注意,当下载发生错误的时候(被删除):

  • <a> 当前页跳转到错误页;
  • window.location.href 当前页跳转到错误页;
  • window.open 在新的窗口打开错误页;

对于浏览器可以识别的文件格式(txt、jpg、png、mp3),可以通过设置 Content-Disposition,告诉浏览器资源应该被下载到本地。

在常规的 HTTP 响应中,Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

Content-Disposition: attachment; filename="filename.jpg"
1

# 请求 API 下载

如果需要处理异常情况,直接下载的方式就不适用了。就需要通过请求 API 返回文件再下载,并处理异常情况。

axios 栗子,通过设置 responseType: 'blob' 来指定返回响应数据类型为 blob,然后下载:

import axios from 'axios';

axios({
  method:'get',
  url: '/download/example.json',
  responseType: 'blob', // 关键
}).then(res => {
  if (res.status === 200){
    downloadFile(res.data, 'A.json');
  }
}).catch(e => {
  console.warn(e);
  // 处理异常情况
  blobToJSON(e.data).then(res => {
    alert(res.message);
  });
});

function downloadFile (data, name = 'download', type = '') {
  if (!(data instanceof Blob)) {
    try {
      data = new Blob([data], { type });
    } catch (e) {
      console.warn('[Download] Unsupported file type');
    }
  }
  // 兼容IE
  if (window.navigator && window.navigator.msSaveBlob) {
    window.navigator.msSaveBlob(data, name);
    return;
  }
  // 使用 a 标签下载
  const a = document.createElement('a');
  a.download = name;
  a.style.display = 'none';
  a.href = window.URL.createObjectURL(data);
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  window.URL.revokeObjectURL(a.href);
}

function blobToJSON (blob) {
  if (!(blob instanceof Blob)) {
    console.warn('[Download] not blob');
    return Promise.resolve(blob);
  }
  return new Promise(resolve => {
    const reader = new FileReader();
    reader.onload = (e) => {
      if (blob.type === 'application/json') {
        resolve(JSON.parse(e.target.result));
      } else {
        resolve({ result: e.target.result });
      }
    };
    reader.readAsText(blob);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

responseType

responseType 属性允许我们手动的设置返回数据的类型。如果我们将它设置为一个空字符串,它将使用默认的 text 类型。

responseType值 xhr.response 数据类型 说明
"" DOMString 为空字符串时,采用默认类型,与 text 相同
"text" DOMString
"document" Document 对象 希望返回 HTML/XML 格式数据时使用
"json" javascript 对象 存在兼容性问题,IE 不支持
"blob" Blob 对象
"arraybuffer" ArrayBuffer 对象

arraybuffer

下载时也可以设置 responseType: 'arraybuffer',除了下载返回数据的时候先转成 blob 对象再下载,异常处理时 arraybuffer 转成 JSON 对象,其它表现 blob 一致(暂时没有找到区别)。

// arraybuffer 转 blob
function arraybufferToBlob(data, type) {
  try {
    return new Blob([data], { type });
  } catch (e) {
    console.warn(e);
  }
}
// arraybuffer 转 json
function arraybufferToJSON (data) {
  try {
    const encode = new TextDecoder('utf-8'); // IE 不支持,引入第三方库或者先转 blob 再转 JSON。
    return JSON.parse(encode.decode(new Uint8Array(data)));
  } catch (e) {
    console.warn('[Download] arraybuffer parse error');
    return { result: data };
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • ArrayBuffer(又称类型化数组):用来表示通用的、固定长度的原始二进制数据缓冲区。你不能直接操纵 ArrayBuffer 的内容,而是需要创建一个类型化数组对象或 DataView 对象,该对象以特定格式表示缓冲区,并使用该对象读取和写入缓冲区的内容。
  • Blob(Binary Large Object):二进制大数据对象,表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。

# 文件名编码问题

通过 Content-Disposition (opens new window) 可以定义文件名。

Content-Disposition: attachment;
                     filename="fname";
                     filename*=utf-8''encoded_fname
1
2
3

fname 是文件名字符串,不需要编码, encoded_fname 是采用了 RFC 5987 (opens new window)中定义的编码规则(URL编码)处理过的文件名。

Producers MUST use either the "UTF-8" ([RFC3629]) or the "ISO-8859-1" ([ISO-8859-1]) character set.

当二者同时出现的时候,应该优先采用 filename*

栗子: 文件名 한.txt

Content-Disposition

filename* 是用单引号作为分隔符,将等号右边分成了三部分:第一部分是字符集(utf-8),中间部分是语言(未填写),最后的%ED%95%9C.txt代表了实际值。

A single quote is used to separate the character set, language, and actual value information in the parameter value string, and an percent sign is used to flag octets encoded in hexadecimal.

# 文件名长度限制

Linux文件名的长度限制是 255 个字符
windows下完全限定文件名必须少于 260 个字符,目录名必须小于 248 个字符

关于文件名长度限制,windows 上实测文件名最多244个字符。 文件名超长,浏览器下载时会自动截取

  • IE:186
  • chrome:230
  • firefox:225
  • safari:227

safari 表现比较特殊,对 Content-Disposition 整体长度有限制,不能超过 255 个字节,filename* 不会解析可以直接使用非 ASCII 字符。一个中文占 3 个字节。所以大概可以支持 73 个汉字,如果要使用 URL 编码,大概只能支持 25 个汉字(不含后缀)。

摘自 safari 社区

safari feat

其它应用处理文件名长度情况:

  • 微云:93
  • csdn:80

# base64 转 blob

function base64ToBlob (code) {
    let parts = code.split(';base64,');
    let contentType = parts[0].split(':')[1];
    let raw = window.atob(parts[1]);
    let rawLength = raw.length;
    let uInt8Array = new Uint8Array(rawLength);
    for(let i = 0; i < rawLength; ++i) {
        uInt8Array[i] = raw.charCodeAt(i);
    }
    return new Blob([uInt8Array], {
        type: contentType
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 结论

  • 主要看是否需要处理异常情况,来决定是直接下载还是请求 api 下载
  • 直接通过 a 标签或者 location.href 下载,一般没有编码问题;
  • 需要处理异常,则通过 js 设置请求参数 responseType: 'blob' 或者 responseType: 'arraybuffer',拿到返回文件再下载
    1. 异常处理的时候返回对象也是 blob 或者 arraybuffer,需要转换;
  • 如果用户设置文件名(含非 ASCII 字符)长度最好不超过 73,具体处理方案:
    1. api 使用 URL 编码处理文件名,前端解码(decodeURI)。注意:Safari 长度限制了 225 个字节左右(中文占 3 个字节,编码后占 9 个),名字全中文不超过 73 个字,支持非 ASCII 字符,直接下载不会解析文件名;
    2. api 不处理文件名,前端生成文件名或者替换非 ASCII 字符部分。注意:需要前后端统一文件名生成规则,长度 186 以内;
    3. 文件名使用英文。注意:长度 186 以内;
    4. 使用异步任务的方式下载,请求 api 后服务端生成文件,返回文件地址,前端直接下载。把异常处理放在请求文件地址那一步。注意:长度 186 以内,服务端需要存放文件,涉及有效期和隐私等;

# 解决方案

可选第三方库:

# 参考

上次更新: 12/23/2021, 11:24:33 AM