Синхронный API файловой системы для рабочих

Введение

API файловой системы HTML5 и веб-работники сами по себе чрезвычайно мощны. API FileSystem наконец-то обеспечивает иерархическое хранилище и файловый ввод-вывод в веб-приложениях, а Workers привносят настоящую асинхронную «многопоточность» в JavaScript. Однако, если вы используете эти API вместе, вы можете создать действительно интересные приложения.

В этом руководстве представлены руководство и примеры кода для использования файловой системы HTML5 внутри веб-работника. Предполагается практическое знание обоих API. Если вы не совсем готовы к погружению в эти API или хотите узнать больше об этих API, прочитайте два замечательных руководства, в которых обсуждаются основы: «Изучение API-интерфейсов файловой системы» и «Основы веб-работников» .

Синхронные и асинхронные API

Асинхронные API JavaScript могут быть сложными в использовании. Они большие. Они сложны. Но что больше всего расстраивает, так это то, что они открывают множество возможностей, чтобы что-то пойти не так. Последнее, с чем вам хотелось бы иметь дело, — это наложение сложного асинхронного API (Файловая система) в уже асинхронный мир (Работники)! Хорошей новостью является то, что API файловой системы определяет синхронную версию, чтобы облегчить работу веб-работников.

По большей части синхронный API точно такой же, как и его асинхронный родственник. Методы, свойства, возможности и функциональность будут вам знакомы. Основные отклонения:

  • Синхронный API можно использовать только в контексте веб-воркера, тогда как асинхронный API можно использовать как внутри рабочего, так и вне него.
  • Обратные звонки отключены. Методы API теперь возвращают значения.
  • Глобальные методы объекта окна ( requestFileSystem() resolveLocalFileSystemURL() ) становятся requestFileSystemSync() resolveLocalFileSystemSyncURL() .

За исключением этих исключений, API те же. Хорошо, мы готовы идти!

Запрос файловой системы

Веб-приложение получает доступ к синхронной файловой системе, запрашивая объект LocalFileSystemSync из веб-воркера. requestFileSystemSync() доступен глобальной области действия Worker:

var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);

Обратите внимание на новое возвращаемое значение теперь, когда мы используем синхронный API, а также на отсутствие обратных вызовов для успешных и ошибочных вызовов.

Как и в обычном API файловой системы, методы на данный момент имеют префикс:

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                 self.requestFileSystemSync;

Работа с квотой

В настоящее время невозможно запросить PERSISTENT квоту в контексте Worker. Я рекомендую позаботиться о проблемах с квотами за пределами Workers. Процесс может выглядеть примерно так:

  1. worker.js: оберните любой код API файловой системы в try/catch , чтобы перехватывать любые ошибки QUOTA_EXCEED_ERR .
  2. worker.js: если вы обнаружите QUOTA_EXCEED_ERR , отправьте postMessage('get me more quota') обратно в основное приложение.
  3. Основное приложение: выполните танец window.webkitStorageInfo.requestQuota() когда получен номер 2.
  4. Основное приложение: после того, как пользователь предоставит дополнительную квоту, отправьте postMessage('resume writes') обратно работнику, чтобы сообщить ему о дополнительном пространстве для хранения.

Это довольно сложный обходной путь, но он должен работать. Дополнительную информацию об использовании PERSISTENT хранилища с API файловой системы см. в разделе «Запрос квоты» .

Работа с файлами и каталогами

Синхронная версия getFile() и getDirectory() возвращает FileEntrySync и DirectoryEntrySync соответственно.

Например, следующий код создает пустой файл с именем «log.txt» в корневом каталоге.

var fileEntry = fs.root.getFile('log.txt', {create: true});

Следующее создает новый каталог в корневой папке.

var dirEntry = fs.root.getDirectory('mydir', {create: true});

Обработка ошибок

Если вам никогда не приходилось отлаживать код Web Worker, я вам завидую! Может быть очень трудно понять, что происходит не так.

Отсутствие обратных вызовов ошибок в синхронном мире делает решение проблем более сложным, чем должно быть. Если мы добавим общую сложность отладки кода Web Worker, вы сразу же разочаруетесь. Одна вещь, которая может облегчить жизнь, — это обернуть весь соответствующий код Worker в try/catch. Затем, если возникнут какие-либо ошибки, переправьте ошибку в основное приложение с помощью postMessage() :

function onError(e) {
    postMessage('ERROR: ' + e.toString());
}

