FileSystem API แบบซิงโครนัสสำหรับผู้ปฏิบัติงาน

เกริ่นนำ

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 ในบริบทของผู้ปฏิบัติงาน เราขอแนะนำให้จัดการกับปัญหาด้านโควต้านอกผู้ปฏิบัติงาน กระบวนการนี้อาจมีลักษณะดังนี้

  1. Worker.js: รวมโค้ด FileSystem API ใน try/catch เพื่อให้ตรวจพบข้อผิดพลาด QUOTA_EXCEED_ERR
  2. Worker.js: หากตรวจพบ QUOTA_EXCEED_ERR ให้ส่ง postMessage('get me more quota') กลับไปที่แอปหลัก
  3. แอปหลัก: ฝึกเต้น window.webkitStorageInfo.requestQuota() เมื่อได้รับ #2
  4. แอปหลัก: หลังจากที่ผู้ใช้ให้โควต้าเพิ่มแล้ว ให้ส่ง 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 ช่วยเปิดความสามารถรูปแบบใหม่ให้กับเว็บแอป ซึ่งผู้คนจำนวนมากยังไม่เคยสำรวจมาก่อน