מבוא
FileSystem API ו-Web Workers ב-HTML5 הם חזקים מאוד בפני עצמם. FileSystem API מאפשר לכם סוף סוף להשתמש באחסון היררכי ובפעולות קלט/פלט של קבצים באפליקציות אינטרנט, ו-Workers מאפשרים לכם להשתמש ב'שרשורי משימות' אסינכרוניים אמיתיים ב-JavaScript. עם זאת, כשמשתמשים בממשקי ה-API האלה יחד, אפשר ליצור אפליקציות מעניינות מאוד.
במדריך הזה מוסבר איך להשתמש ב-FileSystem של HTML5 בתוך Web Worker, ומצורפות דוגמאות קוד. ההנחה היא שיש לכם ידע מעשי בשני ממשקי ה-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 הסינכרוני, וגם בהיעדר הצלחה או קריאה חוזרת (callback) של שגיאות.
בדומה ל-FileSystem API הרגיל, בשלב הזה יש ל-methods קידומת:
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')
חזרה ל-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 הרלוונטי ולנסות לקלוט. לאחר מכן, אם יהיו שגיאות, מעבירים את השגיאה לאפליקציה הראשית באמצעות 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()
באמצעות אלגוריתם יצירת העותקים המובנה.
מה המשמעות של זה? המשמעות היא שקל מאוד להעביר נתונים בינאריים בין האפליקציה הראשית ל-thread של Worker. בדפדפנים שתומכים ביצירת עותקים מובנים של Workers, אפשר להעביר ל-Workers מערכי Typed, אובייקטים מסוג ArrayBuffer
, File
או Blob
. הנתונים עדיין עותק, אבל היכולת להעביר File
מביאה יתרון בביצועים בהשוואה לגישה הקודמת, שבה היה צריך להמיר את הקובץ ל-base64 לפני שמעבירים אותו ל-postMessage()
.
בדוגמה הבאה, רשימת קבצים שנבחרה על ידי המשתמש מועברת ל-Worker ייעודי.
ה-Worker פשוט עובר ברשימת הקבצים (קל להראות שהנתונים המוחזרים הם למעשה FileList
) והאפליקציה הראשית קוראת כל קובץ כ-ArrayBuffer
.
הדוגמה משתמשת גם בגרסה משופרת של השיטה Web Worker שמוטבעת, שמתוארת במאמר Basics of 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, ואז לכתוב את הקבצים האלה ב-HTML5 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 פותחים עולם חדש של אפשרויות לאפליקציות אינטרנט, שרבים עדיין לא חקרו.