簡介
HTML5 的 FileSystem API 和 Web Workers 各自都非常強大。FileSystem API 終於為網路應用程式帶來階層式儲存空間和檔案 I/O,而 Worker 則為 JavaScript 帶來真正的非同步「多執行緒」。不過,如果您同時使用這些 API,就能打造出真正有趣的應用程式。
本教學課程提供指南和程式碼範例,說明如何在 Web Worker 中運用 HTML5 FileSystem。假設您具備這兩種 API 的工作知識。如果您還不太熟悉這些 API,或想進一步瞭解相關資訊,請參閱以下兩個實用教學課程,瞭解相關基本知識:探索 FileSystem API 和 Web Workers 基本知識。
同步與非同步 API
非同步 JavaScript API 的使用難度較高。它們很大,它們很複雜。但最令人沮喪的是,這些方法都會帶來許多出錯的機會。您絕對不希望在非同步的環境 (Worker) 中,再疊加複雜的非同步 API (FileSystem)!好消息是,FileSystem API 定義了同步版本,可減輕 Web Workers 的痛苦。
在大多數情況下,同步 API 與非同步 API 完全相同。您會發現,方法、屬性、功能和功能都很熟悉。主要差異如下:
- 同步 API 只能在 Web Worker 情境中使用,而非同步 API 則可在 Worker 內外使用。
- 回呼已完成。API 方法現在會傳回值。
- 視窗物件上的全域方法 (
requestFileSystem()
和resolveLocalFileSystemURL()
) 會變成requestFileSystemSync()
和resolveLocalFileSystemSyncURL()
。
除了這些例外狀況,其他 API 都相同。好的,我們可以開始了!
要求檔案系統
網路應用程式會透過在網路工作者中要求 LocalFileSystemSync
物件,取得同步檔案系統的存取權。requestFileSystemSync()
會公開至 worker 的全域範圍:
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
請注意,現在我們使用的是同步 API,因此沒有成功和錯誤回呼,但有新的傳回值。
與一般 FileSystem API 一樣,目前會為方法加上前置字元:
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
處理配額
目前無法在 Worker 情境中要求 PERSISTENT
配額。建議您在 Workers 以外處理配額問題。這個程序可能會如下所示:
- worker.js:將任何 FileSystem API 程式碼包裝在
try/catch
中,以便擷取任何QUOTA_EXCEED_ERR
錯誤。 - worker.js:如果您擷取
QUOTA_EXCEED_ERR
,請將postMessage('get me more quota')
傳回至主要應用程式。 - 主應用程式:收到 #2 時,請執行
window.webkitStorageInfo.requestQuota()
舞蹈。 - 主應用程式:使用者授予更多配額後,請將
postMessage('resume writes')
傳回至 worker,通知其有額外的儲存空間。
這是相當複雜的解決方法,但應該可行。如要進一步瞭解如何使用 FileSystem API 搭配 PERSISTENT
儲存空間,請參閱「申請配額」。
使用檔案和目錄
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);
}
傳遞檔案、Blob 和 ArrayBuffer
Web Workers 剛推出時,只允許以 postMessage()
傳送字串資料。後來,瀏覽器開始接受可序列化的資料,這表示可以傳遞 JSON 物件。不過,最近 Chrome 等部分瀏覽器已接受使用結構化複製演算法傳遞更複雜資料類型至 postMessage()
。
這代表什麼意思?這表示在主應用程式和 worker 執行緒之間傳遞二進位資料會變得非常容易。支援 worker 結構化複製功能的瀏覽器可讓您將型別陣列、ArrayBuffer
、File
或 Blob
傳遞至 worker。雖然資料仍是副本,但能夠傳遞 File
代表效能優於先前的方法,因為先前的方法需要先將檔案以 base64 編碼,再傳遞至 postMessage()
。
以下範例會將使用者選取的檔案清單傳遞至專屬的 Worker。Worker 只需傳遞檔案清單 (簡單顯示傳回的資料其實是 FileList
),而主要應用程式會將每個檔案讀取為 ArrayBuffer
。
此範例也使用了「Web Workers 基礎知識」一文中所述的內嵌 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 中讀取檔案
您可以在 worker 中使用非同步 FileReader
API 讀取檔案,這完全沒問題。不過,有更好的方法。在 worker 中,有一個同步 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>
worker.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 的真正缺點源自於 worker 的限制。
基於安全考量,呼叫應用程式和 Web Worker 執行緒之間的資料一律不會共用。呼叫 postMessage()
時,系統一律會將資料複製至 worker 和從 worker 複製資料。因此,並非所有資料類型都能傳遞。
很抱歉,FileEntrySync
和 DirectoryEntrySync
目前不在支援的類型中。那麼,您要如何將這些項目傳回通話應用程式?如要規避這項限制,您可以傳回 filesystem: URLs 清單,而非項目清單。filesystem:
網址只是字串,因此非常容易傳遞。此外,您也可以使用 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>
worker.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 FileSystem。這項工作非常適合由 worker 執行緒處理!
以下範例只會擷取及寫入一個檔案,但您可以將其擴充為下載一組檔案。
主要應用程式:
<!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>
downloader.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);
}
};
結論
Web Workers 是 HTML5 中未充分利用且未受重視的功能。我與之間的對話中,大多數開發人員都不需要額外的運算效益,但這些效益不僅可用於純運算,如果您和我一樣抱持懷疑態度,希望這篇文章能說服您改變想法。 將磁碟作業 (檔案系統 API 呼叫) 或 HTTP 要求等工作卸載至 worker 是自然的做法,也有助於將程式碼分割為不同的區塊。在 Worker 中使用 HTML5 File API 可為網頁應用程式帶來全新的驚奇體驗,許多人還未探索這項功能。