บทนำ
FileSystem API และ Web Worker ของ HTML5 มีประสิทธิภาพอย่างมาก ในที่สุด FileSystem API ก็นำพื้นที่เก็บข้อมูลแบบลําดับชั้นและ I/O ของไฟล์มาสู่เว็บแอปพลิเคชัน และ Workers ก็นํา "การแยกหลายเธรด" แบบแอซิงโครนัสที่แท้จริงมาสู่ JavaScript อย่างไรก็ตาม เมื่อใช้ API เหล่านี้ร่วมกัน คุณสามารถสร้างแอปที่น่าสนใจได้
บทแนะนํานี้จะแสดงคําแนะนําและตัวอย่างโค้ดสําหรับใช้ประโยชน์จาก HTML5 FileSystem ภายใน Web Worker โดยถือว่าผู้อ่านมีความรู้เกี่ยวกับ API ทั้ง 2 รายการ หากยังไม่พร้อมที่จะลองใช้หรือสนใจที่จะเรียนรู้เพิ่มเติมเกี่ยวกับ API เหล่านั้น โปรดอ่านบทแนะนำที่ยอดเยี่ยม 2 รายการที่อธิบายพื้นฐาน ได้แก่ การสํารวจ FileSystem API และพื้นฐานของ Web Worker
API แบบซิงโครนัสกับแบบอะซิงโครนัส
JavaScript API แบบอะซิงโครนัสอาจใช้งานยาก ไฟล์มีขนาดใหญ่ ข้อมูลมีความซับซ้อน แต่สิ่งที่น่าหงุดหงิดที่สุดคือโอกาสที่สิ่งต่างๆ จะผิดพลาดมีมากมาย สิ่งสุดท้ายที่คุณต้องจัดการคือการใช้ API แบบไม่พร้อมกันที่ซับซ้อน (FileSystem) ในสภาพแวดล้อมแบบไม่พร้อมกันอยู่แล้ว (Worker) ข่าวดีคือ FileSystem API กำหนดเวอร์ชันแบบซิงค์เพื่อลดความยุ่งยากใน Web Worker
โดยส่วนใหญ่แล้ว API แบบซิงโครนัสจะเหมือนกับ API แบบอะซิงโครนัสทุกประการ วิธีการ พร็อพเพอร์ตี้ ฟีเจอร์ และฟังก์ชันการทำงานจะคุ้นเคย ความคลาดเคลื่อนหลักๆ มีดังนี้
- API แบบซิงค์ใช้ได้เฉพาะในบริบทของ Web Worker ส่วน API แบบไม่ซิงค์จะใช้ได้ทั้งภายในและภายนอก Worker
- การติดต่อกลับไม่พร้อมใช้งาน ตอนนี้เมธอด API จะแสดงผลค่า
- เมธอดส่วนกลางบนออบเจ็กต์หน้าต่าง (
requestFileSystem()
และresolveLocalFileSystemURL()
) จะเปลี่ยนเป็นrequestFileSystemSync()
และresolveLocalFileSystemSyncURL()
นอกเหนือจากข้อยกเว้นเหล่านี้แล้ว API จะเป็น API เดียวกัน โอเค เราพร้อมแล้ว
การขอระบบไฟล์
เว็บแอปพลิเคชันจะได้รับสิทธิ์เข้าถึงระบบไฟล์แบบซิงค์โดยขอออบเจ็กต์ LocalFileSystemSync
จากภายใน Web Worker requestFileSystemSync()
แสดงในขอบเขตส่วนกลางของ Worker ดังนี้
var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);
โปรดสังเกตค่าที่แสดงผลใหม่เมื่อเราใช้ API แบบซิงค์ รวมถึงการไม่มีคอลแบ็กสําหรับความสําเร็จและข้อผิดพลาด
ขณะนี้เมธอดจะมีคำนำหน้าอยู่เช่นเดียวกับ FileSystem API ปกติ
self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
self.requestFileSystemSync;
การจัดการโควต้า
ปัจจุบันคุณขอโควต้า PERSISTENT
ในบริบทของ Worker ไม่ได้ เราขอแนะนำให้จัดการปัญหาโควต้านอก Workers
กระบวนการอาจมีลักษณะดังนี้
- 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 คุณจะต้องหงุดหงิดในไม่ช้า สิ่งหนึ่งที่จะช่วยให้ชีวิตง่ายขึ้นคือการรวมโค้ด 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 Worker เปิดตัวครั้งแรก อนุญาตให้ส่งข้อมูลสตริงใน postMessage()
เท่านั้น ต่อมาเบราว์เซอร์เริ่มยอมรับข้อมูลที่จัดเรียงได้ ซึ่งหมายความว่าสามารถส่งออบเจ็กต์ JSON ได้ อย่างไรก็ตาม เมื่อเร็วๆ นี้ เบราว์เซอร์บางประเภท เช่น Chrome ยอมรับประเภทข้อมูลที่ซับซ้อนมากขึ้นที่จะส่งผ่าน postMessage()
โดยใช้อัลกอริทึมการโคลน Structured Data
การเปลี่ยนแปลงนี้หมายความว่าอย่างไร ซึ่งหมายความว่าการส่งข้อมูลไบนารีระหว่างแอปหลักกับเธรดเวิร์กเกอร์นั้นง่ายขึ้นมาก เบราว์เซอร์ที่รองรับการโคลนแบบมีโครงสร้างสำหรับ Worker ช่วยให้คุณส่งอาร์เรย์ที่กําหนดประเภท ArrayBuffer
, File
หรือ Blob
ไปยัง Worker ได้ แม้ว่าข้อมูลจะยังคงเป็นสำเนา แต่ความสามารถในการส่ง 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 แบบซิงค์มาจากข้อจำกัดของ Worker
ระบบจะไม่แชร์ข้อมูลระหว่างแอปที่เรียกใช้กับเธรดเวิร์กเกอร์เว็บเพื่อเหตุผลด้านความปลอดภัย ระบบจะคัดลอกข้อมูลจากและไปยัง Worker เสมอเมื่อเรียกใช้ postMessage()
ด้วยเหตุนี้ ระบบจึงส่งข้อมูลบางประเภทไม่ได้
ขออภัย FileEntrySync
และ DirectoryEntrySync
ยังไม่อยู่ในประเภทที่เรายอมรับ แล้วคุณจะนํารายการกลับไปยังแอปการโทรได้อย่างไร
วิธีหนึ่งในการหลีกเลี่ยงข้อจํากัดนี้คือการแสดงรายการ filesystem: URL แทนรายการรายการ filesystem:
URL เป็นเพียงสตริง จึงส่งต่อได้ง่ายมาก นอกจากนี้ คุณยังแก้ไขรายการเหล่านี้เป็นรายการในแอปหลักได้โดยใช้ 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
Use Case ที่พบบ่อยของ 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 Worker เป็นฟีเจอร์ของ HTML5 ที่ไม่ได้ใช้ประโยชน์และไม่ค่อยได้รับการชื่นชม นักพัฒนาแอปส่วนใหญ่ที่เราพูดคุยด้วยไม่จำเป็นต้องใช้ประโยชน์ด้านการคำนวณเพิ่มเติม แต่สามารถใช้เพื่อวัตถุประสงค์อื่นๆ นอกเหนือจากการคำนวณได้ หากคุณมีข้อสงสัย (เช่นเดียวกับเรา) เราหวังว่าบทความนี้จะช่วยเปลี่ยนความคิดของคุณ การย้ายงานต่างๆ เช่น การดำเนินการกับดิสก์ (การเรียกใช้ Filesystem API) หรือคำขอ HTTP ไปยัง Worker นั้นเหมาะเจาะและยังช่วยแบ่งโค้ดออกเป็นส่วนๆ ด้วย File API ของ HTML5 ใน Workers เปิดโอกาสใหม่ๆ ที่น่าทึ่งให้กับเว็บแอปที่ผู้ใช้จำนวนมากยังไม่รู้จัก