FileSystem API đồng bộ dành cho worker

Giới thiệu

FileSystem APIWeb Workers HTML5 cực kỳ mạnh mẽ theo cách riêng. Cuối cùng, FileSystem API cũng mang bộ nhớ phân cấp và I/O tệp đến các ứng dụng web và Worker mang 'đa luồng' không đồng bộ thực sự vào JavaScript. Tuy nhiên, khi sử dụng các API này cùng nhau, bạn có thể tạo ra một số ứng dụng thực sự thú vị.

Phần hướng dẫn này cung cấp các hướng dẫn và ví dụ về mã để tận dụng FileSystem HTML5 bên trong Web Worker. Giả sử bạn đã có kiến thức về cả hai API. Nếu bạn chưa sẵn sàng tìm hiểu sâu hoặc muốn tìm hiểu thêm về các API này, hãy đọc 2 hướng dẫn hay thảo luận về các khái niệm cơ bản: Khám phá các API FileSystemKiến thức cơ bản về nhân viên web.

API đồng bộ và không đồng bộ

API JavaScript không đồng bộ có thể rất khó sử dụng. Chúng lớn. Đó là kiểu phức tạp. Nhưng điều khó chịu nhất là chúng lại mang đến nhiều cơ hội cho sự cố. Điều cuối cùng bạn muốn xử lý là phân lớp trên API không đồng bộ phức tạp (FileSystem) trong một thế giới vốn không đồng bộ (Worker)! Tin vui là FileSystem API đã xác định một phiên bản đồng bộ để giảm bớt các phiền toái trong Web Worker.

Đối với hầu hết các phần, API đồng bộ giống hệt như API không đồng bộ. Các phương thức, thuộc tính, tính năng và chức năng sẽ quen thuộc. Những điểm sai lệch chính là:

  • Bạn chỉ có thể sử dụng API đồng bộ trong ngữ cảnh Web Worker, trong khi API không đồng bộ có thể được dùng trong và ngoài Worker.
  • Không thực hiện được lệnh gọi lại. Các phương thức API hiện trả về giá trị.
  • Các phương thức chung trên đối tượng cửa sổ (requestFileSystem()resolveLocalFileSystemURL()) sẽ trở thành requestFileSystemSync()resolveLocalFileSystemSyncURL().

Ngoại trừ các trường hợp ngoại lệ này, các API đều giống nhau. Được rồi, chúng ta đã sẵn sàng!

Yêu cầu hệ thống tệp

Một ứng dụng web có quyền truy cập vào hệ thống tệp đồng bộ bằng cách yêu cầu một đối tượng LocalFileSystemSync từ trong Web Worker. requestFileSystemSync() hiển thị trong phạm vi toàn cục của Worker:

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

Hãy lưu ý giá trị trả về mới khi chúng ta đang sử dụng API đồng bộ cũng như trường hợp không có lệnh gọi lại thành công và có lỗi.

Giống như API FileSystem thông thường, các phương thức được thêm tiền tố tại thời điểm này:

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

Xử lý hạn mức

Hiện tại, bạn không thể yêu cầu hạn mức PERSISTENT trong ngữ cảnh Worker. Bạn nên quan tâm đến các vấn đề về hạn mức bên ngoài Worker. Quy trình này có thể diễn ra như sau:

  1. worker.js: Gói bất kỳ mã FileSystem API nào trong try/catch để phát hiện mọi lỗi QUOTA_EXCEED_ERR.
  2. worker.js: Nếu bạn phát hiện QUOTA_EXCEED_ERR, hãy gửi lại postMessage('get me more quota') cho ứng dụng chính.
  3. ứng dụng chính: Xem qua bước nhảy window.webkitStorageInfo.requestQuota() khi nhận được nút #2.
  4. ứng dụng chính: Sau khi người dùng cấp thêm hạn mức, hãy gửi lại postMessage('resume writes') cho worker để thông báo cho ứng dụng về dung lượng lưu trữ bổ sung.

Đó là một giải pháp khá phức tạp nhưng sẽ có hiệu quả. Xem phần yêu cầu hạn mức để biết thêm thông tin về cách sử dụng bộ nhớ PERSISTENT bằng FileSystem API.

Làm việc với tệp và thư mục

Phiên bản đồng bộ của getFile()getDirectory() lần lượt trả về FileEntrySyncDirectoryEntrySync.

Ví dụ: Mã sau đây sẽ tạo một tệp trống có tên là "log.txt" trong thư mục gốc.

var fileEntry = fs.root.getFile('log.txt', {create: true});

Thao tác sau đây sẽ tạo một thư mục mới trong thư mục gốc.

var dirEntry = fs.root.getDirectory('mydir', {create: true});

Xử lý lỗi

Nếu bạn chưa từng phải gỡ lỗi mã Web Worker, tôi sẽ làm bạn hài lòng! Bạn có thể thấy rất khó chịu khi phải tìm ra vấn đề đang xảy ra.

Việc thiếu lệnh gọi lại lỗi trong thế giới đồng bộ khiến việc xử lý các vấn đề trở nên khó khăn hơn mức cần thiết. Nếu chúng ta thêm mức độ phức tạp chung của việc gỡ lỗi mã Web Worker, bạn sẽ sớm cảm thấy thất vọng. Một điều có thể giúp cuộc sống trở nên dễ dàng hơn là gói tất cả mã Worker liên quan trong một tệp try/catch. Sau đó, nếu có lỗi xảy ra, hãy chuyển tiếp lỗi đó đến ứng dụng chính bằng cách sử dụng 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);
}

