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

บทนำ

FileSystem API และ Web Worker ของ HTML5 มีประสิทธิภาพอย่างมาก ในที่สุด FileSystem API ก็นําพื้นที่เก็บข้อมูลแบบลําดับชั้นและ I/O ของไฟล์มาสู่เว็บแอปพลิเคชัน และ Workers ก็นํา "การแยกหลายเธรด" แบบแอซิงโครนัสที่แท้จริงมาสู่ JavaScript อย่างไรก็ตาม เมื่อใช้ API เหล่านี้ร่วมกัน คุณสามารถสร้างแอปที่น่าสนใจได้

บทแนะนํานี้จะแสดงคําแนะนําและตัวอย่างโค้ดสําหรับใช้ประโยชน์จาก HTML5 FileSystem ภายใน Web Worker โดยถือว่าผู้อ่านมีความรู้เกี่ยวกับ API ทั้ง 2 รายการ หากคุณยังไม่พร้อมใช้งานหรือสนใจที่จะเรียนรู้เพิ่มเติมเกี่ยวกับ API เหล่านั้น ให้อ่านบทแนะนำที่ยอดเยี่ยม 2 ข้อที่กล่าวถึงข้อมูลพื้นฐาน ได้แก่ การสำรวจ FileSystem API และข้อมูลพื้นฐานของ Web Workers

API แบบซิงโครนัสกับอะซิงโครนัส

JavaScript API แบบอะซิงโครนัสอาจใช้งานยาก ไฟล์มีขนาดใหญ่ ข้อมูลมีความซับซ้อน แต่สิ่งที่น่าหงุดหงิดที่สุดคือโอกาสที่สิ่งต่างๆ จะผิดพลาดมีมากมาย สิ่งสุดท้ายที่ต้องจัดการก็คือการวางเลเยอร์ API แบบไม่พร้อมกันที่ซับซ้อน (FileSystem) ในโลกที่ไม่พร้อมกันอยู่แล้ว (ผู้ปฏิบัติงาน) ข่าวดีคือ FileSystem API กำหนดเวอร์ชันแบบซิงค์เพื่อลดความยุ่งยากใน Web Worker

โดยส่วนใหญ่แล้ว API แบบซิงค์จะเหมือนกับ API แบบแอซิงโครไนซ์ทุกประการ วิธีการ พร็อพเพอร์ตี้ ฟีเจอร์ และฟังก์ชันการทำงานจะคุ้นเคย ความคลาดเคลื่อนหลักๆ มีดังนี้

  • API แบบซิงค์ใช้ได้เฉพาะในบริบทของ Web Worker ส่วน API แบบไม่ซิงค์จะใช้ทั้งในและนอก Worker ได้
  • การติดต่อกลับไม่พร้อมใช้งาน ตอนนี้เมธอด API จะแสดงผลค่า
  • เมธอดร่วมในออบเจ็กต์หน้าต่าง (requestFileSystem() และ resolveLocalFileSystemURL()) จะกลายเป็น requestFileSystemSync() และ resolveLocalFileSystemSyncURL()

นอกเหนือจากข้อยกเว้นเหล่านี้แล้ว API จะเป็น API เดียวกัน โอเค เราพร้อมแล้ว

การขอระบบไฟล์

เว็บแอปพลิเคชันจะได้รับสิทธิ์เข้าถึงระบบไฟล์แบบซิงค์โดยขอออบเจ็กต์ LocalFileSystemSync จากภายใน Web Worker requestFileSystemSync() อยู่ในขอบเขตทั่วโลกของผู้ปฏิบัติงานแล้ว

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

โปรดสังเกตค่าที่แสดงผลใหม่เมื่อเราใช้ API แบบซิงค์ รวมถึงการไม่มีคอลแบ็กสําหรับความสําเร็จและข้อผิดพลาด

ขณะนี้เมธอดจะมีคำนำหน้าอยู่เช่นเดียวกับ FileSystem API ปกติ

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

การจัดการกับโควต้า

ปัจจุบันคุณขอโควต้า PERSISTENT ในบริบทของ Worker ไม่ได้ เราขอแนะนำให้คุณจัดการปัญหาเกี่ยวกับโควต้าที่นอกเหนือจากผู้ปฏิบัติงาน กระบวนการอาจมีลักษณะดังนี้

  1. worker.js: ตัดโค้ด FileSystem API ไว้ใน try/catch เพื่อให้ระบบตรวจจับข้อผิดพลาด QUOTA_EXCEED_ERR
  2. Work.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 คุณจะต้องหงุดหงิดในไม่ช้า สิ่งหนึ่งที่ช่วยให้ชีวิตง่ายขึ้นคือการรวมโค้ด 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 Workers เข้ามาอยู่ในฉากครั้งแรก พวกเขาอนุญาตให้ส่งข้อมูลสตริงใน postMessage() เท่านั้น ต่อมาเบราว์เซอร์เริ่มยอมรับข้อมูลที่จัดเก็บได้ ซึ่งหมายความว่าสามารถส่งออบเจ็กต์ JSON ได้ อย่างไรก็ตาม เมื่อเร็วๆ นี้ เบราว์เซอร์บางประเภท เช่น Chrome ยอมรับประเภทข้อมูลที่ซับซ้อนมากขึ้นที่จะส่งผ่าน postMessage() โดยใช้อัลกอริทึมการโคลน Structured Data

การเปลี่ยนแปลงนี้หมายความว่าอย่างไร ซึ่งหมายความว่าการส่งข้อมูลไบนารีระหว่างแอปหลักกับเธรดเวิร์กเกอร์นั้นง่ายขึ้นมาก เบราว์เซอร์ที่รองรับการโคลนที่มีโครงสร้างสำหรับผู้ปฏิบัติงานช่วยให้คุณส่งอาร์เรย์ประเภท ArrayBuffer, File หรือ Blob ไปยัง Workers ได้ แม้ว่าข้อมูลจะยังคงเป็นสำเนา แต่ความสามารถในการส่ง 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 แบบซิงโครนัสจะดูสะอาดตากว่ามากสำหรับงานบางอย่าง Callback ที่น้อยลงเป็นสิ่งที่ดีและจะทำให้อ่านง่ายขึ้นอย่างแน่นอน ข้อเสียที่แท้จริงของ API แบบซิงค์มาจากข้อจำกัดของ Worker

ระบบจะไม่แชร์ข้อมูลระหว่างแอปที่เรียกใช้กับเธรดเวิร์กเกอร์เว็บเพื่อเหตุผลด้านความปลอดภัย ระบบจะคัดลอกข้อมูลจากและไปยัง 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

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 เปิดโอกาสใหม่ๆ ที่น่าทึ่งให้กับเว็บแอปที่ผู้ใช้จำนวนมากยังไม่รู้จัก