Можно ли сохранить файл непосредственно из веб-воркера?

У меня есть приложение, полностью основанное на браузере (т.е. без бэкэнда), которое анализирует XML-данные в файлах, каждый из которых в среднем составляет около 250 МБ. Фактический синтаксический анализ и синтаксический анализ происходят в веб-воркере, который получает данные порциями по 64 КБ от экземпляра FileReader, и все это довольно производительно.

У меня есть запрос от клиента на расширение этого приложения, чтобы оно могло создавать ZIP-файл, содержащий исходный входной файл и результаты анализа, и позволять пользователю сохранять этот файл на своем локальном компьютере. Создать в памяти ZIP-файл с этим содержимым не проблема. Проблема заключается в передаче большого количества данных из веб-работника, который их генерирует, обратно в основной поток браузера, чтобы их можно было сохранить; попытка сделать это неизменно вызывает сбой или исключение нехватки памяти. (Я пробовал передавать строки сразу и по частям, а также пытался использовать ArrayBuffer в качестве передаваемого объекта, чтобы избежать копирования. Все терпят неудачу одинаково.)

К сожалению, я не знаю способа вызвать операцию сохранения файла непосредственно из рабочего потока. Я знаю несколько способов сделать это из основного потока браузера, но все они требуют либо возможности создавать узлы DOM (что, конечно же, не могут сделать рабочие потоки), либо использования интерфейсов (например, msSaveBlob, saveAs), которые не браузер, кажется, подвергает рабочему потоку. Я потратил некоторое время на поиск возможностей в Интернете, но не нашел ничего полезного; FileWriterSync выглядит неплохо, но его поддерживает только Chrome, а мне нужно ориентироваться также на IE и Firefox.

Есть ли способ, который я упустил из виду, для сохранения файлов непосредственно из веб-воркера? Если так, то, что это? Или мне просто не повезло здесь?


person Aaron Miller    schedule 05.04.2016    source источник
comment
Меня удивляет, что перенос (не копирование) существующего ArrayBuffer из рабочего потока в основной поток вызывает проблемы. Можете ли вы опубликовать минимальный пример, который делает это?   -  person Michal Charemza    schedule 17.04.2016
comment
@MichalCharemza Меня это тоже удивило. Экспериментальный код, в котором я тестировал его, слишком глубоко встроен в (внутреннее, проприетарное) приложение, чтобы упростить публикацию примера, но написать его не должно быть слишком сложно; в конце концов, вы можете просто сгенерировать 250M ArrayBuffer в рабочем потоке и попытаться передать его в основной поток. Это надежно провоцировало исключения OOM или сбои в IE 11, Firefox 38 и Chrome 49 во время моего тестирования.   -  person Aaron Miller    schedule 27.04.2016
comment
вы нашли какой-нибудь способ?   -  person Amritesh Anand    schedule 25.05.2017
comment
@AmriteshAnand Не совсем. Я дошел до того, что смог передать данные обратно в основной поток, но не нашел надежного варианта для фактической записи их на диск - все, что я пробовал, включает преобразование буфера в строку, которая всегда вылетает из браузера из-за нехватки памяти.   -  person Aaron Miller    schedule 03.06.2017


Ответы (1)


демонстрация tl;dr

Вам вообще не нужно копировать весь файл на стороне клиента. Вам даже не нужно передавать его, на самом деле. Сначала резюме.

Вот как создать Blob из некоторого типизированного массива:

// Some arbitrary binary data
const mydata = new Uint16Array([1,2,3,4,5]);
// mydata vs. mydata.buffer does not seem to make any difference
const blob = new Blob([mydata], {type: "octet/stream"});

Вы можете создать URL-адрес объекта, который является копией исходного Blob, управляемого браузером и доступного как URL-адрес. Я сделал это с огромными файлами, не заметив влияния на производительность:

const url = URL.createObjectURL(blob);

Вот как я обычно загружаю URL-адреса:

const link = document.createElement("a");
link.download = "data.bin";
link.href = e.data.link;
link.appendChild(new Text("Download data"));
link.addEventListener("click", function() {
    this.parentNode.removeChild(this);
    // remember to free the object url, but wait until the download is handled
    setTimeout(()=>{URL.revokeObjectURL(e.data.link);}, 500)
});
document.body.appendChild(link);

Вы можете запустить загрузку автоматически, вызвав событие click по этой ссылке. Я предпочитаю, чтобы пользователь сам решал, когда загружать.

Итак, все вместе:

worker.js

// Some arbitrary binary data
const mydata = new Uint16Array([1,2,3,4,5]);

self.onmessage = function(e) {
  console.log("Message: ",e.data)
  switch(e.data.name) {
    case "make-download" : 
        const blob = new Blob([mydata.buffer], {type: "octet/stream"});
        const url = URL.createObjectURL(blob);
        self.postMessage({name:"download-link", link:url});
    break;
    default:
      console.error("Unknown message:", e.data.name);
  }
}

main.js

var worker = new Worker("worker.js");
worker.addEventListener("message", function(e) {
  switch(e.data.name) {
    case "download-link" : {
       if(e.data.error) {
          console.error("Download error: ", e.data.error);
       }
       else {
          const link = document.createElement("a");
          link.download = "data.bin";
          link.href = e.data.link;
          link.appendChild(new Text("Download data"));
          link.addEventListener("click", function() {
              this.parentNode.removeChild(this);
              // remember to free the object url, but wait until the download is handled
              setTimeout(()=>{URL.revokeObjectURL(e.data.link);}, 500)
          });
          document.body.appendChild(link);
       }
       break;
    }
  default:
    console.error("Unknown message:", e.data.name);
  }
});

function requestDownload() {
  worker.postMessage({name:"make-download"});
}

Когда я нажимаю «Загрузить» в своей демонстрации, я вижу это в своем HEX-редакторе:

введите здесь описание изображения

Выглядит просто отлично :)

person Tomáš Zato - Reinstate Monica    schedule 06.09.2019
comment
Для менее сложной реализации main.js вы можете использовать FileSaver.js и вызвать его saveAs в URL-адресе большого двоичного объекта :). Это делает реализацию однострочной и обеспечивает некоторую степень обратной совместимости (по крайней мере, я на это надеюсь). - person zamber; 17.12.2019