将 FileReader ReadAsBinaryString() 迁移到 ReadAsArrayBuffer() 或 ReadAsText()

Migrate FileReader ReadAsBinaryString() to ReadAsArrayBuffer() or ReadAsText()

我意识到新的 Mozilla Firefox return allocation size overflow(在 FileReader.ReadAsBinaryString() 上)当文件大于 200MB(类似的东西)时。

这是我的一些客户端网络浏览器测试代码:

function upload(fileInputId, fileIndex)
{
    var file = document.getElementById(fileInputId).files[fileIndex];
    var blob;
    var reader = new FileReader();
    reader.readAsBinaryString(file); 
    reader.onloadend  = function(evt)
    {
        xhr = new XMLHttpRequest();

        xhr.open("POST", "upload.php", true);

        XMLHttpRequest.prototype.mySendAsBinary = function(text){
            var data = new ArrayBuffer(text.length);
            var ui8a = new Uint8Array(data, 0);
            for (var i = 0; i < text.length; i++){ 
                ui8a[i] = (text.charCodeAt(i) & 0xff);
            }

            if(typeof window.Blob == "function")
            {
                 blob = new Blob([data]);
            }else{
                 var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
                 bb.append(data);
                 blob = bb.getBlob();
            }

            this.send(blob);
        }

        var eventSource = xhr.upload || xhr;
        eventSource.addEventListener("progress", function(e) {
            var position = e.position || e.loaded;
            var total = e.totalSize || e.total;
            var percentage = Math.round((position/total)*100);
        });

        xhr.onreadystatechange = function()
        {
            if(xhr.readyState == 4)
            {
                if(xhr.status == 200)
                {
                    console.log("Done");
                }else{
                    console.log("Fail");
                }
            }
        };
        xhr.mySendAsBinary(evt.target.result);
    };
}

所以我试着把它改成FileReader.ReadAsArrayBuffer(),错误没有出现但是数据不一样(因为它不是作为二进制字符串读取的)。

有没有人有办法解决这个问题? raw/string 除了 FileReader 实现之外,还有什么方法可以将更大的文件从 JS 上传到 Web 服务器?

我在 Mozilla JS 文档中读到说:

This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future. - Mozilla

如果不是ReadAsBinaryString,如何实现ReadAsArrayBuffer或ReadAsText

经过漫长的一周研究和不眠之夜,您无法在不破坏二进制字符串的情况下上传二进制字符串,而且 base64 不适用于所有文件,仅适用于图像,从 client-side 到服务器中断正在发送的字节

要将文件发送到 web-server,您根本不需要 js。 HTML 单独使用 <form> 元素就可以做到这一点。

现在如果你想通过 js,例如捕获不同的 ProgressEvents,那么你可以直接发送你的文件,而不需要在你这边阅读它。

为此,您有两个 (或三个) 解。

如果您的服务器能够处理 PUT requests,您可以简单地 xhr.send(file);

否则,您将不得不经历 FormData

// if you really want to go the XHR way
document.forms[0].onsubmit = function handleSubmit(evt) {
  if(!window.FormData) { // old browser use the <form>
    return;
  }
  // now we handle the submit through js
  evt.preventDefault();
  var fD = new FormData(this);
  var xhr = new XMLHttpRequest();
  xhr.onprogress = function handleProgress(evt){};
  xhr.onload = function handleLoad(evt){};
  xhr.onerror = function handleError(evt){};
  xhr.open(this.method, this.action);
//  xhr.send(fD); // won't work in StackSnippet
  log(fD, this.method, this.action); // so we just log its content
};


function log(formData, method, action) {
  console.log('would have sent');
  for(let [key, val] of formData.entries())
    console.log(key, val);
  console.log('through', method);
  console.log('to', action);
}
<!-- this in itself is enough -->
<form method="POST" action="your_server.page">
  <input type="file" name="file_upload">
  <input type="submit">
</form>

现在,您发送了一条评论说您不能将大于 1GB 的文件上传到您的服务器。 这个限制只是由于你的服务器的配置,所以如果你想接受这么大的文件最好是正确配置它。

但是如果你真的想分块发送你的文件,即使那样也不要离开 Blob 接口。
事实上,Blobs 有一个 slice() 方法,所以使用它。