Chuyển qua Files, Blobs và ArrayBuffers

Trong lần đầu tiên xuất hiện, Web Workers chỉ cho phép gửi dữ liệu chuỗi trong postMessage(). Sau đó, các trình duyệt bắt đầu chấp nhận dữ liệu có thể chuyển đổi tuần tự, nghĩa là có thể truyền đối tượng JSON. Tuy nhiên, gần đây, một số trình duyệt như Chrome chấp nhận truyền các kiểu dữ liệu phức tạp hơn qua postMessage() bằng thuật toán sao chép có cấu trúc.

Điều này có nghĩa là gì? Việc này đồng nghĩa việc truyền dữ liệu nhị phân giữa ứng dụng chính và luồng Worker dễ dàng hơn rất nhiều. Với các trình duyệt hỗ trợ tính năng sao chép có cấu trúc cho Worker, bạn có thể truyền Mảng đã nhập, ArrayBuffer, File hoặc Blob vào Worker. Mặc dù dữ liệu vẫn là một bản sao, nhưng việc có thể truyền File mang lại nhiều lợi ích về hiệu suất hơn so với phương pháp trước đó, trong đó liên quan đến việc chuyển base64 tệp trước khi chuyển tệp vào postMessage().

Ví dụ sau đây chuyển danh sách tệp do người dùng chọn đến một Worker chuyên biệt. Worker chỉ cần chuyển qua danh sách tệp (đơn giản để cho thấy dữ liệu được trả về thực sự là FileList) và ứng dụng chính đọc từng tệp dưới dạng ArrayBuffer.

Mẫu này cũng sử dụng phiên bản cải tiến của kỹ thuật Trình chạy web nội tuyến được mô tả trong phần Kiến thức cơ bản về trình chạy web.

<!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>

Đọc tệp trong Worker

Bạn hoàn toàn có thể sử dụng API FileReader không đồng bộ để đọc tệp trong một Worker. Tuy nhiên, có một cách tốt hơn. Trong Worker, có một API đồng bộ (FileReaderSync) giúp đơn giản hoá việc đọc các tệp:

Ứng dụng chính:

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

Như dự kiến, các lệnh gọi lại sẽ biến mất cùng với FileReader đồng bộ. Việc này giúp đơn giản hoá số lượng lệnh gọi lại lồng nhau khi đọc tệp. Thay vào đó, các phương thức readAs* sẽ trả về tệp đọc.

Ví dụ: Tìm nạp tất cả các mục nhập

Trong một số trường hợp, API đồng bộ sẽ gọn gàng hơn nhiều đối với một số tác vụ. Bạn có ít lệnh gọi lại hơn và chắc chắn là mọi thứ sẽ dễ đọc hơn. Nhược điểm thực sự của API đồng bộ bắt nguồn từ những hạn chế của Worker.

Vì lý do bảo mật, dữ liệu giữa ứng dụng gọi và luồng Web Worker sẽ không bao giờ được chia sẻ. Dữ liệu luôn được sao chép vào và từ Worker khi postMessage() được gọi. Do đó, không phải mọi loại dữ liệu đều có thể truyền được.

Rất tiếc là FileEntrySyncDirectoryEntrySync hiện không thuộc các loại được chấp nhận. Vậy làm cách nào để bạn có thể đưa các mục nhập trở lại ứng dụng gọi? Một cách để tránh giới hạn này là trả về danh sách hệ thống tệp: URL thay vì danh sách các mục nhập. URL filesystem: chỉ là các chuỗi nên rất dễ dàng được truyền đi. Ngoài ra, chúng có thể được phân giải thành các mục nhập trong ứng dụng chính bằng resolveLocalFileSystemURL(). Thao tác này sẽ đưa bạn quay lại đối tượng FileEntrySync/DirectoryEntrySync.

Ứng dụng chính:

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

Ví dụ: Tải tệp xuống bằng XHR2

Một trường hợp sử dụng phổ biến dành cho Worker là tải một loạt các tệp bằng XHR2 xuống và ghi các tệp đó vào HTML5 FileSystem. Đây là một nhiệm vụ hoàn hảo cho luồng Worker!

Ví dụ sau chỉ tìm nạp và ghi một tệp, nhưng bạn có thể mở rộng tệp đó xuống để tải một tập hợp tệp xuống.

Ứng dụng chính:

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

Kết luận

Web Worker là một tính năng HTML5 chưa được sử dụng và chưa được đánh giá cao. Hầu hết các nhà phát triển mà tôi trao đổi đều không cần những lợi ích điện toán bổ sung, nhưng có thể dùng chúng cho nhiều mục đích khác ngoài việc tính toán đơn thuần. Nếu bạn vẫn hoài nghi (như tôi), tôi hy vọng bài viết này đã giúp bạn đổi ý. Việc giảm tải những thứ như thao tác đĩa (lệnh gọi API hệ thống tệp) hoặc yêu cầu HTTP cho Worker là cách tự nhiên và cũng giúp phân cách mã của bạn. API Tệp HTML5 bên trong Worker mở ra một loạt những tính năng tuyệt vời hoàn toàn mới dành cho các ứng dụng web mà nhiều người chưa khám phá.