如何在 JavaScript 中随机访问 blob 的内容而不先加载整个缓冲区?

问题描述 投票:0回答:1

我正在制作一个类似于 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 中。

javascript html
1个回答
0
投票

在创建

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>&lt;</button>
<button id="next" disabled>&gt;</button>

<p id="fileLabel"></p>
<pre id="output"></pre>

© www.soinside.com 2019 - 2024. All rights reserved.