適用於工作站的同步 FileSystem API

簡介

HTML5 FileSystem API 和 Web Worker 本身就非常強大。FileSystem API 終於為網頁應用程式帶來階層式儲存空間和檔案 I/O,而 Worker 則為 JavaScript 帶來真正的非同步「多執行緒」。不過,如果將這些 API 搭配使用,就能建構出一些非常有趣的應用程式。

本教學課程提供指南和程式碼範例,說明如何在 Web Worker 中運用 HTML5 FileSystem。並假設您具備這兩種 API 的實務知識。如果您還沒準備好深入瞭解,或想進一步瞭解這些 API,請參閱兩篇探討基本概念的優質教學課程:探索 FileSystem API 和 Web Worker 基本概念。

同步與非同步 API

非同步 JavaScript API 可能難以使用。他們很大。他們很複雜。 但最令人沮喪的是,這些方法很容易出錯。您最不想處理的事情,就是在已經是非同步的世界 (Worker) 中,疊加複雜的非同步 API (FileSystem)!好消息是,FileSystem API 定義了同步版本,可減輕 Web Worker 的負擔。

同步 API 大致上與非同步 API 相同。您會熟悉這些方法、屬性、特色和功能。主要差異如下:

  • 同步 API 只能在 Web Worker 情境中使用,而非同步 API 則可在 Worker 內外使用。
  • 回呼功能已推出。API 方法現在會傳回值。
  • 視窗物件的全域方法 (requestFileSystem() 和 resolveLocalFileSystemURL()) 會變成 requestFileSystemSync() 和 resolveLocalFileSystemSyncURL()。

除了這些例外狀況,API 都是相同的。好了,可以開始了!

要求檔案系統

網頁應用程式會從 Web Worker 內要求 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,通知 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 Worker 剛推出時,只允許在 postMessage() 中傳送字串資料。後來,瀏覽器開始接受可序列化的資料,這表示可以傳遞 JSON 物件。不過,最近 Chrome 等部分瀏覽器接受使用結構化複製演算法,透過 postMessage() 傳遞更複雜的資料類型。

這代表什麼意思?這表示在主要應用程式和背景工作執行緒之間傳遞二進位資料會容易得多。如果瀏覽器支援 Worker 的結構化複製,您就可以將類型化陣列、ArrayBuffer、File 或 Blob 傳遞至 Worker。雖然資料仍是副本,但能夠傳遞 File 表示效能優於前一種方法,後者涉及在將檔案傳遞至 postMessage() 之前,先對檔案進行 base64 處理。

以下範例會將使用者選取的檔案清單傳遞至專屬 Worker。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 中讀取檔案

在 Worker 中使用非同步 FileReader API 讀取檔案是完全可行的。不過,有更好的方法。在 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>

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: 網址清單,而非項目清單。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。這項工作非常適合交給背景工作執行緒處理!

以下範例只會擷取及寫入一個檔案,但您可以想像將其擴充為下載一組檔案。

主要應用程式:

<!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 Worker 是 HTML5 中未充分利用且未受重視的功能。我接觸過的大多數開發人員都不需要額外的運算優勢,但這些優勢不只能用於純運算。如果你像我一樣抱持懷疑態度,希望這篇文章能讓你改觀。將磁碟作業 (Filesystem API 呼叫) 或 HTTP 要求等工作卸載至 Worker,不僅符合自然流程,也有助於區隔程式碼。Workers 內部的 HTML5 File API 為網頁應用程式開啟了全新的精彩世界,許多人都尚未探索。