try {
    // Error thrown if "log.txt" already exists.
    var fileEntry = fs.root.getFile('log.txt', {create: true, exclusive: true});
} catch (e) {
    onError(e);
}

Передача файлов, больших двоичных объектов и массивов-буферов

Когда веб-воркеры впервые появились на сцене, они позволяли отправлять только строковые данные в postMessage() . Позже браузеры начали принимать сериализуемые данные, что означало возможность передачи объекта JSON. Однако в последнее время некоторые браузеры, такие как Chrome, принимают более сложные типы данных для передачи через postMessage() с использованием алгоритма структурированного клонирования .

Что это на самом деле означает? Это означает, что передавать двоичные данные между основным приложением и рабочим потоком намного проще. Браузеры, поддерживающие структурированное клонирование для Workers, позволяют передавать Typed Arrays, ArrayBuffer , File или Blob в Workers. Хотя данные по-прежнему являются копией, возможность передачи File означает преимущество в производительности по сравнению с предыдущим подходом, который включал в себя базовую обработку файла перед передачей его в postMessage() .

В следующем примере выбранный пользователем список файлов передается выделенному работнику. Worker просто проходит через список файлов (просто показать, что возвращаемые данные на самом деле являются FileList ), а основное приложение считывает каждый файл как ArrayBuffer .

В образце также используется улучшенная версия метода встроенного Web Worker, описанного в разделе Основы Web Worker .

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <title>Passing a FileList to a Worker</title>
    <script type="javascript/worker" id="fileListWorker">
    self.onmessage = function(e) {
    // TODO: do something interesting with the files.
    postMessage(e.data); // Pass through.
    };
    </script>
</head>
<body>
</body>

<input type="file" multiple>

<script>
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    var files = this.files;
    loadInlineWorker('#fileListWorker', function(worker) {

    // Setup handler to process messages from the worker.
    worker.onmessage = function(e) {

        // Read each file aysnc. as an array buffer.
        for (var i = 0, file; file = files[i]; ++i) {
        var reader = new FileReader();
        reader.onload = function(e) {
            console.log(this.result); // this.result is the read file as an ArrayBuffer.
        };
        reader.onerror = function(e) {
            console.log(e);
        };
        reader.readAsArrayBuffer(file);
        }

    };

    worker.postMessage(files);
    });
}, false);


function loadInlineWorker(selector, callback) {
    window.URL = window.URL || window.webkitURL || null;

    var script = document.querySelector(selector);
    if (script.type === 'javascript/worker') {
    var blob = new Blob([script.textContent]);
    callback(new Worker(window.URL.createObjectURL(blob));
    }
}
</script>
</html>

Чтение файлов в Worker

Вполне приемлемо использовать асинхронный API FileReader для чтения файлов в Worker. Однако есть лучший способ. В Workers есть синхронный API ( FileReaderSync ), который упрощает чтение файлов:

Основное приложение:

<!DOCTYPE html>
<html>
<head>
    <title>Using FileReaderSync Example</title>
    <style>
    #error { color: red; }
    </style>
</head>
<body>
<input type="file" multiple />
<output id="error"></output>
<script>
    var worker = new Worker('worker.js');

    worker.onmessage = function(e) {
    console.log(e.data); // e.data should be an array of ArrayBuffers.
    };

    worker.onerror = function(e) {
    document.querySelector('#error').textContent = [
        'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('');
    };

    document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    worker.postMessage(this.files);
    }, false);
</script>
</body>
</html>

рабочий.js

self.addEventListener('message', function(e) {
    var files = e.data;
    var buffers = [];

    // Read each file synchronously as an ArrayBuffer and
    // stash it in a global array to return to the main app.
    [].forEach.call(files, function(file) {
    var reader = new FileReaderSync();
    buffers.push(reader.readAsArrayBuffer(file));
    });

    postMessage(buffers);
}, false);

Как и ожидалось, обратные вызовы исчезли с синхронным FileReader . Это упрощает количество вложенных обратных вызовов при чтении файлов. Вместо этого методы readAs* возвращают прочитанный файл.

Пример: получение всех записей

В некоторых случаях синхронный API намного чище для определенных задач. Меньшее количество обратных вызовов — это хорошо и, безусловно, делает информацию более читабельной. Реальный недостаток синхронного API связан с ограничениями Workers.

По соображениям безопасности данные между вызывающим приложением и потоком Web Worker никогда не передаются. Данные всегда копируются в Worker и из него при вызове postMessage() . В результате не каждый тип данных может быть передан.

