שנדבר על... ארכיטקטורה?
אני רוצה לדבר על נושא חשוב, אבל כזה שיכול להיות שלא מובן מספיק: הארכיטקטורה שבה אתם משתמשים באפליקציית האינטרנט שלכם, ובאופן ספציפי, איך ההחלטות הארכיטקטוניות שלכם משפיעות על בניית אפליקציית אינטרנט מתקדמת.
המונח 'ארכיטקטורה' יכול להישמע מעורפל, ולא תמיד ברור למה זה חשוב. דרך אחת לחשוב על ארכיטקטורה היא לשאול את עצמכם את השאלות הבאות: כשמשתמש מבקר בדף באתר שלי, איזה HTML נטען? ואז, מה נטען כשהם מבקרים בדף אחר?
התשובות לשאלות האלה לא תמיד פשוטות, והן יכולות להיות מורכבות עוד יותר כשמתחילים לחשוב על Progressive Web Apps. לכן המטרה שלי היא להציג לך ארכיטקטורה אפשרית אחת שמצאתי כיעילה. במאמר הזה, אציין את ההחלטות שקיבלתי כ"הגישה שלי" לבניית Progressive Web App.
אתם יכולים להשתמש בגישה שלי כשאתם בונים PWA משלכם, אבל תמיד יש חלופות תקפות אחרות. אני מקווה שההסבר על האופן שבו כל החלקים משתלבים ייתן לכם השראה, ותוכלו להתאים את התוכן לצרכים שלכם.
אפליקציית PWA של Stack Overflow
כדי להמחיש את הנקודות במאמר הזה, יצרתי אפליקציית PWA של Stack Overflow. אני משקיע הרבה זמן בקריאה ובתרומה ל-Stack Overflow, ורציתי ליצור אפליקציית אינטרנט שתאפשר לי לעיין בקלות בשאלות נפוצות בנושא מסוים. הוא מבוסס על Stack Exchange API הציבורי. הוא בקוד פתוח, ואפשר לקבל מידע נוסף בפרויקט GitHub.
אפליקציות מרובות דפים (MPA)
לפני שאפרט, כדאי להגדיר כמה מונחים ולהסביר על חלק מהטכנולוגיות הבסיסיות. קודם כל, אסביר על מה שאני מכנה 'אפליקציות מרובות דפים' או 'MPAs'.
MPA הוא שם מפואר לארכיטקטורה המסורתית שבה נעשה שימוש מאז תחילת האינטרנט. בכל פעם שמשתמש עובר לכתובת URL חדשה, הדפדפן מעבד בהדרגה את קוד ה-HTML שספציפי לדף הזה. אין ניסיון לשמור את מצב הדף או את התוכן בין הניווטים. בכל פעם שאתם נכנסים לדף חדש, אתם מתחילים מחדש.
זה שונה מהמודל של אפליקציה חד-דפית (SPA) ליצירת אפליקציות אינטרנט, שבו הדפדפן מריץ קוד JavaScript כדי לעדכן את הדף הקיים כשהמשתמש מבקר בקטע חדש. גם SPA וגם MPA הם מודלים תקפים לשימוש, אבל בפוסט הזה רציתי לבחון את המושגים של PWA בהקשר של אפליקציה מרובת דפים.
מהיר ואמין
שמעת אותי (ואנשים רבים אחרים) משתמש בביטוי "אפליקציית אינטרנט מתקדמת" או PWA. יכול להיות שחלק מהמידע הרקע כבר מוכר לכם, כי הוא מופיע במקומות אחרים באתר הזה.
אפשר לחשוב על PWA כאפליקציית אינטרנט שמספקת חוויית משתמש ברמה גבוהה, ושבאמת ראויה למקום במסך הבית של המשתמש. הראשי תיבות FIRE מייצגות את המילים Fast (מהיר), Integrated (משולב), Reliable (מהימן) ו-Engaging (מושך), ומתארות את כל המאפיינים שצריך לקחת בחשבון כשמפתחים PWA.
במאמר הזה אתמקד בקבוצת משנה של המאפיינים האלה: מהיר ואמין.
מהיר: המונח 'מהיר' מקבל משמעויות שונות בהקשרים שונים, אבל אני אדבר על היתרונות של טעינה מהרשת של כמה שפחות נתונים.
אמינות: אבל מהירות גולמית לא מספיקה. כדי שאפליקציית האינטרנט תפעל כמו אפליקציית PWA, היא צריכה להיות אמינה. הוא צריך להיות עמיד מספיק כדי לטעון משהו תמיד, גם אם זה רק דף שגיאה מותאם אישית, בלי קשר למצב הרשת.
מהירות אמינה: לסיום, אנסח מחדש את ההגדרה של PWA ואסביר מה זה אומר לבנות משהו שהוא מהיר באופן אמין. לא מספיק שהחיבור יהיה מהיר ואמין רק כשאתם מחוברים לרשת עם זמן אחזור נמוך. מהירות אמינה פירושה שהמהירות של אפליקציית האינטרנט עקבית, ללא קשר לתנאי הרשת הבסיסיים.
טכנולוגיות מאפשרות: Service Workers + Cache Storage API
אפליקציות PWA מציבות רף גבוה של מהירות ועמידות. למזלנו, פלטפורמת האינטרנט מציעה כמה אבני בניין שיעזרו לכם להשיג ביצועים כאלה. הכוונה שלי היא ל-service workers ול-Cache Storage API.
אתם יכולים ליצור Service Worker שמקשיב לבקשות נכנסות, מעביר חלק מהן לרשת ושומר עותק של התגובה לשימוש עתידי באמצעות Cache Storage API.

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

