ממשק API הסינכרוני של FileSystem לעובדים

מבוא

FileSystem API ו-Web Workers ב-HTML5 הם חזקים מאוד בפני עצמם. FileSystem API מאפשר לכם סוף סוף להשתמש באחסון היררכי ובפעולות קלט/פלט של קבצים באפליקציות אינטרנט, ו-Workers מאפשרים לכם להשתמש ב 'שרשורי משימות' אסינכרוניים אמיתיים ב-JavaScript. עם זאת, כשמשתמשים בממשקי ה-API האלה יחד, אפשר ליצור אפליקציות מעניינות מאוד.

במדריך הזה מוסבר איך להשתמש ב-FileSystem של HTML5 בתוך Web Worker, ומפורטות דוגמאות לקוד. ההנחה היא שיש לכם ידע מעשי בשני ממשקי ה-API. אם אתם לא מוכנים להתחיל מיד או שאתם רוצים לקבל מידע נוסף על ממשקי ה-API האלה, כדאי לקרוא את שני מדריכי ה-API המצוינים הבאים, שבהם מוסבר על העקרונות הבסיסיים: Exploring the FileSystem APIs ו-Basics of Web Workers.

ממשקי API סינכרוניים לעומת אסינכרוניים

יכול להיות שיהיה קשה להשתמש בממשקי API של JavaScript לא-סינכרוניים. הם גדולים. הם מורכבים. אבל הכי מתסכל הוא שיש בהן הרבה הזדמנויות לטעויות. הדבר האחרון שתרצו להתעסק בו הוא שכבות של ממשק API אסינכררוני מורכב (FileSystem) בעולם שכבר אסינכררוני (Workers)! החדשות הטובות הן שFileSystem API מגדיר גרסה סינכרונית כדי להקל על הבעיה ב-Web Workers.

ברוב המקרים, ה-API הסינכרוני זהה לבן הדוד האסינכרוני שלו. השיטות, המאפיינים, התכונות והפונקציונליות יהיו מוכרים לכם. הסטיות העיקריות הן:

  • אפשר להשתמש ב-API הסינכרוני רק בהקשר של Web Worker, ואילו ב-API האסינכרוני אפשר להשתמש ב-Worker ובחוץ ממנו.
  • אין אפשרות להתקשרות חזרה. שיטות API מחזירות עכשיו ערכים.
  • השיטות הגלובליות באובייקט window‏ (requestFileSystem() ו-resolveLocalFileSystemURL()) הופכות ל-requestFileSystemSync() ו-resolveLocalFileSystemSyncURL().

מלבד החריגות האלה, ממשקי ה-API זהים. בסדר, הכול מוכן.

שליחת בקשה למערכת קבצים

אפליקציית אינטרנט מקבלת גישה למערכת הקבצים הסינכרונית על ידי בקשה לאובייקט LocalFileSystemSync מתוך Web Worker. המשתנה requestFileSystemSync() חשוף להיקף הגלובלי של Worker:

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

שימו לב לערך ההחזרה החדש עכשיו כשאנחנו משתמשים ב-API הסינכרוני, וגם לכך שאין קריאות חוזרות (callbacks) של הצלחה ושגיאה.

בדומה ל-FileSystem API הרגיל, בשלב הזה יש ל-methods קידומת:

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

טיפול במכסות

בשלב זה אי אפשר לבקש מכסה של PERSISTENT בהקשר של Worker. מומלץ לטפל בבעיות הקשורות למכסות מחוץ ל-Workers. התהליך עשוי להיראות כך:

  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') חזרה ל-worker כדי להודיע לו על נפח אחסון נוסף.

זוהי פתרון עקיף מורכב למדי, אבל הוא אמור לפעול. למידע נוסף על שימוש באחסון 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, אני מקנא בכם! לפעמים קשה מאוד להבין מה הבעיה.

היעדר קריאות חזרה (callbacks) של שגיאות בעולם הסינכרוני מקשה על הטיפול בבעיות. אם נוסיף את המורכבות הכללית של ניפוי באגים בקוד של 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);
}

העברה של קבצים, Blobs ו-ArrayBuffers