К сожалению, FileEntrySync и DirectoryEntrySync в настоящее время не относятся к принятым типам. Так как же вернуть записи в вызывающее приложение? Один из способов обойти ограничение — вернуть список URL-адресов файловой системы вместо списка записей. filesystem: URL-адреса — это просто строки, поэтому их очень легко передавать. Более того, их можно преобразовать в записи в основном приложении с помощью resolveLocalFileSystemURL() . Это вернет вас к объекту FileEntrySync / DirectoryEntrySync .

Основное приложение:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Listing filesystem entries using the synchronous API</title>
</head>
<body>
<script>
    window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                        window.webkitResolveLocalFileSystemURL;

    var worker = new Worker('worker.js');
    worker.onmessage = function(e) {
    var urls = e.data.entries;
    urls.forEach(function(url, i) {
        window.resolveLocalFileSystemURL(url, function(fileEntry) {
        console.log(fileEntry.name); // Print out file's name.
        });
    });
    };

    worker.postMessage({'cmd': 'list'});
</script>
</body>
</html>

рабочий.js

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                self.requestFileSystemSync;

var paths = []; // Global to hold the list of entry filesystem URLs.

function getAllEntries(dirReader) {
    var entries = dirReader.readEntries();

    for (var i = 0, entry; entry = entries[i]; ++i) {
    paths.push(entry.toURL()); // Stash this entry's filesystem: URL.

    // If this is a directory, we have more traversing to do.
    if (entry.isDirectory) {
        getAllEntries(entry.createReader());
    }
    }
}

function onError(e) {
    postMessage('ERROR: ' + e.toString()); // Forward the error to main app.
}

self.onmessage = function(e) {
    var data = e.data;

    // Ignore everything else except our 'list' command.
    if (!data.cmd || data.cmd != 'list') {
    return;
    }

    try {
    var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);

    getAllEntries(fs.root.createReader());

    self.postMessage({entries: paths});
    } catch (e) {
    onError(e);
    }
};

Пример: загрузка файлов с использованием XHR2.

Обычный вариант использования Worker — загрузка набора файлов с помощью XHR2 и запись этих файлов в файловую систему HTML5. Это идеальная задача для рабочего потока!

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

Основное приложение:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Download files using a XHR2, a Worker, and saving to filesystem</title>
</head>
<body>
<script>
    var worker = new Worker('downloader.js');
    worker.onmessage = function(e) {
    console.log(e.data);
    };
    worker.postMessage({fileName: 'GoogleLogo',
                        url: 'googlelogo.png', type: 'image/png'});
</script>
</body>
</html>

загрузчик.js:

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                self.requestFileSystemSync;

function makeRequest(url) {
    try {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false); // Note: synchronous
    xhr.responseType = 'arraybuffer';
    xhr.send();
    return xhr.response;
    } catch(e) {
    return "XHR Error " + e.toString();
    }
}

function onError(e) {
    postMessage('ERROR: ' + e.toString());
}

onmessage = function(e) {
    var data = e.data;

    // Make sure we have the right parameters.
    if (!data.fileName || !data.url || !data.type) {
    return;
    }
    
    try {
    var fs = requestFileSystemSync(TEMPORARY, 1024 * 1024 /*1MB*/);

    postMessage('Got file system.');

    var fileEntry = fs.root.getFile(data.fileName, {create: true});

    postMessage('Got file entry.');

    var arrayBuffer = makeRequest(data.url);
    var blob = new Blob([new Uint8Array(arrayBuffer)], {type: data.type});

    try {
        postMessage('Begin writing');
        fileEntry.createWriter().write(blob);
        postMessage('Writing complete');
        postMessage(fileEntry.toURL());
    } catch (e) {
        onError(e);
    }

    } catch (e) {
    onError(e);
    }
};

Заключение

Веб-воркеры — это недостаточно используемая и недооцененная функция HTML5. Большинству разработчиков, с которыми я общаюсь, не нужны дополнительные вычислительные преимущества, но их можно использовать не только для чистых вычислений. Если вы настроены скептически (как и я), надеюсь, эта статья помогла вам изменить свое мнение. Выгрузка таких вещей, как операции с диском (вызовы API файловой системы) или HTTP-запросы на Worker, является естественным решением и также помогает разделить ваш код. Файловые API HTML5 внутри Workers открывают совершенно новые возможности для веб-приложений, которые многие еще не исследовали.