הימנעות מהרשת ככל האפשר היא חלק חשוב בהבטחת ביצועים מהירים ואמינים.
JavaScript איזומורפי
מושג נוסף שאני רוצה להסביר הוא מה שלפעמים נקרא JavaScript איזומורפי או אוניברסלי. במילים פשוטות, מדובר ברעיון שאותו קוד JavaScript יכול להיות משותף בין סביבות זמן ריצה שונות. כשבניתי את ה-PWA, רציתי לשתף קוד JavaScript בין שרת הקצה העורפי שלי לבין ה-service worker.
יש הרבה גישות תקפות לשיתוף קוד בדרך הזו, אבל הגישה שלי הייתה להשתמש במודולים של ES כמקור הקוד הסופי. לאחר מכן, ביצעתי טרנספילציה של המודולים האלה וצרפתי אותם לחבילה עבור השרת ועובד השירות באמצעות שילוב של Babel ו-Rollup. בפרויקט שלי, קבצים עם סיומת הקובץ .mjs
הם קוד שנמצא במודול ES.
השרת
אחרי שהסברתי את המושגים והטרמינולוגיה, אפרט עכשיו איך בניתי את אפליקציית ה-PWA של Stack Overflow. אני אתחיל בהסבר על שרת הקצה העורפי שלנו, ואסביר איך הוא משתלב בארכיטקטורה הכוללת.
חיפשתי שילוב של קצה עורפי דינמי עם אירוח סטטי, והגישה שלי הייתה להשתמש בפלטפורמת Firebase.
Firebase Cloud Functions יפעיל באופן אוטומטי סביבה מבוססת-Node כשתתקבל בקשה נכנסת, וישתלב עם מסגרת ה-HTTP הפופולרית Express, שכבר הכרתי. הוא גם מציע אירוח מוכן לשימוש לכל המשאבים הסטטיים באתר שלי. בואו נראה איך השרת מטפל בבקשות.
כשדפדפן שולח בקשת ניווט לשרת שלנו, היא עוברת את התהליך הבא:

