我正在制作一个类似于 https://hexed.it/ 的二进制文件编辑器。我希望应用程序能够处理大文件(数 GB)而无需长时间等待。
用户打开文件(blob)的初始代码:
const domFile = input?.files?.[0];
if (!domFile){
return;
}
const name = domFile.name;
const buffer = await domFile.arrayBuffer();
const file: File = {
name,
buffer
}
state.files.push(file);
state.currentFile = file;
有问题的线路是这样的:
const buffer = await domFile.arrayBuffer();
问题是,即使我没有渲染整个文件,UI 也会在整个文件加载后才会更新。
渲染器在任何给定时间仅在 dom 中渲染 50 行。这意味着我只需要缓冲区中的 50*16=800 字节来渲染文件的特定部分。这 50 条线段现在将被称为“窗口”形式。
加载整个缓冲区后,我可以对其进行随机访问并渲染当前窗口,效果很好。
但是,当打开大到 7 GB 的文件时,程序会暂停大约一分钟来读取整个缓冲区,这并不理想。
目前的解决方案:
const domFile = input?.files?.[0];
if (!domFile){
return;
}
const name = domFile.name;
const buffer = new ArrayBuffer(domFile.size);
const file: File = {
name,
buffer
}
state.files.push(file);
state.currentFile = file;
const queuingStrategy = new CountQueuingStrategy({ highWaterMark: 1024 });
let bytesWritten = 0;
const view = new Uint8Array(buffer);
const writableStream = new WritableStream(
{
write(chunk) {
console.log(bytesWritten,chunk);
return new Promise((resolve, reject) => {
requestAnimationFrame(()=>{
view.set(chunk,bytesWritten);
bytesWritten+=chunk.length;
resolve();
})
});
},
close() {
console.log("done");
},
abort(err) {
console.error("Sink error:", err);
},
},
queuingStrategy,
);
domFile.stream().pipeTo(writableStream);
使用流 API,我可以在查看器中打开文件,而无需等待读取所有字节。这稍微好一些,因为我不必等待一分钟来渲染文件的开头,但如果我想查看文件的结尾,我仍然需要等待整个文件加载,因为这会加载以顺序的方式访问所有字节,而不是仅随机访问当时需要的字节。
在 hexedit 上,我可以打开同一个文件并滚动到末尾,而无需等待整个文件加载。遗憾的是 hexedit 不是开源的,所以我不知道他们是如何实现的。
TLDR:如何从 blob 中随机访问 800 字节的窗口,而不首先将整个文件加载到 ArrayBuffer 中。
在创建
Blob
之前,您可以直接使用 .slice()
(File
是 Blob
)随机访问 ArrayBuffer
:
const input = document.querySelector('#upload')
const output = document.querySelector('#output');
const fileLabel = document.querySelector('#fileLabel');
const previousButton = document.querySelector('#previous');
const nextButton = document.querySelector('#next');
let domFile = null;
let offset = 0;
const windowSize = 50;
const columns = 5;
function toBinaryString (bytes) {
const binaryString = bytes.reduce((str, byte, index) => {
const separator = index % columns === columns - 1 ? '\n' : ' ';
return str + byte.toString(2).padStart(8, '0') + separator;
}, '');
return binaryString;
}
async function showSlice () {
output.innerText = 'Sampling...';
const windowEnd = Math.min(domFile.size, offset + windowSize);
const sampleBlob = domFile.slice(offset, windowEnd);
const sampleBuffer = await sampleBlob.arrayBuffer();
const binaryString = toBinaryString(new Uint8Array(sampleBuffer));
output.innerText = binaryString;
}
function handleUpdate () {
domFile = input?.files?.[0];
if (!domFile){
previousButton.disabled = true;
nextButton.disabled = true;
fileLabel.innerText = 'No file selected';
output.innerText = '(select a file to see slices)';
return;
}
// Prevent reading before / after the end of the file:
offset = Math.max(0, offset);
if (offset + windowSize >= domFile.size) {
offset = windowSize * Math.floor(domFile.size / windowSize);
}
const windowEnd = Math.min(domFile.size, offset + windowSize);
// Update the UI:
fileLabel.innerText = 'Showing bytes ' + offset + ' to ' + windowEnd + ' of ' + domFile.name;
previousButton.disabled = offset === 0;
nextButton.disabled = offset + windowSize >= domFile.size;
// Show the data as a binary string:
showSlice();
}
input.addEventListener('change', () => {
offset = 0;
handleUpdate();
});
previousButton.addEventListener('click', () => {
offset -= windowSize;
handleUpdate();
});
nextButton.addEventListener('click', () => {
offset += windowSize;
handleUpdate();
});
// Init the UI
handleUpdate();
<input type="file" id="upload">
<button id="previous" disabled><</button>
<button id="next" disabled>></button>
<p id="fileLabel"></p>
<pre id="output"></pre>