Chrome FileReader 对于大文件 (>= 300MB) 返回空字符串

tmu*_*sch 7 javascript google-chrome filereader

目标:

  • 在浏览器中,从用户文件系统中读取文件作为 base64 字符串
  • 这些文件最大为 1.5GB

问题:

  • 以下脚本在 Firefox 上运行得非常好。与文件大小无关。
  • 在 Chrome 上,该脚本适用于较小的文件(我测试过约 5MB 大小的文件)
  • 如果您选择更大的文件(例如 400MB),FileReader 将完成,不会出现错误或异常,但返回空字符串而不是 base64 字符串

问题:

  • 这是 Chrome 的错误吗?
  • 为什么既没有错误也没有异常?
  • 我该如何解决或解决这个问题?

重要的:

请注意,分块对我来说不是一个选项,因为我需要通过“POST”将完整的 base64 字符串发送到不支持分块的 API。

代码:

'use strict';

var filePickerElement = document.getElementById('filepicker');

filePickerElement.onchange = (event) => {
  const selectedFile = event.target.files[0];
  console.log('selectedFile', selectedFile);

  readFile(selectedFile);
};

function readFile(selectedFile) {
  console.log('START READING FILE');
  const reader = new FileReader();

  reader.onload = (e) => {
    const fileBase64 = reader.result.toString();

    console.log('ONLOAD','base64', fileBase64);
    
    if (fileBase64 === '') {
      alert('Result string is EMPTY :(');
    } else {
        alert('It worked as expected :)');
    }
  };

  reader.onprogress = (e) => {
    console.log('Progress', ~~((e.loaded / e.total) * 100 ), '%');
  };

  reader.onerror = (err) => {
    console.error('Error reading the file.', err);
  };

  reader.readAsDataURL(selectedFile);
}
Run Code Online (Sandbox Code Playgroud)
<!doctype html>
<html lang="en">

<head>
  <!-- Required meta tags -->
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <!-- Bootstrap CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">

  <title>FileReader issue example</title>
</head>

<body>

  <div class="container">
    <h1>FileReader issue example</h1>
    <div class="card">
      <div class="card-header">
        Select File:
      </div>
      <div class="card-body">
        <input type="file" id="filepicker" />
      </div>
    </div>

  </div>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-p34f1UUtsS3wqzfto5wAAmdvj+osOnFyQFpp4Ua3gs/ZVWx6oOypYoCJhGGScy+8"
    crossorigin="anonymous"></script>
  <script src="main.js"></script>
</body>

</html>
Run Code Online (Sandbox Code Playgroud)

Kai*_*ido 4

\n

这是 Chrome 的错误吗?

\n
\n

正如我在对Chrome, FileReader API, event.target.result === ""的回答中所说,这是 V8(Chrome 的,但也包括 Node-js 和其他 JavaScript JS 引擎)的限制。
\n这是故意的,因此不能真正称为“错误”。
\n技术细节是,这里实际失败的是在 64 位系统上构建一个超过 512MB(减去标头)的字符串,因为在 V8 中,所有堆对象必须适合 Smi(小整数)(参见此提交)。

\n
\n

为什么既没有错误也没有异常?

\n
\n

这可能是一个错误......正如我在链接的答案中所示的那样,直接创建这样的字符串时我们会得到一个 RangeError :

\n

\r\n
\r\n
const header = 24;\nconst bytes = new Uint8Array( (512 * 1024 * 1024) - header );\nlet txt = new TextDecoder().decode( bytes );\nconsole.log( txt.length ); // 536870888\ntxt += "f"; // RangeError
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

在FileReader::readOperation的第 3 步中,UA 必须

\n
\n

如果包数据抛出异常错误:

\n
    \n
  • 将 fr\xe2\x80\x99s 错误设置为错误。
  • \n
  • 在 fr 处触发一个名为 error 的进度事件。
  • \n
\n
\n

但在这里,我们没有这个错误。

\n

\r\n
\r\n
const bytes = Uint32Array.from( { length: 600 * 1024 * 1024 / 4 }, (_) => Math.random() * 0xFFFFFFFF );\nconst blob = new Blob( [ bytes ] );\nconst fr = new FileReader();\nfr.onerror = console.error;\nfr.onload = (evt) => console.log( "success", fr.result.length, fr.error );\nfr.readAsDataURL( blob );
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

我将提出一个关于此问题的问题,因为您应该能够处理来自 FileReader 的错误。

\n
\n

我该如何解决或解决这个问题?

\n
\n

最好的肯定是让您的 API 端点直接接受二进制资源而不是 data:// URL,无论如何都应该避免这样做。

\n

如果这不可行,“未来”的解决方案是将 ReadableStream POST 到您的端点,并在来自 Blob 的流上自行进行 data:// URL 转换。

\n
class base64StreamEncoder {\n  constructor( header ) {\n    if( header ) {\n      this.header = new TextEncoder().encode( header );\n    }\n    this.tail = [];\n  }\n  transform( chunk, controller ) {\n    const encoded = this.encode( chunk );\n    if( this.header ) {\n      controller.enqueue( this.header );\n      this.header = null;\n    }\n    controller.enqueue( encoded );\n  }\n  encode( bytes ) {\n    let binary = Array.from( this.tail )\n        .reduce( (bin, byte) => bin + String.fromCharCode( byte ), "" );\n    const tail_length = bytes.length % 3;\n    const last_index = bytes.length - tail_length;\n    this.tail = bytes.subarray( last_index );\n    for( let i = 0; i<last_index; i++ ) {\n        binary += String.fromCharCode( bytes[ i ] );\n    }\n    const b64String = window.btoa( binary );\n    return new TextEncoder().encode( b64String );\n  }\n  flush( controller ) {\n    // force the encoding of the tail\n    controller.enqueue( this.encode( new Uint8Array() ) );\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

实例: https: //base64streamencoder.glitch.me/

\n

目前,您必须将 base64 表示的块存储在 Blob 中,如 Endless 的答案所示。

\n

但请注意,由于这是 V8 的限制,即使服务器端也可能面临如此大的字符串问题,所以无论如何,您应该联系 API 的维护人员。

\n