בניתוח המקרה הזה נסביר איך Kiwix, ארגון ללא מטרות רווח, משתמשת בטכנולוגיה של אפליקציות אינטרנט מתקדמות (Progressive Web Apps) וב-File System Access API כדי לאפשר למשתמשים להוריד ולאחסן בארכיונים גדולים באינטרנט לשימוש אופליין. מידע על ההטמעה הטכנית של הקוד שמיועד למערכת הקבצים הפרטית של המקור (OPFS), תכונה חדשה בדפדפן ב-Kiwix PWA שמשפרת את ניהול הקבצים ומספקת גישה משופרת לארכיונים בלי בקשות להרשאה. במאמר הזה מוסבר על האתגרים ומתואר פיתוח עתידי פוטנציאלי של מערכת הקבצים החדשה.
מידע על Kiwix
יותר מ-30 שנה אחרי הולדת האינטרנט, שליש מאוכלוסיית העולם עדיין ממתין לגישה אמינה לאינטרנט, לפי האיחוד הבינלאומי של טלקומוניקציות (ITU). האם זהו סוף הסיפור? כמובן שלא. הצוות של Kiwix, ארגון ללא מטרות רווח שנמצא בשווייץ, פיתח סביבה עסקית של אפליקציות ותוכן בקוד פתוח שמטרתה להפוך את הידע לזמין לאנשים עם גישה מוגבלת לאינטרנט או ללא גישה בכלל. הרעיון הוא שאם לכם אין גישה קלה לאינטרנט, מישהו יכול להוריד בשבילכם משאבים חשובים, במקום ובזמן שבהם יש חיבור, ולאחסן אותם באופן מקומי לשימוש במצב אופליין מאוחר יותר. עכשיו אפשר להמיר לארכיונים דחוסים מאוד, שנקראים קובצי ZIM, אתרים חיוניים רבים, כמו ויקיפדיה, Project Gutenberg, Stack Exchange ואפילו הרצאות TED, ולקרוא אותם בזמן אמת באמצעות דפדפן Kiwix.
בארכיונים של ZIM נעשה שימוש בדחיסה יעילה מאוד של Zstandard (ZSTD) (בגרסאות ישנות יותר נעשה שימוש ב-XZ), בעיקר לאחסון קובצי HTML, JavaScript ו-CSS, בעוד שתמונות בדרך כלל עוברות המרה לפורמט WebP דחוס. כל קובץ ZIM כולל גם כתובת URL ואינדקס של כותרות. הדחיסה היא המפתח כאן, כי כל תוכן ויקיפדיה באנגלית (6.4 מיליון מאמרים ותמונות) נדחס ל-97GB אחרי ההמרה לפורמט ZIM. נשמע הרבה, אבל אם תחשבו את זה, תוכלו להבין שכל הידע האנושי יכול להיכנס עכשיו לטלפון Android מדור הביניים. יש גם הרבה מקורות מידע קטנים יותר, כולל גרסאות של ויקיפדיה לפי נושאים, כמו מתמטיקה, רפואה ועוד.
ב-Kiwix יש מגוון אפליקציות מקוריות לשימוש במחשב (Windows/Linux/macOS) וגם לשימוש בנייד (iOS/Android). עם זאת, בניתוח המקרה הזה נתמקד באפליקציה מסוג Progressive Web App (PWA), שמטרתה לספק פתרון אוניברסלי ופשוט לכל מכשיר שיש בו דפדפן מודרני.
נבחן את האתגרים שמתעוררים בפיתוח אפליקציית אינטרנט אוניברסלית שצריכה לספק גישה מהירה לארכיונים גדולים של תוכן במצב אופליין מלא, וגם כמה ממשקי API מודרניים של JavaScript, במיוחד File System Access API ו-Origin Private File System, שמספקים פתרונות חדשניים ומעניינים לאתגרים האלה.
אפליקציית אינטרנט לשימוש במצב אופליין?
משתמשי Kiwix הם קבוצה אקלקטית עם צרכים שונים רבים, ול-Kiwix יש שליטה מועטה או אפסית במכשירים ובמערכות ההפעלה שבהם המשתמשים יגשו לתוכן שלהם. חלק מהמכשירים האלה עשויים להיות איטיים או מיושנים, במיוחד באזורים בעולם שבהם רמת הכנסה נמוכה. ב-Kiwix מנסים לכסות כמה שיותר תרחישים לדוגמה, אבל הארגון גם הבין שהוא יכול להגיע ליותר משתמשים באמצעות התוכנה הכי אוניברסלית בכל מכשיר: דפדפן האינטרנט. בהשראת חוק Atwood, שלפיו כל אפליקציה שאפשר לכתוב ב-JavaScript תכתב בסופו של דבר ב-JavaScript, לפני כ-10 שנים כמה מפתחי Kiwix החלו להעביר את תוכנת Kiwix מ-C++ ל-JavaScript.
הגרסה הראשונה של הגרסה הזו, שנקראת Kiwix HTML5, הייתה ל-Firefox OS (מערכת ההפעלה של Firefox, שכבר לא קיימת) ולתוספים לדפדפנים. בלב הקוד היה (ועדיין יש) מנוע דחיסה של C++ (XZ ו-ZSTD) שעבר הידור לשפת JavaScript ביניים של ASM.js, ולאחר מכן ל-Wasm או ל-WebAssembly, באמצעות המְהַדר Emscripten. תוספי הדפדפן, ששינו את השם ל-Kiwix JS, עדיין מפותחים באופן פעיל.
נכנסים לProgressive Web App (PWA). מפתחי Kiwix הבינו את הפוטנציאל של הטכנולוגיה הזו, ופיתחו גרסת PWA ייעודית של Kiwix JS, והוסיפו שילובים עם מערכות הפעלה שיאפשרו לאפליקציה להציע יכולות כמו של אפליקציה מקומית, במיוחד בתחומים של שימוש אופליין, התקנה, טיפול בקבצים וגישה למערכת הקבצים.
אפליקציות PWA שמתמקדות בשימוש אופליין הן קלות מאוד, ולכן הן מושלמות להקשרים שבהם יש חיבור לאינטרנט בנייד לסירוגין או חיבור יקר. הטכנולוגיה שעומדת מאחורי התכונה הזו היא Service Worker API ו-Cache API הקשור, שבהם משתמשות כל האפליקציות שמבוססות על Kiwix JS. ממשקי ה-API האלה מאפשרים לאפליקציות לפעול כשרתי, ליירט בקשות אחזור מהמסמך או מהמאמר הראשי שמוצג, ולהפנות אותן לקצה העורפי (JS) כדי לחלץ ולבנות תשובה מהארכיון של ZIM.
אחסון, אחסון בכל מקום
בגלל הגודל הגדול של הארכיונים בפורמט ZIM, האחסון והגישה אליהם, במיוחד במכשירים ניידים, הם כנראה הבעיה הגדולה ביותר של מפתחי Kiwix. משתמשי קצה רבים של Kiwix מורידים תוכן מתוך האפליקציה כשיש חיבור לאינטרנט, כדי להשתמש בו במצב אופליין מאוחר יותר. משתמשים אחרים מורידים למחשב באמצעות טורנט, ולאחר מכן מעבירים למכשיר נייד או לטאבלט. חלק מהמשתמשים מחליפים תוכן באמצעות דיסקונים אונטרנטיים או דיסקים קשיחים ניידים באזורים שבהם האינטרנט הנייד לא יציב או יקר. כל הדרכים האלה לגישה לתוכן ממיקומים שרירותיים שזמינים למשתמשים צריכות להיות נתמכות על ידי Kiwix JS ו-Kiwix PWA.
בהתחלה, File API אפשר ל-Kiwix JS לקרוא ארכיונים ענקיים של מאות GB (אחד מהארכיונים שלנו בפורמט ZIM הוא בגודל 166GB!), גם במכשירים עם נפח זיכרון נמוך. יש תמיכה ב-API הזה בכל דפדפן, גם בדפדפנים ישנים מאוד, ולכן הוא משמש כחלופה אוניברסלית למקרה שאין תמיכה ב-API חדשים יותר. זה פשוט כמו הגדרת אלמנט input
ב-HTML, במקרה של Kiwix:
<input
type="file"
accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
value="Select folder with ZIM files"
id="archiveFilesLegacy"
multiple
/>
אחרי הבחירה, רכיב הקלט מכיל את אובייקטי הקבצים, שהם למעשה מטא-נתונים שמפנים לנתונים הבסיסיים באחסון. מבחינה טכנית, הקצה העורפי של Kiwix, שמבוסס על אובייקטים ונכתב ב-JavaScript טהור בצד הלקוח, קורא קטעים קטנים מהארכיון הגדול לפי הצורך. אם צריך לבצע לחתיכות האלה לחץ, הקצה העורפי מעביר אותן למפריד ה-Wasm, ומקבל עוד חתיכות לפי הצורך, עד שהלחץ של blob מלא (בדרך כלל מאמר או נכס) יבוטל. כלומר, אף פעם לא צריך לקרוא את הארכיון הגדול במלואו לזיכרון.
למרות שהוא אוניברסלי, ל-File API יש חיסרון שגורם לאפליקציות Kiwix JS להיראות מסורבלות ומיושנות בהשוואה לאפליקציות ילידיות: המשתמש צריך לבחור ארכיונים באמצעות בורר קבצים, או לגרור ולשחרר קובץ באפליקציה, בכל פעם שמפעילים את האפליקציה, כי באמצעות ה-API הזה אין דרך לשמור הרשאות גישה מסשן אחד לסשן הבא.
כדי לצמצם את חוויית המשתמש (UX) הירודה הזו, כמו מפתחים רבים, מפתחי ה-JS של Kiwix בחרו בהתחלה ב-Electron. ElectronJS הוא מסגרת מדהימה שמספקת תכונות חזקות, כולל גישה מלאה למערכת הקבצים באמצעות ממשקי Node API. עם זאת, יש לכך כמה חסרונות ידועים:
- הוא פועל רק במערכות הפעלה למחשב.
- הוא גדול וכבד (70MB עד 100MB).
גודל האפליקציות של Electron, בגלל שכל אפליקציה כוללת עותק מלא של Chromium, נמוך בהרבה מ5.1MB בלבד של אפליקציית ה-PWA המינימלית והמקובצת.
האם הייתה דרך שבה Kiwix יכולה לשפר את המצב של משתמשי אפליקציית ה-PWA?
File System Access API מציל את המצב
בסביבות שנת 2019, הצוות של Kiwix גילה ממשק API חדש שעבר ניסיון ראשוני ב-Chrome 78, שנקרא אז Native File System API. הוא הבטיח את היכולת לקבל מאחז קובץ של קובץ או תיקייה ולשמור אותו במסד נתונים של IndexedDB. חשוב לדעת שהכינוי הזה נשמר בין סשנים של האפליקציה, כך שהמשתמש לא נדרש לבחור שוב את הקובץ או התיקייה כשמפעילים מחדש את האפליקציה (אבל הוא כן צריך לענות על בקשה מהירה להרשאה). עד שהיא הגיעה לייצור, השם שלה השתנה ל-File System Access API, והחלקים המרכזיים שלה אושרו על ידי WHATWG כ-File System API (FSA).
אז איך פועל החלק של ה-API שנקרא גישה למערכת הקבצים? כמה נקודות חשובות שכדאי לזכור:
- זהו ממשק API אסינכרוני (למעט פונקציות מיוחדות ב-Web Workers).
- צריך להפעיל את הבוררים של הקבצים או הספריות באופן פרוגרמטי על ידי תיעוד של פעולת המשתמש (לחיצה או הקשה על רכיב בממשק המשתמש).
- כדי שהמשתמש יוכל לתת שוב הרשאה לגשת לקובץ שנבחר בעבר (בסשן חדש), נדרשת גם פעולת משתמש. למעשה, הדפדפן יסרב להציג את בקשת ההרשאה אם היא לא תופעל על ידי פעולת משתמש.
הקוד פשוט יחסית, מלבד הצורך להשתמש ב-IndexedDB API הלא נוח כדי לאחסן את הלחצנים של הקבצים והספריות. החדשות הטובות הן שיש כמה ספריות שמבצעות הרבה מהעבודה הקשה בשבילכם, כמו browser-fs-access. ב-Kiwix JS, החלטנו לעבוד ישירות עם ממשקי ה-API, שיש להם תיעוד מפורט מאוד.
פתיחת בוררי קבצים וספריות
פתיחת בורר קבצים נראית בערך כך (כאן נעשה שימוש ב-Promises, אבל אם אתם מעדיפים את ה-sugar של async/await
, תוכלו לעיין במדריך של Chrome למפתחים):
return window
.showOpenFilePicker({ multiple: false })
.then(function (fileHandles) {
return processFileHandle(fileHandles[0]);
})
.catch(function (err) {
// This is normal if app is launching
console.warn(
'User cancelled, or cannot access fs without user gesture',
err,
);
});
הערה: כדי לפשט את הקוד, הוא מעבד רק את הקובץ הראשון שנבחר (ואוסר לבחור יותר מקובץ אחד). אם רוצים לאפשר לבחור כמה קבצים באמצעות { multiple: true }
, פשוט עוטפים את כל ה-Promises שמעבדים כל אחיזה בהצהרה Promise.all().then(...)
, לדוגמה:
let promisesForFiles = fileHandles.map(function (fileHandle) {
return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
// Do something with the files array
console.log(arrayOfFiles);
}).catch(function (err) {
// Handle any errors that occurred during processing
console.error('Error processing file handles!', err);
)};
עם זאת, מומלץ לבקש מהמשתמש לבחור את הספרייה שמכילה את הקבצים האלה, ולא את הקבצים הבודדים שבה, במיוחד מכיוון שמשתמשי Kiwix נוטים לארגן את כל קובצי ה-ZIM שלהם באותה ספרייה. הקוד להפעלת בורר הספריות כמעט זהה לקוד שלמעלה, אלא שמשתמשים ב-window.showDirectoryPicker.then(function (dirHandle) { … });
.
עיבוד ה-handle של הקובץ או הספרייה
אחרי שמקבלים את הכינוי, צריך לעבד אותו. הפונקציה processFileHandle
עשויה להיראות כך:
function processFileHandle(fileHandle) {
// Serialize fileHandle to indexedDB
serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
console.debug('IndexedDB responded with ' + val);
});
return fileHandle.getFile().then(function (file) {
// Do something with the file
return file;
});
}
חשוב לזכור שצריך לספק את הפונקציה ששומרת את ה-file handle. אין שיטות נוחות לכך, אלא אם משתמשים בספריית הפשטה. אפשר לראות את ההטמעה של Kiwix בקובץ cache.js
, אבל אפשר לפשט אותה מאוד אם משתמשים בה רק כדי לאחסן ולשלוף את הכינוי של קובץ או תיקייה.
עיבוד תיקיות הוא קצת יותר מורכב, כי צריך לעבור על הרשומות בתיקייה שנבחרה באמצעות entries.next()
אסינכררוני כדי למצוא את הקבצים או סוגי הקבצים הרצויים. יש כמה דרכים לעשות זאת, אבל זה הקוד שמשמש ב-Kiwix PWA, באופן כללי:
let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
// Do something with the entry list
return entryList;
});
/**
* Iterates FileSystemDirectoryHandle iterator and adds entries to an array
* @param {Iterator} entries An asynchronous iterator of entries
* @param {Array} archives An array to which to add the entries (may be empty)
* @return {Promise<Array>} A Promise for an array of entries in the directory
*/
function iterateAsyncDirEntries(entries, archives) {
return entries
.next()
.then(function (result) {
if (!result.done) {
let entry = result.value[1];
// Filter for the files you want
if (/\.zim(\w\w)?$/i.test(entry.name)) {
archives.push(entry);
}
return iterateAsyncDirEntryArray(entries, archives);
} else {
// We've processed all the entries
if (!archives.length) {
console.warn('No archives found in the picked directory!');
}
return archives;
}
})
.catch(function (err) {
console.error('There was an error processing the directory!', err);
});
}
חשוב לזכור שלכל רשומה ב-entryList
, תצטרכו מאוחר יותר לאחזר את הקובץ באמצעות entry.getFile().then(function (file) { … })
כשתרצו להשתמש בו, או את המקבילה באמצעות const file = await entry.getFile()
ב-async function
.
אפשר להמשיך?
הדרישה מהמשתמש להעניק הרשאה באמצעות מחווה בכל הפעלה חוזרת של האפליקציה מוסיפה קצת חיכוך לפתיחה (מחדש) של קבצים ותיקיות, אבל עדיין הרבה יותר זורמת מאשר צורך לבחור מחדש קובץ. מפתחי Chromium משלימים כרגע את הקוד שיאפשר הרשאות קבועות לאפליקציות PWA מותקנות. זהו משהו שרבים ממפתחי ה-PWA ביקשו, ואנחנו מצפים לו מאוד.
אבל מה אם לא נצטרך לחכות?! מפתחי Kiwix גילו לאחרונה שאפשר להסיר את כל הבקשות להרשאות כבר עכשיו, באמצעות תכונה חדשה ומגניבה של File Access API שנתמכת בדפדפנים Chromium ו-Firefox (ונתמכת באופן חלקי ב-Safari, אבל עדיין חסרה FileSystemWritableFileStream
). התכונה החדשה הזו היא Origin Private File System.
מעבר לשימוש ב-native: מערכת הקבצים הפרטית של Origin
מערכת הקבצים הפרטית של המקור (OPFS) עדיין תכונה ניסיונית ב-PWA של Kiwix, אבל הצוות מאוד רוצה לעודד משתמשים לנסות אותה כי היא צוברת פערים בין אפליקציות מקוריות לאפליקציות אינטרנט. אלה היתרונות העיקריים:
- אפשר לגשת לארכיונים ב-OPFS בלי בקשות להרשאה, גם בזמן ההפעלה. המשתמשים יכולים להמשיך לקרוא מאמר ולעיין בארכיון מהמקום שבו הפסיקו בסשן הקודם, ללא שום בעיה.
- הוא מספק גישה לאופטימיזציה גבוהה של הקבצים שמאוחסנים בו: ב-Android, המהירות השתפרה פי 5 עד פי 10.
הגישה הרגילה לקבצים ב-Android באמצעות File API איטית מאוד, במיוחד (כפי שקורה לעתים קרובות אצל משתמשי Kiwix) אם ארכיונים גדולים מאוחסנים בכרטיס microSD ולא באחסון של המכשיר. כל זה משתנה עם ה-API החדש. רוב המשתמשים לא יוכלו לאחסן קובץ של 97GB ב-OPFS (המערכת צורכת נפח אחסון במכשיר, ולא בכרטיס ה-microSD), אבל היא מושלמת לאחסון ארכיונים בגודל קטן עד בינוני. רוצים לקבל את האנציקלופדיה הרפואית המלאה ביותר מ-WikiProject Medicine? אין בעיה, בגודל של 1.7GB הוא נכנס בקלות ל-OPFS. (טיפ: מחפשים את other → mdwiki_en_all_maxi בספרייה שבאפליקציה).
איך פועלת מערכת OPFS
OPFS היא מערכת קבצים שסופקת על ידי הדפדפן, נפרדת לכל מקור, שאפשר להתייחס אליה כאל אחסון ברמת האפליקציה ב-Android. אפשר לייבא קבצים ל-OPFS ממערכת הקבצים שגלויה למשתמשים, או להוריד אותם ישירות אליה (ה-API מאפשר גם ליצור קבצים ב-OPFS). אחרי שהם נמצאים ב-OPFS, הם מבודדים משאר המכשיר. בדפדפנים מבוססי Chromium למחשב, אפשר גם לייצא קבצים בחזרה מ-OPFS למערכות הקבצים שגלויות למשתמשים.
כדי להשתמש ב-OPFS, השלב הראשון הוא לבקש גישה אליו באמצעות navigator.storage.getDirectory()
(שוב, אם אתם מעדיפים לראות קוד באמצעות await
, תוכלו לקרוא את המאמר Origin Private File System):
return navigator.storage
.getDirectory()
.then(function (handle) {
return processDirHandle(handle);
})
.catch(function (err) {
console.warn('Unable to get the OPFS directory entry', err);
});
הכינוי שמתקבל מהפעולה הזו הוא מאותו סוג של FileSystemDirectoryHandle
שמתקבל מ-window.showDirectoryPicker()
שצוין למעלה, כלומר אפשר לעשות שימוש חוזר בקוד שמטפל בזה (ומזל שאין צורך לאחסן אותו ב-indexedDB
– פשוט מקבלים אותו כשצריך). נניח שכבר יש לכם כמה קבצים ב-OPFS ואתם רוצים להשתמש בהם. באמצעות הפונקציה iterateAsyncDirEntries()
שצוינה קודם, תוכלו לבצע פעולה כמו:
return navigator.storage.getDirectory().then(function (dirHandle) {
let entries = dirHandle.entries();
return iterateAsyncDirEntries(entries, [])
.then(function (archiveList) {
return archiveList;
})
.catch(function (err) {
console.error('Unable to iterate OPFS entries', err);
});
});
חשוב לזכור שעדיין צריך להשתמש ב-getFile()
בכל רשומה שרוצים לעבוד איתה במערך archiveList
.
ייבוא קבצים ל-OPFS
אז איך מעבירים קבצים ל-OPFS מלכתחילה? לא כל כך מהר! קודם כל, צריך להעריך את נפח האחסון שעומד לרשותכם, ולוודא שהמשתמשים לא מנסים להעלות קובץ בנפח 97GB אם הוא לא יתאים.
קל לקבל את המכסה המשוערת:
navigator.storage.estimate().then(function (estimate) { … });
. קצת יותר קשה להבין איך להציג את זה למשתמש. באפליקציית Kiwix, בחרנו להציג חלונית קטנה באפליקציה שמופיעה לצד תיבת הסימון, ומאפשרת למשתמשים לנסות את OPFS:
הלוח מאוכלס באמצעות estimate.quota
ו-estimate.usage
, לדוגמה:
let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
OPFSQuota = estimate.quota - estimate.usage;
document.getElementById('OPFSQuota').innerHTML =
'<b>OPFS storage quota:</b><br />Used: <b>' +
percent +
'%</b>; ' +
'Remaining: <b>' +
(OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
' GB</b>';
});
כפי שניתן לראות, יש גם לחצן שמאפשר למשתמשים להוסיף קבצים ל-OPFS ממערכת הקבצים שגלויה למשתמשים. החדשות הטובות הן שאפשר פשוט להשתמש ב-File API כדי לקבל את אובייקט הקובץ (או האובייקטים) הנדרשים שייבאו. למעשה, חשוב לא להשתמש ב-window.showOpenFilePicker()
כי Firefox לא תומך בשיטה הזו, בעוד ש-OPFS כן נתמך.
הלחצן הגלוי Add file(s) שמוצג בצילום המסך שלמעלה הוא לא בורר קבצים מדור קודם, אבל הוא click()
בורר מדור קודם מוסתר (רכיב <input type="file" multiple … />
) כשלוחצים או מקישים עליו. לאחר מכן האפליקציה מתעדת את האירוע change
של הקלט של הקובץ המוסתר, בודקת את הגודל של הקבצים ומשייכת אותם אם הם גדולים מדי לתקרה. אם הכל בסדר, שואלים את המשתמש אם הוא רוצה להוסיף את החשבון:
archiveFilesLegacy.addEventListener('change', function (files) {
const filesArray = Array.from(files.target.files);
// Abort if user didn't select any files
if (filesArray.length === 0) return;
// Calculate the size of the picked files
let filesSize = 0;
filesArray.forEach(function (file) {
filesSize += file.size;
});
// Check the size of the files does not exceed the quota
if (filesSize > OPFSQuota) {
// Oh no, files are too big! Tell user...
console.log('Files would exceed the OPFS quota!');
} else {
// Ask user if they're sure... if user said yes...
return importOPFSEntries(filesArray)
.then(function () {
// Tell user we successfully imported the archives
})
.catch(function (err) {
// Tell user there was an error (error catching is important!)
});
}
});
בחלק ממערכות ההפעלה, כמו Android, ייבוא הארכיונים הוא לא הפעולה המהירה ביותר, ולכן ב-Kiwix מוצגים גם באנר וספינר קטן בזמן הייבוא. הצוות לא הצליח להבין איך להוסיף אינדיקטור של התקדמות לתהליך הזה: אם תמצאו פתרון, אפשר לשלוח את התשובה בגלויות.
אז איך הטמיעו ב-Kiwix את הפונקציה importOPFSEntries()
? לשם כך משתמשים בשיטה fileHandle.createWriteable()
, שמאפשרת להעביר כל קובץ בסטרימינג ל-OPFS. כל העבודה הקשה מתבצעת על ידי הדפדפן. (ב-Kiwix משתמשים ב-Promises מסיבות שקשורות לקוד המורשת שלנו, אבל צריך לציין שבמקרה הזה await
יוצר תחביר פשוט יותר ומאפשר להימנע מהאפקט של 'פירמידה של אבדון').
function importOPFSEntries(files) {
// Get a handle on the OPFS directory
return navigator.storage
.getDirectory()
.then(function (dir) {
// Collect the promises for each file that we want to write
let promises = files.map(function (file) {
// Create the file and get a writeable handle on it
return dir
.getFileHandle(file.name, { create: true })
.then(function (fileHandle) {
// Get a writer for the file
return fileHandle.createWritable().then(function (writer) {
// Show a banner / spinner, then write the file
return writer
.write(file)
.then(function () {
// Finished with this writer
return writer.close();
})
.catch(function (err) {
console.error('There was an error writing to the OPFS!', err);
});
});
})
.catch(function (err) {
console.error('Unable to get file handle from OPFS!', err);
});
});
// Return a promise that resolves when all the files have been written
return Promise.all(promises);
})
.catch(function (err) {
console.error('Unable to get a handle on the OPFS directory!', err);
});
}
הורדת זרם קבצים ישירות ל-OPFS
וריאציה על כך היא היכולת להעביר קובץ בסטרימינג מהאינטרנט ישירות ל-OPFS, או לכל ספרייה שיש לכם להן מזהה ספרייה (כלומר, ספריות שנבחרו באמצעות window.showDirectoryPicker()
). הקוד הזה מבוסס על אותם עקרונות כמו הקוד שלמעלה, אבל הוא יוצר Response
שמורכב מ-ReadableStream
ומבקר שמוסיף לתור את הבייטים שנקראים מהקובץ המרוחק. לאחר מכן, ה-Response.body
שנוצר מועבר לכותב הקובץ החדש בתוך ה-OPFS.
במקרה כזה, Kiwix יכולה לספור את הבייטים שעוברים דרך ReadableStream
, וכך לספק למשתמש אינדיקטור של התקדמות, וגם להזהיר אותו לא לצאת מהאפליקציה במהלך ההורדה. הקוד מורכב מדי כדי להציג אותו כאן, אבל מכיוון שהאפליקציה שלנו היא אפליקציית FOSS, תוכלו לעיין במקור אם אתם רוצים לעשות משהו דומה. זהו הממשק של Kiwix (הערכים השונים של ההתקדמות שמוצגים למטה הם בגלל שהבאנר מתעדכן רק כשהאחוז משתנה, אבל החלונית Download progress מתעדכנת בתדירות גבוהה יותר):
מכיוון שההורדה יכולה להיות פעולה ארוכה למדי, Kiwix מאפשרת למשתמשים להשתמש באפליקציה באופן חופשי במהלך הפעולה, אבל מוודאת שהבאנר תמיד מוצג כדי להזכיר למשתמשים לא לסגור את האפליקציה עד שההורדה תושלם.
הטמעת מנהל קבצים בתוך האפליקציה
בשלב הזה, מפתחי ה-PWA של Kiwix הבינו שלא מספיק שאפשר להוסיף קבצים ל-OPFS. האפליקציה גם צריכה לתת למשתמשים דרך למחוק קבצים שהם כבר לא צריכים מאזור האחסון הזה, ובאופן אידיאלי גם לייצא קבצים נעולים ב-OPFS בחזרה למערכת הקבצים שגלויה למשתמשים. למעשה, נאלצנו להטמיע מערכת ניהול קבצים בתוך האפליקציה.
רציתי להמליץ על התוסף הנהדר OPFS Explorer ל-Chrome (הוא פועל גם ב-Edge). העדכון מוסיף כרטיסייה בכלי הפיתוח שמאפשרת לראות בדיוק מה נמצא ב-OPFS, וגם למחוק קבצים זדוניים או כושלים. השירות הזה עזר לנו מאוד לבדוק אם הקוד פועל, לעקוב אחרי התנהגות ההורדות ולנקות את הניסויים שלנו בפיתוח.
ייצוא של קבצים תלוי ביכולת לקבל מאחז קובץ בקובץ או בתיקייה שנבחרו, שבהם Kiwix תשמור את הקובץ שיוצאו. לכן, הפעולה הזו פועלת רק בהקשרים שבהם אפשר להשתמש בשיטה window.showSaveFilePicker()
. אם קובצי Kiwix היו קטנים מ-GB, היינו יכולים ליצור blob בזיכרון, לתת לו כתובת URL ואז להוריד אותו למערכת הקבצים שגלויה למשתמש.
לצערנו, אי אפשר לעשות זאת עם ארכיונים גדולים כל כך. אם יש תמיכה ביצוא, הוא פשוט למדי: הוא כמעט זהה, רק הפוך, לשמירת קובץ ב-OPFS (מקבלים את ה-handle של הקובץ שרוצים לשמור, מבקשים מהמשתמש לבחור מיקום לשמירה באמצעות window.showSaveFilePicker()
, ואז משתמשים ב-createWriteable()
ב-saveHandle
). אפשר לראות את הקוד במאגר.
מחיקת קבצים נתמכת בכל הדפדפנים, וניתן לבצע אותה באמצעות dirHandle.removeEntry('filename')
פשוט. במקרה של Kiwix, העדפנו להריץ את הרשומות ב-OPFS כמו שעשינו למעלה, כדי שנוכל לבדוק קודם אם הקובץ שנבחר קיים ולבקש אישור, אבל יכול להיות שזה לא יהיה נחוץ לכולם. שוב, אפשר לבדוק את הקוד שלנו אם רוצים.
הוחלט לא להעמיס את ממשק המשתמש של Kiwix בלחצנים שמציעים את האפשרויות האלה, ובמקום זאת להציב סמלים קטנים מתחת לרשימה של הארכיון. הקשה על אחד מהסמלים האלה תשנה את הצבע של רשימת הארכיונים, כסימן חזותי למשתמש לגבי הפעולה שהוא עומד לבצע. לאחר מכן, המשתמש לוחץ או מקשקש על אחד מהארכיונים, והפעולה המתאימה (ייצוא או מחיקה) מתבצעת (אחרי אישור).
לסיום, הנה הדגמה בסרטון של כל התכונות לניהול קבצים שצוינו למעלה – הוספה של קובץ ל-OPFS, הורדה ישירה של קובץ אליו, מחיקה של קובץ וייצוא למערכת הקבצים שגלויה למשתמש.
עבודה של מפתחים אף פעם לא מסתיימת
OPFS הוא חידוש נהדר למפתחי אפליקציות PWA, שמספק תכונות חזקות מאוד לניהול קבצים שעוזר לצמצם את הפער בין אפליקציות מקוריות לאפליקציות אינטרנט. אבל מפתחים הם חבורה אומללה – הם אף פעם לא מרוצים לגמרי! מערכת OPFS כמעט מושלמת, אבל לא לגמרי… נהדר שהתכונות העיקריות פועלות גם בדפדפני Chromium וגם בדפדפני Firefox, ושהן מוטמעות גם ב-Android וגם במחשב. אנחנו מקווים שמערכת התכונות המלאה תוטמע גם ב-Safari וב-iOS בקרוב. הבעיות הבאות עדיין לא טופלו:
- כרגע, ב-Firefox יש מגבלה של 10GB על המכסה של OPFS, ללא קשר למרחב הדיסק שזמין. רוב מחברי ה-PWA יכולים להסתפק במכסה הזו, אבל היא מגבילה מאוד את Kiwix. למרבה המזל, דפדפני Chromium הרבה יותר נדיבים.
- נכון לעכשיו אי אפשר לייצא קבצים גדולים מ-OPFS למערכת הקבצים שגלויות למשתמשים בדפדפנים לנייד או ב-Firefox למחשב, כי
window.showSaveFilePicker()
לא מיושם. בדפדפנים האלה, קבצים גדולים נלכדים ב-OPFS. המדיניות הזו מנוגדת לאתוס של Kiwix, שמבוסס על גישה פתוחה לתוכן ועל היכולת לשתף ארכיונים בין משתמשים, במיוחד באזורים שבהם החיבור לאינטרנט לא יציב או יקר. - המשתמשים לא יכולים לקבוע איזה נפח אחסון ישמש את מערכת הקבצים הווירטואלית של OPFS. הבעיה הזו בולטת במיוחד במכשירים ניידים, שבהם יכול להיות למשתמשים נפח אחסון גדול בכרטיס microSD, אבל נפח אחסון קטן מאוד במכשיר.
אבל בסך הכול, אלה בעיות קטנות ביחס לשיפור המשמעותי בגישה לקבצים ב-PWA. צוות Kiwix PWA מודה מאוד למפתחים ולתומכים של Chromium שהציעו ועיצבו לראשונה את File System Access API, ועל העבודה הקשה שהובילה להסכמה בין ספקי הדפדפנים לגבי החשיבות של מערכת הקבצים הפרטית של המקור. ב-Kiwix JS PWA, ה-PWA פתר הרבה מהבעיות שקשורות לחוויית המשתמש שהפריעו לאפליקציה בעבר, ועוזר לנו לשפר את הנגישות של תוכן Kiwix לכולם. נשמח לשמוע מה חשבתם על אפליקציית Kiwix ל-PWA ולשלוח משוב למפתחים.
באתרים הבאים אפשר למצוא מקורות מידע מעולים על היכולות של אפליקציות PWA:
- Project Fugu API showcase: אוסף של אפליקציות אינטרנט שממחישות את היכולות שמצמצמות את הפער בין אפליקציות מקוריות לבין אפליקציות אינטרנט לנייד.
- מה אפשר לעשות היום עם אפליקציות PWA: הצגה של מה שאפשר לעשות היום עם אפליקציות PWA.