Введение
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. Процесс может выглядеть примерно так:
- worker.js: оберните любой код API файловой системы в
try/catch
, чтобы перехватывать любые ошибкиQUOTA_EXCEED_ERR
. - worker.js: если вы обнаружите
QUOTA_EXCEED_ERR
, отправьтеpostMessage('get me more quota')
обратно в основное приложение. - Основное приложение: выполните танец
window.webkitStorageInfo.requestQuota()
когда получен номер 2. - Основное приложение: после того, как пользователь предоставит дополнительную квоту, отправьте
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 открывают совершенно новые возможности для веб-приложений, которые многие еще не исследовали.