適用於工作站的同步 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;

處理配額

目前無法在工作站環境中要求 PERSISTENT 配額。建議您在 Workers 以外處理配額問題。程序大致如下:

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

這個替代方案相當複雜,但應該沒問題。如要進一步瞭解如何使用 PERSISTENT 儲存空間搭配 FileSystem API,請參閱「申請配額」。

使用檔案和目錄

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 結構化複製功能的瀏覽器可讓您將型別陣列、ArrayBufferFileBlob 傳遞至 worker。雖然資料仍是副本,但能夠傳遞 File 代表效能優於先前的方法,因為先前的方法需要先將檔案編碼為 base64,再傳遞至 postMessage()

以下範例會將使用者選取的檔案清單傳遞至專屬的 Worker。Worker 只需傳遞檔案清單 (簡單顯示傳回的資料其實是 FileList),而主要應用程式會將每個檔案讀取為 ArrayBuffer

此範例也使用「網路工作站基本知識」中所述的內嵌 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>

在工作站中讀取檔案

您可以使用非同步 FileReader API 讀取 worker 中的檔案。但還有更好的方法。在 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 下載檔案

工作站的常見用途是使用 XHR2 下載多個檔案,然後將這些檔案寫入 HTML5 檔案系統。這對 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 可為網頁應用程式帶來全新的驚奇體驗,許多人還未探索這項功能。