เกริ่นนำ
FileSystem API และ Web Workers ของ HTML5 มีประสิทธิภาพในตัวเองอยู่แล้ว ในที่สุด FileSystem API จะนำพื้นที่เก็บข้อมูลแบบลำดับชั้นและ I/O ของไฟล์มาใช้กับเว็บแอปพลิเคชัน และผู้ปฏิบัติงานก็นำ "มัลติเทรด" แบบไม่พร้อมกันมาไว้ใน JavaScript ด้วย แต่เมื่อใช้ API เหล่านี้ร่วมกัน คุณจะสามารถสร้างแอปที่น่าสนใจอย่างแท้จริงได้
บทแนะนำนี้จะให้คำแนะนำและตัวอย่างโค้ดสำหรับการใช้ประโยชน์จาก HTML5 FileSystem ภายใน Web Worker โดยเปรียบได้กับความรู้เกี่ยวกับ API ที่ใช้งานได้ทั้ง 2 แบบ หากยังไม่พร้อมเจาะลึกหรือสนใจดูข้อมูลเพิ่มเติมเกี่ยวกับ API เหล่านั้น ให้อ่านบทแนะนำที่ยอดเยี่ยม 2 เรื่องที่พูดถึงข้อมูลเบื้องต้น ได้แก่ การสำรวจ API ของ FileSystem และพื้นฐานของ Web Workers
API แบบซิงโครนัสกับอะซิงโครนัส
JavaScript API แบบอะซิงโครนัสอาจใช้งานยาก ภาพมีขนาดใหญ่ มีความซับซ้อน แต่สิ่งที่น่าหงุดหงิดที่สุดคือให้โอกาสมากมายแก่ข้อผิดพลาดที่เกิดขึ้น สิ่งสุดท้ายที่คุณต้องจัดการคือการวางเลเยอร์บน API แบบไม่พร้อมกัน (FileSystem) ที่ซับซ้อนในโลกแบบอะซิงโครนัสอยู่แล้ว (Workers)! ข่าวดีคือ FileSystem API กำหนดเวอร์ชันที่ทำงานพร้อมกันเพื่อลดความยุ่งยากในการทำงานของ Web Workers
โดยส่วนใหญ่แล้ว API แบบซิงโครนัสจะเหมือนกับ API แบบอะซิงโครนัสทุกประการ ผู้คนจะคุ้นเคยกับเมธอด พร็อพเพอร์ตี้ ฟีเจอร์ และฟังก์ชันการทำงาน ส่วนเบี่ยงเบนที่สำคัญมีดังนี้
- API แบบซิงโครนัสจะสามารถใช้ได้ในบริบทของ Web Worker เท่านั้น ในขณะที่ API แบบอะซิงโครนัสสามารถใช้ทั้งในและนอก Worker
- โค้ดเรียกกลับไม่ทำงาน ตอนนี้เมธอด API จะแสดงผลค่า
- เมธอดส่วนกลางในออบเจ็กต์หน้าต่าง (
requestFileSystem()
และresolveLocalFileSystemURL()
) จะกลายเป็นrequestFileSystemSync()
และresolveLocalFileSystemSyncURL()
นอกจากข้อยกเว้นเหล่านี้แล้ว API ยังเหมือนกัน โอเค พร้อมใช้งานแล้ว
การขอระบบไฟล์
เว็บแอปพลิเคชันรับสิทธิ์เข้าถึงระบบไฟล์แบบซิงโครนัสโดยการขอออบเจ็กต์ LocalFileSystemSync
จากภายใน Web Worker ขอบเขตการทำงานของ requestFileSystemSync()
จะอยู่ในขอบเขตรวมของผู้ปฏิบัติงาน ได้แก่
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
โปรดสังเกตเห็นว่าค่าผลลัพธ์ใหม่ในตอนนี้เราใช้ API แบบซิงโครนัส รวมถึงการเรียกกลับที่ไม่สำเร็จและข้อผิดพลาด
เช่นเดียวกับ FileSystem API ปกติ เมธอดจะมีคำนำหน้าในขณะนี้ ดังนี้
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
การรับมือกับโควต้า
ขณะนี้ คุณยังไม่สามารถขอโควต้า PERSISTENT
ในบริบทของผู้ปฏิบัติงาน เราขอแนะนำให้จัดการกับปัญหาด้านโควต้านอกผู้ปฏิบัติงาน
กระบวนการนี้อาจมีลักษณะดังนี้
- Worker.js: รวมโค้ด FileSystem API ใน
try/catch
เพื่อให้ตรวจพบข้อผิดพลาดQUOTA_EXCEED_ERR
- Worker.js: หากตรวจพบ
QUOTA_EXCEED_ERR
ให้ส่งpostMessage('get me more quota')
กลับไปที่แอปหลัก - แอปหลัก: ฝึกเต้น
window.webkitStorageInfo.requestQuota()
เมื่อได้รับ #2 - แอปหลัก: หลังจากที่ผู้ใช้ให้โควต้าเพิ่มแล้ว ให้ส่ง
postMessage('resume writes')
กลับให้ผู้ปฏิบัติงานเพื่อแจ้งพื้นที่เก็บข้อมูลเพิ่มเติม
นี่เป็นวิธีแก้ปัญหาชั่วคราวซึ่งค่อนข้างเกี่ยวข้อง แต่ก็ควรจะได้ผล โปรดดูการขอโควต้าสำหรับข้อมูลเพิ่มเติมเกี่ยวกับการใช้พื้นที่เก็บข้อมูล PERSISTENT
กับ FileSystem API
การทำงานกับไฟล์และไดเรกทอรี
เวอร์ชันซิงโครนัสของ 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
คุณก็จะหงุดหงิดทันที สิ่งหนึ่งที่ทำให้ชีวิตง่ายขึ้นคือการรวมโค้ดผู้ปฏิบัติงานที่เกี่ยวข้องทั้งหมดไว้ในการลองใช้/จับ จากนั้น หากมีข้อผิดพลาดเกิดขึ้น ให้ส่งต่อข้อผิดพลาดไปยังแอปหลักโดยใช้ 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);
}
การส่งผ่าน Files, Blob และ ArrayBuffers
เมื่อ Web Worker เริ่มทำงานเป็นครั้งแรก พวกเขาอนุญาตให้ส่งข้อมูลสตริงใน postMessage()
เท่านั้น ต่อมา เบราว์เซอร์เริ่มยอมรับข้อมูลที่ต่อเนื่องกันซึ่งหมายถึงการส่งออบเจ็กต์ JSON ได้ อย่างไรก็ตาม เมื่อเร็วๆ นี้ เบราว์เซอร์บางประเภท เช่น Chrome ยินยอมให้มีการส่งข้อมูลประเภทที่ซับซ้อนขึ้นผ่าน postMessage()
โดยใช้อัลกอริทึมการโคลนที่มีโครงสร้าง
นั่นหมายความว่า ซึ่งหมายความว่าการส่งข้อมูลไบนารีระหว่างแอปหลักและเทรดผู้ปฏิบัติงานจะง่ายขึ้นมาก เบราว์เซอร์ที่รองรับการโคลนที่มีโครงสร้างสำหรับผู้ปฏิบัติงานจะช่วยให้คุณส่งต่ออาร์เรย์ประเภท, ArrayBuffer
, File
หรือ Blob
ไปยังผู้ปฏิบัติงานได้ แม้ว่าข้อมูลจะยังคงเป็นสำเนา แต่การส่ง File
ได้หมายถึงประโยชน์ด้านประสิทธิภาพที่มากกว่าวิธีการเดิม ซึ่งจะเกี่ยวข้องกับไฟล์ base64 ก่อนที่จะส่งไปยัง postMessage()
ตัวอย่างต่อไปนี้จะส่งรายการไฟล์ที่ผู้ใช้เลือกไปยังผู้ปฏิบัติงานที่ได้รับมอบหมาย
ผู้ปฏิบัติงานเพียงแค่ส่งผ่านรายการไฟล์ (แสดงข้อมูลที่ส่งคืนได้ง่ายๆ ว่าเป็น 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>
การอ่านไฟล์ในผู้ปฏิบัติงาน
การใช้ FileReader
API แบบไม่พร้อมกันเพื่ออ่านไฟล์ใน Worker ถือเป็นเรื่องที่ยอมรับได้อย่างยิ่ง แต่ก็ต้องมีวิธีที่ดีกว่า ใน 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 แบบซิงโครนัสเกิดขึ้นจากข้อจำกัดของผู้ปฏิบัติงาน
ด้วยเหตุผลด้านความปลอดภัย ระบบจะไม่แชร์ข้อมูลระหว่างแอปการโทรกับชุดข้อความ Web Worker ข้อมูลจะถูกคัดลอกไปยังและจากผู้ปฏิบัติงานเสมอเมื่อมีการเรียก postMessage()
ด้วยเหตุนี้ จึงไม่สามารถส่งข้อมูลได้บางประเภท
ขออภัย ขณะนี้ FileEntrySync
และ DirectoryEntrySync
ไม่จัดอยู่ในหมวดหมู่ที่เรายอมรับ แล้วคุณจะนำรายการกลับมายังแอปการโทรได้อย่างไร
วิธีหนึ่งในการหลีกเลี่ยงข้อจำกัดคือการแสดงผลรายการ filesystem: URL แทนรายการรายการ 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
กรณีการใช้งานทั่วไปสำหรับ Workers คือการดาวน์โหลดไฟล์จำนวนมากโดยใช้ XHR2 และเขียนไฟล์เหล่านั้นลงใน FileSystem ของ 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);
}
};
บทสรุป
Web Workers เป็นฟีเจอร์ HTML5 ที่ได้รับการปรับปรุงและใช้งานไม่ค่อยได้ประโยชน์ นักพัฒนาซอฟต์แวร์ส่วนใหญ่ที่ผมคุยด้วยไม่จำเป็นต้องได้ประโยชน์ด้านการคำนวณเพิ่มเติม แต่ยังใช้ประโยชน์ได้มากกว่าแค่การคำนวณเพียงอย่างเดียว หากคุณไม่แน่ใจ (อย่างที่ผมเคยตอบ) เราหวังว่าบทความนี้จะช่วยให้คุณเปลี่ยนใจได้ การลดภาระงานต่างๆ เช่น การดำเนินการกับดิสก์ (การเรียก Filesystem API) หรือคำขอ HTTP ไปยังผู้ปฏิบัติงานเป็นสิ่งที่เหมาะสมอยู่แล้วและช่วยจัดแบ่งโค้ดของคุณด้วย ไฟล์ API ของ HTML5 ภายใน Workers ช่วยเปิดความสามารถรูปแบบใหม่ให้กับเว็บแอป ซึ่งผู้คนจำนวนมากยังไม่เคยสำรวจมาก่อน