将 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:
- 流媒体。使用此方法,文件将流式传输,因此我们可以避免多次设置php。
- 轻松加密。由于文件是通过
ArrayBuffer
发送的,因此开发人员可以在上传过程中轻松 Encrypt
文件。
此方法也支持上传任何类型的文件。我做了一些测试,我意识到 ReadAsArrayBuffer()
方法 比 ReadAsBinaryString()
和直接表单上传更快 。你可以试试。
安全通知
以上代码只是测试代码,要在生产中使用,您必须考虑在GET
或POST
中使用HTTPS发送数据。
我意识到新的 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:
- 流媒体。使用此方法,文件将流式传输,因此我们可以避免多次设置php。
- 轻松加密。由于文件是通过
ArrayBuffer
发送的,因此开发人员可以在上传过程中轻松Encrypt
文件。
此方法也支持上传任何类型的文件。我做了一些测试,我意识到 ReadAsArrayBuffer()
方法 比 ReadAsBinaryString()
和直接表单上传更快 。你可以试试。
安全通知
以上代码只是测试代码,要在生产中使用,您必须考虑在GET
或POST
中使用HTTPS发送数据。