השרת מנתב את הבקשה על סמך כתובת ה-URL, ומשתמש בלוגיקה של תבניות כדי ליצור מסמך HTML מלא. אני משתמש בשילוב של נתונים מ-Stack Exchange API, וגם בקטעי HTML חלקיים שהשרת מאחסן באופן מקומי. אחרי ש-Service Worker יודע איך להגיב, הוא יכול להתחיל לשדר HTML בחזרה לאפליקציית האינטרנט שלנו.
יש שני חלקים בתמונה הזו שכדאי לבדוק בפירוט רב יותר: ניתוב ותבניות.
ניתוב
בנוגע לניתוב, הגישה שלי הייתה להשתמש בתחביר הניתוב המקורי של מסגרת Express. היא גמישה מספיק כדי להתאים לקידומות פשוטות של כתובות URL, וגם לכתובות URL שכוללות פרמטרים כחלק מהנתיב. בשלב הזה יוצרים מיפוי בין שמות של מסלולים לבין התבנית הבסיסית של Express שצריך להתאים לה.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index&
#39;, '/'],
]);
export default routes;
אחר כך אוכל להפנות למיפוי הזה ישירות מהקוד של השרת. כשנמצאת התאמה לתבנית Express נתונה, הפונקציה המתאימה לטיפול בבקשה מגיבה עם לוגיקה של תבניות שספציפית לנתיב התואם.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), as>ync (req, res) = {
// Templa
ting logic.
});
תבניות בצד השרת
איך נראית לוגיקת התבניות הזו? אני בחרתי בגישה שבה הרכבתי רצף של קטעי HTML חלקיים, אחד אחרי השני. המודל הזה מתאים במיוחד להזרמה.
השרת שולח בחזרה קוד HTML ראשוני באופן מיידי, והדפדפן יכול לעבד את הדף החלקי הזה מיד. כשהשרת מרכיב את שאר מקורות הנתונים, הוא מעביר אותם בדפדפן בסטרימינג עד שהמסמך מושלם.
כדי להבין למה אני מתכוון, אפשר לעיין בקוד Express של אחד מהנתיבים שלנו:
app.get(routes.get('index'), async (req>, res) = {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end
();
});
באמצעות השיטה write()
של האובייקט response
, ובהפניה לתבניות חלקיות שמאוחסנות באופן מקומי, אני יכול להתחיל את הזרם של התגובה באופן מיידי, בלי לחסום מקור נתונים חיצוני. הדפדפן לוקח את ה-HTML הראשוני הזה ומציג ממשק משמעותי והודעת טעינה באופן מיידי.
בחלק הבא של הדף אנחנו משתמשים בנתונים מ-Stack Exchange API. כדי לקבל את הנתונים האלה, השרת שלנו צריך לשלוח בקשת רשת. אפליקציית האינטרנט לא יכולה להציג שום דבר אחר עד שהיא מקבלת תשובה ומעבדת אותה, אבל לפחות המשתמשים לא מסתכלים על מסך ריק בזמן ההמתנה.
אחרי שאפליקציית האינטרנט מקבלת את התגובה מ-Stack Exchange API, היא קוראת לפונקציית תבניות בהתאמה אישית כדי לתרגם את הנתונים מה-API ל-HTML המתאים.
שפת תבניות
יצירת תבניות יכולה להיות נושא שנוי במחלוקת, והגישה שבה השתמשתי היא רק אחת מתוך רבות. כדאי להשתמש בפתרון משלכם, במיוחד אם יש לכם קשרים קודמים למסגרת קיימת של תבניות.
במקרה השימוש שלי, היה הגיוני להסתמך רק על תבניות מילוליות של JavaScript, עם קצת לוגיקה שמופרדת לפונקציות עזר. אחד היתרונות בבניית MPA הוא שלא צריך לעקוב אחרי עדכוני מצב ולבצע רינדור מחדש של ה-HTML. לכן, גישה בסיסית שיוצרת HTML סטטי עבדה בשבילי.
לכן, הנה דוגמה לאופן שבו אני יוצר תבנית לחלק הדינמי של קוד ה-HTML של דף האינדקס של אפליקציית האינטרנט שלי. בדומה למסלולים שלי, לוגיקת התבניות מאוחסנת במודול ES שאפשר לייבא גם לשרת וגם ל-service worker.
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}"< Qu>estions/h3`;
cons<t form = `form me>tho<d=&qu>ot;GET".../form`;
const questionCards = i>tems
.map(item =
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('&<#39;);
const que>stions = `div id<=&qu>ot;questions"${questionCards}/div`;
return title + form + questions;
}
פונקציות התבנית האלה הן JavaScript טהור, וכדאי לפצל את הלוגיקה לפונקציות עזר קטנות יותר כשמתאים. במקרה הזה, אני מעביר כל אחד מהפריטים שמוחזרים בתשובת ה-API לפונקציה כזו, שיוצרת רכיב HTML רגיל עם כל המאפיינים המתאימים.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url=>"${<qu>estio
nUrl(id)}"${title}/a`;
}
חשוב במיוחד מאפיין הנתונים
שאני מוסיף לכל קישור, data-cache-url
, שמוגדר לכתובת ה-URL של Stack Exchange API שאני צריך כדי להציג את השאלה המתאימה. חשוב לזכור את זה. אחזור לזה מאוחר יותר.
אם חוזרים לפונקציית הטיפול בבקשות, אחרי שהתבנית מוכנה, אני מעביר את החלק האחרון של קוד ה-HTML של הדף לדפדפן ומסיים את ההעברה. זהו האות לדפדפן שהרינדור המתקדם הסתיים.
app.get(routes.get('index'), async (req>, res) = {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end
();
});
זה היה סיור קצר בהגדרת השרת שלי. משתמשים שנכנסים לאפליקציית האינטרנט שלי בפעם הראשונה תמיד יקבלו תגובה מהשרת, אבל כשמבקר חוזר לאפליקציית האינטרנט שלי, ה-service worker שלי יתחיל להגיב. בואו נתחיל.
קובץ השירות (service worker)