כש-Web Workers הופיעו לראשונה, הם איפשרו לשלוח רק נתוני מחרוזות ב-postMessage(). מאוחר יותר, הדפדפנים התחילו לקבל נתונים שניתן לסדר בסדרה, כך שאפשר היה להעביר אובייקט JSON. עם זאת, לאחרונה דפדפנים מסוימים כמו Chrome מקבלים סוגי נתונים מורכבים יותר שאפשר להעביר דרך postMessage() באמצעות אלגוריתם יצירת העותקים המובנה.

מה המשמעות של זה? המשמעות היא שקל הרבה יותר להעביר נתונים בינאריים בין האפליקציה הראשית לבין שרשור ה-Worker. בדפדפנים שתומכים ביצירת עותקים מובנים של Workers, אפשר להעביר ל-Workers מערכי Typed, אובייקטים מסוג ArrayBuffer, File או Blob. הנתונים עדיין עותק, אבל היכולת להעביר File מביאה יתרון בביצועים בהשוואה לגישה הקודמת, שבה היה צריך להמיר את הקובץ ל-base64 לפני שמעבירים אותו ל-postMessage().

בדוגמה הבאה, רשימת קבצים שנבחרה על ידי המשתמש מועברת ל-Worker ייעודי. ה-Worker פשוט עובר ברשימת הקבצים (קל להראות שהנתונים המוחזרים הם למעשה FileList) והאפליקציה הראשית קוראת כל קובץ כ-ArrayBuffer.

הדוגמה כוללת גם גרסה משופרת של השיטה של Web Worker בשורה שמתוארת במאמר היסודות של Web Workers.

<!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>

קריאת קבצים ב-Worker

מותר להשתמש ב-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);

כצפוי, קריאות חזרה (callbacks) לא קיימות ב-FileReader הסינכרוני. כך אפשר לפשט את כמות ההטמעה של פונקציות החזרה (callbacks) בזמן קריאת קבצים. במקום זאת, השיטות readAs* מחזירות את הקובץ שנקרא.

דוגמה: אחזור כל הרשומות

במקרים מסוימים, ממשק ה-API הסינכרוני נקי יותר למשימות מסוימות. פחות קריאות חוזרות הן דבר טוב, והן בהחלט עוזרות לקריאת הקוד. החיסרון האמיתי של ה-API הסינכרוני נובע מהמגבלות של Workers.

מטעמי אבטחה, אף פעם לא מתבצע שיתוף נתונים בין האפליקציה הקוראת לבין שרשור של Web Worker. הנתונים תמיד מועתקים אל Worker וממנו כשקוראים ל-postMessage(). כתוצאה מכך, לא ניתן להעביר כל סוג נתונים.

לצערנו, FileEntrySync ו-DirectoryEntrySync לא נכללים כרגע בסוגי הקבצים המותרים. אז איך אפשר להחזיר את הרשומות לאפליקציית השיחות? אחת מהדרכים לעקוף את המגבלה היא להחזיר רשימה של filesystem: URLs במקום רשימה של רשומות. כתובות ה-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. זוהי משימה מושלמת ל-Thread עובד!

בדוגמה הבאה מתבצעת אחזור וכתיבה של קובץ אחד בלבד, אבל אפשר להרחיב אותה כדי להוריד קבוצת קבצים.

האפליקציה הראשית:

<!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 שרבים לא משתמשים בה ולא מעריכים אותה. רוב המפתחים שאני מדבר איתם לא זקוקים ליתרונות החישוביים הנוספים, אבל אפשר להשתמש בהם למטרות נוספות מלבד חישובים טהורים. אם אתם ספקנים (כמוני), אני מקווה שהמאמר הזה עזר לכם לשנות את דעתכם. העברת משימות כמו פעולות דיסק (קריאות ל-API של מערכת הקבצים) או בקשות HTTP ל-Worker היא התאמה טבעית, והיא גם עוזרת לפלח את הקוד. ממשקי ה-API של קבצים ב-HTML5 ב-Workers פותחים עולם חדש של אפשרויות לאפליקציות אינטרנט, שרבים עדיין לא חקרו.