適用於工作站的同步 FileSystem API

引言

HTML5 FileSystem API網路工作站本身就非常實用。FileSystem API 最終為網頁應用程式提供階層式儲存空間和檔案 I/O,工作站將真正的非同步「多執行緒」功能導入 JavaScript。不過,如果您搭配使用這些 API,就能建構一些真正有趣的應用程式。

本教學課程提供指南和程式碼範例,協助您在網路工作站中使用 HTML5 檔案系統。假設您具備這兩個 API 的工作知識。如果您尚未準備好深入瞭解或有興趣進一步瞭解這些 API,請參考下列兩堂實用的教學課程:探索 FileSystem API網路工作人員的基本知識

同步 API 與非同步 API

非同步 JavaScript API 可能很難使用。大型。名稱非常複雜。最讓人感到沮喪的是,這種解決方案提供大量出錯的機會。 最後,您還想要在已經非同步的世界 (Workers) 中,對複雜的非同步 API (FileSystem) 進行分層處理!好消息是 FileSystem API 定義了同步版本,以減輕網路工作站的負擔。

在大部分的情況下,同步 API 與非同步親筆完全相同。方法、屬性、特色和功能應該熟悉。主要偏差如下:

  • 同步 API 只能在 Web Worker 環境中使用,而非同步 API 可以在工作站內或外使用。
  • 回呼傳出。API 方法現在會傳回值。
  • 視窗物件 (requestFileSystem()resolveLocalFileSystemURL()) 上的全域方法會變成 requestFileSystemSync()resolveLocalFileSystemSyncURL()

除了這些例外狀況外,API 是相同的。好的,我們準備妥當了!

要求檔案系統

網頁應用程式可以從 Web Worker 中要求 LocalFileSystemSync 物件,取得同步檔案系統的存取權。系統會向工作站的全域範圍公開 requestFileSystemSync()

var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);

請注意,我們現在使用的是同步 API,以及沒有成功和錯誤回呼,並據此顯示新的回傳值。

與一般 FileSystem API 一樣,方法目前會加上前置字串:

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                 self.requestFileSystemSync;

處理配額

目前無法在工作站結構定義中要求 PERSISTENT 配額。建議您處理工作站以外的配額問題。程序大致如下:

  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') 傳回工作站,通知工作站額外的儲存空間。

這是相當有用的解決方法。如要進一步瞭解如何將 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 程式碼偵錯作業,您會立即感到困擾。我們簡化操作的其中一項做法,就是將所有相關的工作站程式碼納入 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 和 ArrayBuffers 周圍傳遞

Web Worker 首次進入場景時,只能在 postMessage() 中傳送字串資料。之後瀏覽器開始接受可序列化的資料,這表示有可能傳遞 JSON 物件。不過,Chrome 等部分瀏覽器最近接受更複雜的資料類型,並透過結構化本機副本演算法透過 postMessage() 傳遞。

這代表什麼意思?這表示在主應用程式與工作站執行緒之間傳遞二進位資料可大幅簡化。如果瀏覽器支援工作站的結構化複製功能,您就能將型別陣列、ArrayBufferFileBlob 傳遞至 worker。雖然資料仍是副本,但能夠傳遞 File 對較先前的方法來說能帶來效能優勢,因為在將資料傳遞至 postMessage() 之前,必須採用 base64 處理檔案。

以下範例會將使用者選取的檔案清單傳遞至專屬工作站。工作站只會通過檔案清單 (顯示傳回資料的簡單就是 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>

讀取 worker 中的檔案

您可以在 worker 中使用非同步 FileReader API 讀取檔案。不過,還有更好的做法。工作站中有同步 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 的實際缺點,取決於工作站的限制。

基於安全考量,呼叫應用程式和 Web Worker 執行緒之間的資料絕不會共用。呼叫 postMessage() 時,系統一律會將資料複製到工作站,並從工作站複製資料。因此,並非所有資料類型都可以傳送。

不過,FileEntrySyncDirectoryEntrySync 目前並不是可接受的類型。那麼,該如何將項目傳回呼叫應用程式?規避限制的一種方式是傳回 filesystem: URL 清單,而非項目清單。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 檔案系統。這對工作站執行緒來說是很理想的任務!

以下範例只會擷取並寫入一個檔案,但您可以使用圖片展開圖片來下載一組檔案。

主應用程式:

<!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);
    }
};

結語

網路工作站是 HTML5 使用率偏低且欠缺充分利用的功能。我交談過的開發人員大多不需要額外的運算優勢,但可用於單純的運算以外的用途。如果你有懷疑,希望這篇文章可以幫助你改變主意。 將光碟作業 (Filesystem API 呼叫) 或 HTTP 要求卸載至工作站的做法很正常,也有助於將程式碼進行劃分。Workers 內的 HTML5 File API 為網頁應用程式開啟了一系列絕佳功能,方便許多使用者從未探索過。