Giới thiệu
FileSystem API và Web Workers của HTML5 rất mạnh mẽ ở khía cạnh riêng của chúng. Cuối cùng, API Hệ thống tệp mang đến bộ nhớ phân cấp và I/O tệp cho các ứng dụng web, còn Worker mang đến tính năng "đa luồng" không đồng bộ thực sự cho JavaScript. Tuy nhiên, khi sử dụng các API này cùng nhau, bạn có thể xây dựng một số ứng dụng thực sự thú vị.
Hướng dẫn này cung cấp hướng dẫn và ví dụ về mã để tận dụng FileSystem HTML5 bên trong một Worker web. Bài viết này giả định 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 hoặc muốn tìm hiểu thêm về các API đó, hãy đọc hai hướng dẫn tuyệt vời thảo luận về các kiến thức cơ bản: Khám phá API FileSystem và Kiến thức cơ bản về Worker web.
API đồng bộ và không đồng bộ
API JavaScript không đồng bộ có thể khó sử dụng. Chúng có kích thước lớn. Chúng phức tạp. Nhưng điều khó chịu nhất là chúng tạo ra nhiều cơ hội để mọi thứ diễn ra không như mong muốn. Điều cuối cùng bạn muốn xử lý là phân lớp trên một 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 sự phiền toái trong Web Worker.
Trong hầu hết các trường hợp, 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. Các sai số chính là:
- Bạn chỉ có thể sử dụng API đồng bộ trong ngữ cảnh của Worker web, trong khi API không đồng bộ có thể được sử dụng trong và ngoài Worker.
- Lệnh gọi lại đã hết. Các phương thức API hiện trả về giá trị.
- Các phương thức toàn cục trên đối tượng cửa sổ (
requestFileSystem()
vàresolveLocalFileSystemURL()
) trở thànhrequestFileSystemSync()
vàresolveLocalFileSystemSyncURL()
.
Ngoài những ngoại lệ này, các API này giống nhau. Được rồi, chúng ta đã sẵn sàng!
Yêu cầu hệ thống tệp
Ứ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 đối tượng LocalFileSystemSync
từ bên trong một Worker trên web. requestFileSystemSync()
sẽ hiển thị với phạm vi chung của Worker:
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
Lưu ý giá trị trả về mới hiện tại chúng ta đang sử dụng API đồng bộ cũng như không có lệnh gọi lại thành công và lỗi.
Giống như API FileSystem thông thường, các phương thức hiện có tiền tố:
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 xử lý các vấn đề về hạn mức bên ngoài Worker.
Quy trình này có thể như sau:
- worker.js: Gói mọi mã FileSystem API trong
try/catch
để phát hiện mọi lỗiQUOTA_EXCEED_ERR
. - worker.js: Nếu bạn bắt được
QUOTA_EXCEED_ERR
, hãy gửipostMessage('get me more quota')
trở lại ứng dụng chính. - ứng dụng chính: Thực hiện quy trình
window.webkitStorageInfo.requestQuota()
khi nhận được #2. - ứng dụng chính: Sau khi người dùng cấp thêm hạn mức, hãy gửi
postMessage('resume writes')
trở lại worker để thông báo cho worker về dung lượng lưu trữ bổ sung.
Đó là một giải pháp tương đối phức tạp, nhưng sẽ có hiệu quả. Hãy 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 API FileSystem.
Làm việc với tệp và thư mục
Phiên bản đồng bộ của getFile()
và getDirectory()
lần lượt trả về FileEntrySync
và DirectoryEntrySync
.
Ví dụ: mã sau đây tạo một tệp trống có tên "log.txt" trong thư mục gốc.
var fileEntry = fs.root.getFile('log.txt', {create: true});
Nội dung 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 bao giờ phải gỡ lỗi mã Web Worker, tôi thật ghen tị với bạn! Có thể sẽ rất đau đớn khi 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ẽ nhanh chóng thấy thất vọng. Một điều có thể giúp bạn dễ dàng hơn là gói tất cả mã Worker liên quan trong một 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 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);
}
Truyền xung quanh Tệp, Blob và ArrayBuffer
Khi mới xuất hiện, Web Worker 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 các loại dữ liệu phức tạp hơn được truyền qua postMessage()
bằng cách sử dụng thuật toán sao chép có cấu trúc.
Điều này thực sự có ý nghĩa gì? Điều đó có nghĩa là việc truyền dữ liệu nhị phân giữa ứng dụng chính và luồng Worker sẽ dễ dàng hơn rất nhiều. Các trình duyệt hỗ trợ tính năng nhân bản có cấu trúc cho Worker cho phép bạn 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
có nghĩa là bạn sẽ nhận được lợi ích về hiệu suất so với phương pháp trước đây, trong đó liên quan đến việc chuyển đổi tệp sang base64 trước khi truyền tệp đó vào postMessage()
.
Ví dụ sau đây truyền danh sách tệp do người dùng chọn đến một Worker chuyên dụng.
Worker chỉ cần truyền qua danh sách tệp (đơn giản để hiển thị 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 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 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 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ẽ rõ ràng hơn nhiều đối với một số tác vụ nhất định. Việc có ít lệnh gọi lại hơn sẽ rất tốt và chắc chắn sẽ giúp mọi thứ dễ đọc hơn. Nhược điểm thực sự của API đồng bộ bắt nguồn từ các 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 loại dữ liệu nào cũng có thể được truyền.
Rất tiếc, FileEntrySync
và DirectoryEntrySync
hiện không thuộc các loại được chấp nhận. Vậy làm cách nào để đưa các mục nhập quay lại ứng dụng gọi?
Một cách để khắc phục hạn chế này là trả về danh sách filesystem: URLs (hệ thống tệp: URL) thay vì danh sách mục nhập. URL filesystem:
chỉ là chuỗi, nên rất dễ truyền. Hơn nữa, bạn có thể phân giải các mục này thành mục trong ứng dụng chính bằng resolveLocalFileSystemURL()
. Thao tác này sẽ đưa bạn trở 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 của Worker là tải nhiều tệp xuống bằng XHR2 rồi ghi các tệp đó vào HTML5 FileSystem. Đây là một tác vụ hoàn hảo cho luồng Worker!
Ví dụ sau đây chỉ tìm nạp và ghi một tệp, nhưng bạn có thể mở rộng ví dụ này để 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
Worker là một tính năng của HTML5 bị sử dụng chưa đúng cách và chưa được đánh giá đúng mức. Hầu hết các nhà phát triển mà tôi trò chuyện không cần đến các lợi ích tính toán bổ sung, nhưng họ có thể sử dụng các lợi ích này cho nhiều mục đích hơn là chỉ tính toán thuần tuý. Nếu bạn vẫn còn hoài nghi (như tôi trước đây), tôi hy vọng bài viết này đã giúp bạn thay đổi suy nghĩ. Việc giảm tải các hoạt động như thao tác trên đĩa (lệnh gọi API hệ thống tệp) hoặc yêu cầu HTTP cho một Worker là một lựa chọn phù hợp và cũng giúp phân chia mã của bạn. API tệp HTML5 bên trong Worker mở ra một thế giới mới đầy thú vị cho các ứng dụng web mà nhiều người chưa khám phá.