document.forms[0].onsubmit = function handleSubmit(evt) {
  evt.preventDefault();
  var file = this.elements[0].files[0];
  var processed = 0;
  if(file) {
//  var MAX_CHUNK_SIZE = Math.min(file.size, server_max_size);
    // for demo we just split in 10 chunks
    var MAX_CHUNK_SIZE = file.size > 10 ? (file.size / 10) | 0 : 1;
    loadChunk(0);
  }
  function loadChunk(start) {
    var fD = new FormData();
    var sliced = file.slice(start, start+MAX_CHUNK_SIZE);
    processed += sliced.size; // only for demo
    fD.append('file_upload', sliced, file.name);
    fD.append('starting_index', start);
    if(start + MAX_CHUNK_SIZE >= file.size) {
      fD.append('last_chunk', true);
    }
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'your_server.page');
    xhr.onload = function onchunkposted(evt) {
      if(start + MAX_CHUNK_SIZE >= file.size) {
        console.log('All done. Original file size: %s, total of chunks sizes %s', file.size, processed);
        return;
      }
      loadChunk(start + MAX_CHUNK_SIZE);
    };
//    xhr.send(fD);
    log(fD);
    setTimeout(xhr.onload, 200); // fake XHR onload
  }
};

    function log(formData, method, action) {
      console.log('would have sent');
      for(let [key, val] of formData.entries())
        console.log(key, val);
    }
<form method="POST" action="your_server.page">
  <input type="file" name="file_upload">
  <input type="submit">
</form>

但是您绝对不需要为此操作通过 FileReader。

实际上,对于某些 Android 不支持将 Blob 传递到 FormData 的浏览器来说,在这里使用 FileReader 唯一有意义的情况是,即使它们没有提供任何线索。
因此,在这种情况下,您必须设置服务器以让您知道请求为空,然后仅将文件读取为您将发送原始文件 in-place 的 dataURI。

凯多的说法是正确的

To send Files to a web-server, you simply don't need js

但这并没有回答我的问题。使用简单 XMLHttpRequest() 可以上传文件并跟踪这些进度。但是,事实并非如此。从 <form> 或使用 XMLHttpRequest() 直接上传将需要在 php 设置中增加 upload limit。这种方法对我来说不方便。如果客户端上传文件为 4GB 怎么办?所以我需要增加到 4GB。然后下一次,客户端上传文件为 6GB,那么我必须增加到 6GB。

使用slice() 方法对于更大的文件是有意义的,因为我们可以将它一部分一部分地发送到服务器。不过这次我还没用呢

这是我的一些测试,效果如我所愿。如果我错了,希望有高手指正。 我的 Upload.js

function upload(fileInputId, fileIndex)
{
    var file = document.getElementById(fileInputId).files[fileIndex];
    var blob;
    var reader = new FileReader();

    reader.readAsArrayBuffer(file);
    reader.onloadend  = function(evt)
    {
        xhr = new XMLHttpRequest();
        xhr.open("POST", "upload.php?name=" + base64_encode(file.name), true);

        XMLHttpRequest.prototype.mySendAsBinary = function(text){
            var ui8a = new Uint8Array(new Int8Array(text));

            if(typeof window.Blob == "function")
            {
                 blob = new Blob([ui8a]);
            }else{

                var bb = new (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder)();
                bb.append(ui8a);
                blob = bb.getBlob();
            }

            this.send(blob);
        }

        var eventSource = xhr.upload || xhr;
        eventSource.addEventListener("progress", function(e) {
            var position = e.position || e.loaded;
            var total = e.totalSize || e.total;
            var percentage = Math.round((position/total)*100);
            console.log(percentage);
        });

        xhr.onreadystatechange = function()
        {
            if(xhr.readyState == 4)
            {
                if(xhr.status == 200)
                {
                    console.log("Done");
                }else{
                    console.log("Fail");
                }
            }
        };

        xhr.mySendAsBinary(evt.target.result);
    };
}

下面是PHP服务器如何监听来自JS

ArrayBuffer
if(isset($_GET["name"])){
    $name = base64_decode($_GET["name"]);
    $loc = $name;
    $inputHandler = fopen('php://input', "r");
    $fileHandler = fopen($loc, "w+");

    while(true) {
        //$buffer = fgets($inputHandler, 1024);
        $buffer = fread($inputHandler, 1000000);

        if (strlen($buffer) == 0) {
            fclose($inputHandler);
            fclose($fileHandler);
            return true;
        }

        //$b = base64_encode($buffer);

        fwrite($fileHandler, $buffer);
    }
}

上述方法效果很好。 FileReader 读取文件作为 ArrayBuffer 上传到服务器。对我来说,从 ReadAsBinaryString() 迁移到 ReadAsArrayBuffer() 很重要,而且 ReadAsArrayBuffer()ReadAsBinaryString()

有更好的性能

这里有一些原因,为什么一些开发者依赖 FileReader API:

  1. 流媒体。使用此方法,文件将流式传输,因此我们可以避免多次设置php。
  2. 轻松加密。由于文件是通过 ArrayBuffer 发送的,因此开发人员可以在上传过程中轻松 Encrypt 文件。

此方法也支持上传任何类型的文件。我做了一些测试,我意识到 ReadAsArrayBuffer() 方法 ReadAsBinaryString() 和直接表单上传更快 。你可以试试。

安全通知

以上代码只是测试代码,要在生产中使用,您必须考虑在GETPOST中使用HTTPS发送数据。