התרשים הזה אמור להיראות מוכר – הרבה מהחלקים שתיארתי קודם מופיעים כאן בסידור קצת שונה. בואו נסביר את תהליך הבקשה, תוך התייחסות ל-Service Worker.
ה-service worker מטפל בבקשת ניווט נכנסת לכתובת URL מסוימת, ובדומה לשרת שלי, הוא משתמש בשילוב של לוגיקת ניתוב ותבניות כדי להבין איך להגיב.
הגישה זהה לגישה הקודמת, אבל עם פרימיטיבים שונים ברמה נמוכה, כמו fetch()
ו-Cache Storage API. אני משתמש במקורות הנתונים האלה כדי ליצור את תגובת ה-HTML, ש-service worker מעביר בחזרה לאפליקציית האינטרנט.
Workbox
במקום להתחיל מאפס עם פרימיטיבים ברמה נמוכה, אבנה את ה-service worker שלי על בסיס קבוצה של ספריות ברמה גבוהה שנקראות Workbox. הוא מספק בסיס מוצק ללוגיקה של שמירת נתונים במטמון, ניתוב ויצירת תגובות של כל Service Worker.
ניתוב
בדיוק כמו בקוד בצד השרת, ה-service worker צריך לדעת איך להתאים בקשה נכנסת ללוגיקת התגובה המתאימה.
הגישה שלי הייתה לתרגם כל נתיב Express לביטוי רגולרי תואם, באמצעות ספרייה שימושית בשם regexparam
. אחרי התרגום, אוכל להשתמש בתמיכה המובנית של Workbox בניתוב ביטויים רגולריים.
אחרי שמייבאים את המודול עם הביטויים הרגולריים, רושמים כל ביטוי רגולרי בנתב של Workbox. בתוך כל מסלול אני יכול לספק לוגיקה מותאמת אישית של תבניות כדי ליצור תשובה. השימוש בתבניות בקובץ השירות (service worker) קצת יותר מורכב מאשר בשרת העורפי שלי, אבל Workbox עוזר עם הרבה מהעבודה הקשה.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templ
ating logic.
);
שמירה במטמון של נכסים סטטיים
חלק חשוב בתהליך יצירת התבניות הוא לוודא שתבניות ה-HTML החלקיות שלי זמינות באופן מקומי דרך Cache Storage API, ושהן מתעדכנות כשאני פורס שינויים באפליקציית האינטרנט. תחזוקת מטמון יכולה להיות מועדת לשגיאות כשמבצעים אותה באופן ידני, ולכן אני משתמש ב-Workbox כדי לטפל בטרום-אחסון במטמון כחלק מתהליך הבנייה שלי.
אני מציין ל-Workbox אילו כתובות URL להוסיף למטמון מראש באמצעות קובץ הגדרות, שמצביע על הספרייה שמכילה את כל הנכסים המקומיים שלי, יחד עם קבוצה של תבניות להתאמה. הקובץ הזה נקרא באופן אוטומטי על ידי ה-CLI של Workbox, שמופעל בכל פעם שאני בונה מחדש את האתר.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Othe
r options...
};
Workbox מצלם תמונה של תוכן כל קובץ, ומזריק אוטומטית את רשימת כתובות ה-URL והעדכונים האלה לקובץ הסופי של ה-service worker. עכשיו יש ל-Workbox את כל מה שצריך כדי שהקבצים שנשמרו מראש במטמון יהיו זמינים תמיד ומעודכנים. התוצאה היא קובץ service-worker.js
שמכיל משהו דומה לזה:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746
a9ecc6',
},
// etc.
]);
למשתמשים בתהליך בנייה מורכב יותר, ל-Workbox יש תוסף webpack
ומודול node גנרי, בנוסף לממשק שורת הפקודה.
סטרימינג
בשלב הבא, אני רוצה שקובץ השירות (service worker) ישדר את ה-HTML החלקי ששמור במטמון מראש בחזרה לאפליקציית האינטרנט באופן מיידי. זה חלק חשוב מאוד בלהיות "מהיר באופן מהימן" – תמיד מקבלים משהו משמעותי על המסך באופן מיידי. למזלנו, אפשר לעשות את זה באמצעות Streams API בתוך service worker.
יכול להיות ששמעתם על Streams API בעבר. הקולגה שלי, ג'ייק ארצ'יבלד, משבח אותו כבר שנים. הוא ניבא שנת 2016 תהיה השנה של מקורות נתונים לאתרים. ממשק Streams API עדיין מצוין כמו שהיה לפני שנתיים, אבל עם הבדל חשוב.
בעבר רק Chrome תמך ב-Streams, אבל עכשיו יש תמיכה רחבה יותר ב-Streams API. הסיפור הכללי חיובי, ועם קוד מתאים לחזרה למצב הקודם, אין שום דבר שימנע מכם להשתמש בסטרימינג ב-service worker שלכם היום.
יכול להיות שיש דבר אחד שמונע ממך לעשות את זה, והוא להבין איך Streams API באמת עובד. הוא חושף קבוצה חזקה מאוד של פרימיטיבים, ומפתחים שמכירים את השימוש בו יכולים ליצור זרימות נתונים מורכבות, כמו הבאות:
const stream = new ReadableStream({
pull(controller) {
return sources[0]
.then(r => r.read())
.then(result => {
if (result.done) {
sources.shift();
if (sources.length === 0) return controller.close();
return this.pull(controller);
} else {
controller.enqueue(result.value);
}
});
},
});
אבל לא כולם יכולים להבין את ההשלכות המלאות של הקוד הזה. במקום לנתח את הלוגיקה הזו, בואו נדבר על הגישה שלי להזרמת Service Worker.
אני משתמש במעטפת חדשה לגמרי ברמה גבוהה,
workbox-streams
.
אני יכול להעביר אותו בתערובת של מקורות סטרימינג, גם ממטמון וגם מנתוני זמן ריצה שיכולים להגיע מהרשת. Workbox מתאם בין המקורות השונים ומאחד אותם לתגובה אחת שמוזרמת.
בנוסף, Workbox מזהה באופן אוטומטי אם יש תמיכה ב-Streams API, ואם אין תמיכה כזו, הוא יוצר תגובה מקבילה שאינה סטרימינג. המשמעות היא שלא צריך לדאוג לגבי כתיבת חלופות, כי הסטרימינג מתקרב לתמיכה של 100% בדפדפנים.
שמירה במטמון בזמן ריצה
בואו נבדוק איך service worker שלי מתמודד עם נתוני זמן ריצה מ-Stack Exchange API. אני משתמש בתמיכה המובנית של Workbox באסטרטגיית שמירת נתונים במטמון מסוג stale-while-revalidate, וגם בתפוגה, כדי לוודא שהאחסון של אפליקציית האינטרנט לא יגדל ללא הגבלה.
הגדרתי שתי אסטרטגיות ב-Workbox כדי לטפל במקורות השונים שמהם תורכב התגובה בסטרימינג. בעזרת Workbox, בכמה קריאות לפונקציות והגדרות, אנחנו יכולים לבצע פעולות שדורשות אחרת מאות שורות של קוד שנכתב ידנית.
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});
const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});
האסטרטגיה הראשונה קוראת נתונים שנשמרו במטמון מראש, כמו תבניות ה-HTML החלקיות שלנו.
בשיטה השנייה מיושמת לוגיקת שמירת נתונים במטמון מסוג stale-while-revalidate, יחד עם תפוגה של נתונים במטמון מסוג least-recently-used (הנתונים שהשימוש בהם היה הכי מזמן) ברגע שמגיעים ל-50 רשומות.
אחרי שהגדרתי את האסטרטגיות האלה, כל מה שנשאר לי לעשות זה להסביר ל-Workbox איך להשתמש בהן כדי ליצור תגובה מלאה שמועברת בסטרימינג. אני מעביר מערך של מקורות כפונקציות, וכל אחת מהפונקציות האלה תופעל באופן מיידי. Workbox לוקח את התוצאה מכל מקור ומעביר אותה לאפליקציית האינטרנט ברצף, ורק אם הפונקציה הבאה במערך עדיין לא הסתיימה.
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'})>,
() = cacheStrategy.makeRequest({request: '/navbar.html'}),
async >({event, url}) = {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(...);
const data = await listResponse.json();
return templates.index(tag, >data.items);
},
() = cacheStrategy.makeRequest({reque
st: '/foot.html'}),
]);
שני המקורות הראשונים הם תבניות חלקיות שמאוחסנות במטמון מראש ונשלפות ישירות מ-Cache Storage API, כך שהן תמיד יהיו זמינות באופן מיידי. כך נבטיח שההטמעה של Service Worker תגיב לבקשות במהירות ובאופן מהימן, בדיוק כמו הקוד בצד השרת.
פונקציית המקור הבאה מאחזרת נתונים מ-Stack Exchange API ומעבדת את התגובה ל-HTML שאפליקציית האינטרנט מצפה לו.
האסטרטגיה stale-while-revalidate אומרת שאם יש לי תגובה ששמורה במטמון לקריאה הזו ל-API, אוכל להזרים אותה לדף באופן מיידי, תוך כדי עדכון של רשומת המטמון ברקע לקראת הפעם הבאה שתישלח בקשה.
לבסוף, אני משדר עותק שמור במטמון של הכותרת התחתונה וסוגר את תגי ה-HTML הסופיים, כדי להשלים את התגובה.
קוד השיתוף מאפשר לשמור על סנכרון
תשימו לב שחלקים מסוימים בקוד של Service Worker נראים מוכרים. הלוגיקה של תבניות ו-HTML חלקי שבהם נעשה שימוש בקובץ שירות (service worker) זהה ללוגיקה שבה נעשה שימוש ב-handler בצד השרת. שיתוף הקוד הזה מבטיח שהמשתמשים יקבלו חוויה עקבית, בין אם הם מבקרים באפליקציית האינטרנט שלי בפעם הראשונה או חוזרים לדף שעבר עיבוד על ידי Service Worker. זה היופי של JavaScript איזומורפי.
שיפורים דינמיים הדרגתיים
בדקתי את השרת ואת ה-service worker של ה-PWA שלי, אבל יש עוד קטע לוגיקה אחד שצריך להסביר: יש כמות קטנה של JavaScript שמופעלת בכל אחד מהדפים שלי, אחרי שהם מוזרמים במלואם.
הקוד הזה משפר בהדרגה את חוויית המשתמש, אבל הוא לא חיוני – אפליקציית האינטרנט תמשיך לפעול גם אם הוא לא יופעל.
מטא-נתונים של דף
האפליקציה שלי משתמשת ב-JavaScript בצד הלקוח כדי לעדכן את המטא-נתונים של דף מסוים על סמך התגובה של ה-API. מכיוון שאני משתמש באותו קטע ראשוני של HTML שמור במטמון לכל דף, אפליקציית האינטרנט מסתיימת עם תגים כלליים בראש המסמך שלי. אבל באמצעות תיאום בין התבניות שלי לבין הקוד בצד הלקוח, אני יכול לעדכן את הכותרת של החלון באמצעות מטא-נתונים ספציפיים לדף.
כחלק מקוד התבנית, הגישה שלי היא לכלול תג script שמכיל את המחרוזת עם התו הנכון לביטול בריחה.
const metadataScript = `<script>
self._title = '${escape(item.title)<}';>
/s
cript`;
אחר כך, אחרי שהדף נטען, אני קורא את המחרוזת ומעדכן את שם המסמך.
if (self._title) {
document.title = unescape(self._title);
}
אם יש עוד חלקי מטא-נתונים ספציפיים לדף שאתם רוצים לעדכן באפליקציית האינטרנט שלכם, אתם יכולים לפעול באותו אופן.
חוויית משתמש אופליין
השיפור המתקדם השני שהוספתי משמש כדי להדגיש את היכולות שלנו במצב אופליין. בניתי PWA אמין, ואני רוצה שהמשתמשים ידעו שכשהם במצב אופליין, הם עדיין יכולים לטעון דפים שהם ביקרו בהם בעבר.
קודם כל, אני משתמש ב-Cache Storage API כדי לקבל רשימה של כל בקשות ה-API ששמורים במטמון, ואני מתרגם את הרשימה הזו לרשימה של כתובות URL.
זוכר את מאפייני הנתונים המיוחדים שדיברתי עליהם, שכל אחד מהם מכיל את כתובת ה-URL של בקשת ה-API שנדרשת כדי להציג שאלה? אני יכול להשוות את מאפייני הנתונים האלה לרשימת כתובות ה-URL שנשמרו במטמון, וליצור מערך של כל הקישורים לשאלות שלא תואמים.
כשהדפדפן עובר למצב אופליין, אני עובר בלולאה על רשימת הקישורים שלא נשמרו במטמון, ומחליש את הבהירות של הקישורים שלא יפעלו. חשוב לזכור שזו רק רמז ויזואלי למשתמש לגבי מה שהוא צריך לצפות מהדפים האלה – אני לא משבית את הקישורים או מונע מהמשתמש לנווט.
const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);
const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filte>r(card = {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandle>r = () = {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onli>neHandler = () = {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListe
ner('offline', offlineHandler);
טעויות נפוצות
הסברתי עכשיו את הגישה שלי ליצירת PWA עם כמה דפים. יש הרבה גורמים שצריך לקחת בחשבון כשמגבשים גישה משלכם, ויכול להיות שבסופו של דבר תבחרו באפשרויות שונות משלי. הגמישות הזו היא אחד היתרונות הגדולים של פיתוח לאינטרנט.
יש כמה טעויות נפוצות שאפשר לעשות כשמקבלים החלטות לגבי הארכיטקטורה, ואני רוצה לעזור לכם להימנע מהן.
לא מומלץ לשמור במטמון HTML מלא
לא מומלץ לאחסן במטמון מסמכי HTML מלאים. קודם כל, זה בזבוז של מקום. אם אפליקציית האינטרנט שלכם משתמשת באותה מבנה בסיסי של HTML לכל אחד מהדפים שלה, בסופו של דבר תאחסנו עותקים של אותו תג עיצוב שוב ושוב.
חשוב מכך, אם תפעילו שינוי במבנה ה-HTML המשותף של האתר, כל אחד מהדפים ששמורים במטמון עדיין יציג את הפריסה הישנה. תארו לעצמכם את התסכול של מבקר חוזר שרואה שילוב של דפים ישנים וחדשים.
סחף של שרת / service worker
בעיה נוספת שכדאי להימנע ממנה היא חוסר סנכרון בין השרת לבין Service Worker. הגישה שלי הייתה להשתמש ב-JavaScript איזומורפי, כדי שאותו קוד יפעל בשני המקומות. יכול להיות שזה לא תמיד אפשרי, בהתאם לארכיטקטורת השרת הקיימת.
לא משנה אילו החלטות ארכיטקטוניות תקבלו, כדאי שתהיה לכם אסטרטגיה להרצת קוד שקול של ניתוב ותבניות בשרת וב-service worker.
תרחישים של מקרי קצה
פריסה או עיצוב לא עקביים
מה קורה כשמתעלמים מהבעיות האלה? ובכן, יכולות להיות כל מיני תקלות, אבל התרחיש הגרוע ביותר הוא שמשתמש חוזר יבקר בדף שנשמר במטמון עם פריסה ישנה מאוד – אולי כזו עם טקסט בכותרת שכבר לא עדכני, או כזו שמשתמשת בשמות של מחלקות CSS שכבר לא תקפים.
תרחיש הגרוע ביותר: ניתוב שבור
לחלופין, משתמש יכול להיתקל בכתובת URL שמטופלת על ידי השרת שלכם, אבל לא על ידי ה-service worker. אתר מלא בפריסות זומבי ובמבוי סתום הוא לא PWA אמין.
טיפים להצלחה
אבל אתם לא לבד! הטיפים הבאים יעזרו לכם להימנע מהבעיות האלה:
שימוש בספריות של תבניות וניתוב שיש להן הטמעות בכמה שפות
כדאי לנסות להשתמש בספריות של תבניות וניתוב שיש להן הטמעות של JavaScript. אני יודע שלא לכל מפתח יש את הפריבילגיה
אבל יש כמה מסגרות פופולריות של תבניות וניתוב עם הטמעות בכמה שפות. אם תמצאו אחת שתומכת ב-JavaScript וגם בשפה של השרת הנוכחי, תהיו קרובים יותר להשגת סנכרון בין ה-service worker לבין השרת.
העדפה של תבניות עוקבות ולא תבניות מקוננות
לאחר מכן, מומלץ להשתמש בסדרה של תבניות עוקבות שאפשר להזרים אחת אחרי השנייה. אין בעיה אם בחלקים מאוחרים יותר של הדף נעשה שימוש בלוגיקה מורכבת יותר של תבניות, כל עוד אפשר להזרים את החלק הראשוני של ה-HTML במהירות האפשרית.
שמירה במטמון של תוכן סטטי ודינמי ב-service worker
כדי ליהנות מהביצועים הכי טובים, כדאי לשמור במטמון מראש את כל המשאבים הסטטיים הקריטיים של האתר. כדאי גם להגדיר לוגיקה של שמירת נתונים במטמון בזמן ריצה כדי לטפל בתוכן דינמי, כמו בקשות API. השימוש ב-Workbox מאפשר לכם להסתמך על אסטרטגיות שנבדקו היטב ומוכנות לייצור, במקום להטמיע את הכל מאפס.
חסימה ברשת רק כשאין ברירה אחרת
בנוסף, צריך לחסום ברשת רק כשאי אפשר להזרים תגובה מהמטמון. הצגה מיידית של תגובת API שנשמרה במטמון יכולה לשפר את חוויית המשתמש בהשוואה להמתנה לנתונים חדשים.