適用於工作站的同步 FileSystem API

簡介

HTML5 的 FileSystem APIWeb Workers 各自都非常強大。FileSystem API 終於為網路應用程式帶來階層式儲存空間和檔案 I/O,而 Worker 則為 JavaScript 帶來真正的非同步「多執行緒」。不過,如果您同時使用這些 API,就能打造出真正有趣的應用程式。

本教學課程提供指南和程式碼範例,說明如何在 Web Worker 中運用 HTML5 FileSystem。假設您具備這兩種 API 的工作知識。如果您還不太熟悉這些 API,或想進一步瞭解相關資訊,請參閱以下兩個實用教學課程,瞭解相關基本知識:探索 FileSystem APIWeb 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 以外處理配額問題。這個程序可能會如下所示:

  1. worker.js:將任何 FileSystem API 程式碼包裝在 try/catch 中,以便擷取任何 QUOTA_EXCEED_ERR 錯誤。
  2. worker.js:如果您擷取 QUOTA_EXCEED_ERR,請將 postMessage('get me more quota') 傳回至主要應用程式。
  3. 主應用程式:收到 #2 時,請執行 window.webkitStorageInfo.requestQuota() 舞蹈。
  4. 主應用程式:使用者授予更多配額後,請將 postMessage('resume writes') 傳回至 worker,通知其有額外的儲存空間。

這是相當複雜的解決方法,但應該可行。如要進一步瞭解如何使用 FileSystem API 搭配 PERSISTENT 儲存空間,請參閱「申請配額」。

使用檔案和目錄

getFile()getDirectory() 的同步版本分別會傳回 FileEntrySyncDirectoryEntrySync

舉例來說,下列程式碼會在根目錄中建立名為「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 結構化複製功能的瀏覽器可讓您將型別陣列、ArrayBufferFileBlob 傳遞至 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 複製資料。因此,並非所有資料類型都能傳遞。

很抱歉,FileEntrySyncDirectoryEntrySync 目前不在支援的類型中。那麼,您要如何將這些項目傳回通話應用程式?如要規避這項限制,您可以傳回 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 可為網頁應用程式帶來全新的驚奇體驗,許多人還未探索這項功能。