שיפור הדרגתי של Progressive Web App

פיתוח לדפדפנים מודרניים ושיפור הדרגתי כמו שעשינו ב-2003

במרץ 2003, ניק פינק וסטיב צ'מפיון הדהימו את עולם עיצוב האינטרנט עם הרעיון של שיפור הדרגתי – אסטרטגיה לעיצוב אינטרנט שמתמקדת תחילה בטעינה של תוכן הליבה של דף האינטרנט, ולאחר מכן מוסיפים בהדרגה שכבות עשירות יותר של הצגה ותכונות, עם דגש על איכות טכנית גבוהה. בשנת 2003, שיפור הדרגתי התבסס על שימוש בתכונות מודרניות של CSS, ב-JavaScript לא פולשני ואפילו רק ב-Scalable Vector Graphics. שיפור הדרגתי בשנת 2020 ואילך הוא שימוש ביכולות מודרניות של דפדפנים.

עיצוב אינטרנט כוללני לעתיד באמצעות שיפור הדרגתי. שקף הכותרת מהמצגת המקורית של Finck ו-Champeon.
תצוגה: עיצוב אינקלוסיבי של אתרים לעתיד באמצעות שיפורים הדרגתיים. (מקור)

JavaScript מודרני

בנוגע ל-JavaScript, התמיכה בדפדפנים בתכונות ה-JavaScript העדכניות ביותר של הליבה של ES 2015 מצוינת. התקן החדש כולל הבטחות (promises), מודולים, כיתות, ליבות תבנית, פונקציות חץ, let ו-const, פרמטרים שמוגדרים כברירת מחדל, גנרטורים, הקצאה לניתוח מבנה (destructuring), ‎rest ו-‎spread, Map/Set, WeakMap/WeakSet ועוד הרבה. כל האפשרויות נתמכות.

טבלת התמיכה של CanIUse בתכונות של ES6, שמציגה את התמיכה בכל הדפדפנים העיקריים.
טבלת התמיכה בדפדפנים ב-ECMAScript 2015‏ (ES6). (מקור)

פונקציות אסינכררוניות (async) הן תכונה של ES 2017 ואחת מהתכונות האהובות עלי, ואפשר להשתמש בהן בכל הדפדפנים העיקריים. מילות המפתח async ו-await מאפשרות לכתוב התנהגות אסינכררונית שמבוססת על הבטחה בסגנון נקי יותר, בלי צורך להגדיר מחרוזות הבטחה באופן מפורש.

טבלת התמיכה של CanIUse לפונקציות אסינכררוניות, שמציגה את התמיכה בכל הדפדפנים העיקריים.
טבלת התמיכה בדפדפנים של פונקציות Async. (מקור)

אפילו שפות שנוספו לאחרונה מאוד ל-ES 2020, כמו שרשרת אופציונלית ושילוב של nullish, קיבלו תמיכה מהר מאוד. בהמשך אפשר לראות דוגמת קוד. כשמדובר בתכונות הליבה של JavaScript, הדשא לא יכול להיות ירוק הרבה יותר ממה שהוא היום.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
תמונת הרקע הירוקה האיקונית של Windows XP.
כשמדובר בתכונות הליבה של JavaScript, המצב טוב. (צילום מסך של מוצר של Microsoft, שנעשה בו שימוש עם הרשאה).

האפליקציה לדוגמה: Fugu Greetings

לצורך המאמר הזה אנחנו עובדים עם PWA פשוטה, בשם Fugu Greetings (GitHub). שם האפליקציה הזה הוא מחווה ל-Project Fugu 🐡, מאמץ לתת לאינטרנט את כל התכונות של אפליקציות ל-Android, ל-iOS ולמחשב. מידע נוסף על הפרויקט זמין בדף הנחיתה שלו.

Fugu Greetings היא אפליקציית ציור שמאפשרת ליצור כרטיסי ברכה וירטואליים ולשלוח אותם למי שאתם אוהבים. הוא מדגים את המושגים המרכזיים של אפליקציות PWA. הוא מהימן ומאפשר שימוש מלא במצב אופליין, כך שגם אם אין לכם חיבור לרשת, עדיין תוכלו להשתמש בו. אפשר גם להתקין את האפליקציה במסך הבית של המכשיר, והיא משתלבת בצורה חלקה עם מערכת ההפעלה כאפליקציה עצמאית.

אפליקציית PWA של Fugu Greetings עם ציור שדומה ללוגו של קהילת ה-PWA.
האפליקציה לדוגמה Fugu Greetings.

שיפור הדרגתי

עכשיו אפשר להמשיך לדבר על שיפור הדרגתי. במילון של MDN Web Docs מוגדר המונח כך:

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

בדרך כלל משתמשים בזיהוי תכונות כדי לקבוע אם הדפדפנים יכולים לטפל בפונקציונליות מודרנית יותר, ואילו בפוליפילים משתמשים לרוב כדי להוסיף תכונות חסרות באמצעות JavaScript.

[…]

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

שותפים ל-MDN

יצירה של כל כרטיס ברכה מאפס יכולה להיות מסורבלת מאוד. אז למה לא להוסיף תכונה שמאפשרת למשתמשים לייבא תמונה ולהתחיל ממנה? בגישה מסורתית, הייתם משתמשים ברכיב <input type=file> כדי לעשות זאת. קודם צריך ליצור את הרכיב, להגדיר את type שלו ל-'file' ולהוסיף סוגי MIME לנכס accept, ואז "ללחוץ" עליו באופן פרוגרמטי ולהאזין לשינויים. כשאתם בוחרים תמונה, היא מיובאת ישירות ללוח הציור.

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

אם יש תכונה של ייבוא, סביר להניח שצריכה להיות גם תכונה של ייצוא כדי שהמשתמשים יוכלו לשמור את כרטיסי הברכה באופן מקומי. הדרך המסורתית לשמירת קבצים היא ליצור קישור עוגן עם מאפיין download וכתובת URL של blob כ-href. צריך גם "ללחוץ" עליו באופן פרוגרמטי כדי להפעיל את ההורדה, ולמנוע דליפות זיכרון, חשוב לא לשכוח לבטל את כתובת ה-URL של אובייקט ה-blob.

const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

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

מה אם יש דרך טובה יותר? מה אם תוכלו פשוט לפתוח קובץ מקומי, לערוך אותו ואז לשמור את השינויים בקובץ חדש או בקובץ המקורי שפתחתם בהתחלה? מסתבר שה-File System Access API מאפשר לפתוח וליצור קבצים וספריות, וגם לשנות ולשמור אותם.

אז איך מזהים תכונות API? ב-File System Access API מוצגת שיטה חדשה, window.chooseFileSystemEntries(). כתוצאה מכך, עליי לטעון באופן מותנה מודולים שונים של ייבוא וייצוא, בהתאם לזמינות של השיטה הזו. בהמשך מוסבר איך לעשות זאת.

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

לפני שנכנס לפרטים של File System Access API, אבקש להדגיש במהירות את דפוס השיפור ההדרגתי. בדפדפנים שלא תומכים כרגע ב-File System Access API, אני טוען את הסקריפטים מדור קודם. בהמשך מפורטות הכרטיסיות של הרשתות ב-Firefox וב-Safari.

בכלי לבדיקת האינטרנט של Safari מוצגים הקבצים הקודמים שנטענים.
כרטיסיית הרשת של Web Inspector ב-Safari.
הכלים למפתחים של Firefox שבהם מוצגים הקבצים הקודמים שנטענים.
כרטיסיית הרשת של Firefox Developer Tools.

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

כלי הפיתוח של Chrome שמוצגים בו הקבצים המודרניים שנטענים.
כרטיסיית הרשת של כלי הפיתוח ל-Chrome.

File System Access API

עכשיו, אחרי שסיימתי להתייחס לנושא הזה, הגיע הזמן לבחון את ההטמעה בפועל על סמך File System Access API. כדי לייבא תמונה, אני קורא ל-window.chooseFileSystemEntries() ומעביר לה מאפיין accepts שבו אני רוצה ליצור קובצי תמונה. יש תמיכה גם בסיומות הקבצים וגם בסוגי MIME. הפעולה הזו יוצרת מאחז קובץ, שממנו אפשר לקבל את הקובץ בפועל באמצעות קריאה ל-getFile().

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

הייצוא של תמונה הוא כמעט זהה, אבל הפעם צריך להעביר למתודה chooseFileSystemEntries() פרמטר מסוג 'save-file'. מכאן מופיעה תיבת דו-שיח לשמירת קובץ. כשהקובץ פתוח, לא צריך לעשות זאת כי 'open-file' היא ברירת המחדל. הגדרתי את הפרמטר accepts כמו קודם, אבל הפעם רק תמונות בפורמט PNG. שוב מקבלים מאחזר קובץ, אבל במקום לקבל את הקובץ, הפעם יוצרים מקור כתיבה באמצעות קריאה ל-createWritable(). בשלב הבא, כותבים לקובץ את ה-blob, שהוא התמונה של כרטיס האיחול. בסוף, סוגרים את הסטרימינג לכתיבה.

תמיד יכול להיכשל: יכול להיות שנגמר המקום בדיסק, יכול להיות שיש שגיאת כתיבה או קריאה או פשוט שהמשתמש מבטל את תיבת הדו-שיח של הקובץ. לכן אני תמיד כולל את השיחות בהצהרת try...catch.

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

אני יכול לפתוח קובץ כמו קודם באמצעות התכונה 'שיפור התקדמות' עם File System Access API. הקובץ המיובא מצויר ישירות על הלוח. אני יכול לבצע את העריכות ולשמור אותן באמצעות תיבת דו-שיח אמיתית לשמירה, שבה אוכל לבחור את השם ואת מיקום האחסון של הקובץ. עכשיו הקובץ מוכן לשמירה לנצח.

אפליקציית Fugu Greetings עם תיבת דו-שיח לפתיחת קובץ.
תיבת הדו-שיח לפתיחת הקובץ.
אפליקציית Fugu Greetings כוללת עכשיו תמונה מיובאת.
התמונה שיובאה.
אפליקציית Fugu Greetings עם התמונה ששונתה.
שמירת התמונה ששונתה בקובץ חדש.

ממשקי ה-API של Web Share ו-Web Share Target

מלבד שמירת התמונה לתמיד, אולי אני רוצה לשתף את כרטיס האיחול שלי. זה משהו שאפשר לעשות באמצעות Web Share API ו-Web Share Target API. למערכות הפעלה לניידים, ולאחרונה גם למחשבים, יש מנגנוני שיתוף מובנים. לדוגמה, בהמשך מוצגת גיליון השיתוף של Safari למחשב ב-MacOS, שהופעל ממאמר בבלוג שלי. כשאתם לוחצים על הלחצן שיתוף הכתבה, אתם יכולים לשתף קישור לכתבה עם חבר, למשל דרך אפליקציית Messages ב-macOS.

גיליון השיתוף של Safari במחשב ב-macOS, שהופעלה על ידי לחצן השיתוף בכתבה
Web Share API ב-Safari למחשב ב-macOS.

הקוד שצריך כדי לעשות זאת הוא פשוט למדי. אני קורא ל-navigator.share() ומעביר לו שדות אופציונליים, title, text ו-url באובייקט. אבל מה קורה אם רוצים לצרף תמונה? עדיין אין תמיכה בכך ברמה 1 של Web Share API. החדשות הטובות הן שרמת שיתוף האינטרנט 2 כוללת יכולות שיתוף קבצים.

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

אראה לך איך לעשות את זה עם אפליקציית כרטיסי הפתיחה של Fugu. קודם כול, צריך להכין אובייקט data עם מערך files שמכיל blob אחד, ואז title ו-text. בשלב הבא, השיטה המומלצת היא navigator.canShare() החדשה, שעושה את מה ששמה מרמז: היא אומרת אם הדפדפן יכול לשתף באופן טכני את האובייקט data שאני מנסה לשתף. אם navigator.canShare() יגיד לי שאפשר לשתף את הנתונים, אהיה מוכן להתקשר אל navigator.share() כמו קודם. מכיוון שכל דבר יכול להיכשל, אני משתמש שוב בבלוק try...catch.

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

כמו קודם, אני משתמש בשיפור הדרגתי. אם גם 'share' וגם 'canShare' קיימים באובייקט navigator, רק אז אוכל להמשיך ולטעון את share.mjs דרך import() דינמי. בדפדפנים כמו Safari לנייד, שמתקיימים בהם רק אחד משני התנאים, המערכת לא טוענת את הפונקציונליות.

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

באפליקציית Fugu Greetings, אם מקישים על הלחצן שיתוף בדפדפן נתמך כמו Chrome ב-Android, נפתחת גיליון השיתוף המובנה. לדוגמה, אפשר לבחור ב-Gmail, וחלון הווידג'ט של יצירת האימייל יופיע עם התמונה המצורפת.

גיליון שיתוף ברמת מערכת ההפעלה שבו מוצגות אפליקציות שונות שאיתן אפשר לשתף את התמונה.
בחירה באפליקציה שבה רוצים לשתף את הקובץ.
הווידג&#39;ט של Gmail לכתיבה של אימייל עם התמונה המצורפת.
הקובץ מצורף לאימייל חדש בחלונית הכתיבה של Gmail.

ממשק ה-API של Contact Picker

בשלב הבא אדבר על אנשי קשר, כלומר על פנקס הכתובות או על אפליקציית ניהול אנשי הקשר של המכשיר. כשכותבים כרטיס ברכה, לא תמיד קל לכתוב נכון את השם של מישהו. לדוגמה, יש לי חבר בשם סרגיי, והוא מעדיף שהשם שלו ייכתב באותיות קיריליות. אני משתמש במקלדת QWERTZ גרמנית ואין לי מושג איך להקליד את השם שלו. זו בעיה שאפשר לפתור באמצעות Contact Picker API. מכיוון שהחבר שלי שמור באפליקציית אנשי הקשר בטלפון, באמצעות ה-API של בורר אנשי הקשר אפשר לגשת לאנשי הקשר מהאינטרנט.

קודם כול, צריך לציין את רשימת הנכסים שאליהם אני רוצה לגשת. במקרה הזה, אני רוצה רק את השמות, אבל בתרחישי שימוש אחרים יכול להיות שאתעניין במספרי טלפון, באימיילים, בסמלי דמויות או בכתובות פיזיות. בשלב הבא, מגדירים אובייקט options ומגדירים את multiple כ-true, כדי שאפשר יהיה לבחור יותר מרשומה אחת. לבסוף, אפשר לקרוא לפונקציה navigator.contacts.select(), שמחזירה את המאפיינים הרצויים לאנשי הקשר שהמשתמש בחר.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

עד עכשיו כבר הבנתם את התבנית: אנחנו טוענים את הקובץ רק כשיש תמיכה בפועל ב-API.

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

ב-Fugu Greeting, כשמקישים על הלחצן אנשי קשר ובוחרים את שני החברים הכי טובים, Сергей Михайлович Брин ו-劳伦斯·爱德华·"拉里"·佩奇, אפשר לראות שהכלי לבחירת אנשי קשר מוגבל להצגת השמות שלהם בלבד, אבל לא את כתובות האימייל שלהם או מידע אחר כמו מספרי הטלפון שלהם. לאחר מכן השמות שלהם מופיעים בכרטיס הברכה שלי.

בוחר אנשי קשר מציג את השמות של שני אנשי קשר בפנקס הכתובות.
בחירת שני שמות באמצעות בורר אנשי הקשר מתוך פנקס הכתובות.
שמות שני אנשי הקשר שנבחרו בעבר מצוירים על כרטיס האיחול.
לאחר מכן, שני השמות יופיעו על כרטיס האיחול.

ממשק API של לוח אסינכרוני

בשלב הבא נסביר איך מעתיקים ומדביקים. אחת מהפעולות האהובות עלינו כמפתחי תוכנה היא העתקה והדבקה. כמחבר של כרטיסי ברכה, לפעמים אני רוצה לעשות זאת. יכול להיות שארצה להדביק תמונה בכרטיס ברכה שאני עובד עליו, או להעתיק את כרטיס הברכה כדי שאוכל להמשיך לערוך אותו במקום אחר. Async Clipboard API תומך גם בטקסט וגם בתמונות. אראה לכם איך הוספתי תמיכה בהעתקה ובהדבקה לאפליקציית Fugu Greetings.

כדי להעתיק משהו ללוח המערכת, צריך לכתוב בו. השיטה navigator.clipboard.write() מקבלת מערך של פריטים מהלוח כפרמטר. כל פריט בלוח העריכה הוא למעשה אובייקט עם blob כערך, וסוג ה-blob כמפתח.

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

כדי להדביק, צריך לבצע לולאה על הפריטים שבלוח העריכה, שמקבלים באמצעות קריאה ל-navigator.clipboard.read(). הסיבה לכך היא שייתכן שכמה פריטים בלוח העריכה מופיעים בלוח בייצוגים שונים. לכל פריט בלוח העריכה יש שדה types שמציין את סוגי ה-MIME של המשאבים הזמינים. אני קורא ל-method‏ getType() של פריט הלוח, ומעביר את סוג ה-MIME שקיבלתי מקודם.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

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

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

איך זה עובד בפועל? יש לי תמונה פתוחה באפליקציית Preview של macOS והעתקתי אותה ללוח. כשלחצתי על הדבקה, האפליקציה Fugu Greetings ביקשה ממני לאפשר לה לראות טקסט ותמונות בלוח העריכה.

אפליקציית Fugu Greetings עם ההודעה על בקשת הרשאה ללוח העריכה.
בקשת ההרשאה ללוח.

בסיום, אחרי אישור ההרשאה, התמונה מועתקת לאפליקציה. אפשר גם להשתמש באפשרות ההפוכה. אעתיק כרטיס ברכה ללוח. כשפותחים את Preview ולוחצים על File ואז על New from Clipboard, כרטיס האיחול מועתק לתמונה חדשה ללא שם.

אפליקציית Preview ב-macOS עם תמונה ללא שם שהועתקה.
תמונה מודבקת באפליקציית Preview ב-macOS.

Badging API

ממשק API שימושי נוסף הוא Badging API. כ-PWA שניתן להתקנה, לאפליקציית Fugu Greetings יש כמובן סמל אפליקציה שהמשתמשים יכולים למקם במעמד האפליקציות או במסך הבית. דרך מהנה וקלה להדגים את ה-API היא להשתמש בו (לשימוש לרעה) ב-Fugu Greetings בתור מונה של משיכות עט. הוספתי event listener שמגדיל את מונה התנועות של העט בכל פעם שהאירוע pointerdown מתרחש, ולאחר מכן מגדיר את תג הסמל המעודכן. בכל פעם שמוחקים את הלוח, המונה מתאפס והתג מוסר.

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

התכונה הזו היא שיפור הדרגתי, ולכן לוגיק הטעינה הוא רגיל.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

בדוגמה הזו, ציירתי את המספרים מ-1 עד 7, תוך שימוש בתנועת עט אחת לכל מספר. מונה התגים שמופיע על הסמל הוא עכשיו שבע.

המספרים מאחד עד שבעה שמצוירים על כרטיס ברכה, כל אחד במשיחת עט אחת בלבד.
ציור המספרים מ-1 עד 7 באמצעות שבעה קווים בעט.
סמל תג באפליקציית Fugu Greetings שבו מוצג המספר 7.
מספר הפסקות הכתיבה בטופס של תג סמל האפליקציה.

Periodic Background Sync API

רוצים להתחיל כל יום מחדש עם משהו חדש? תכונה מגניבה באפליקציית Fugu Greetings היא שהיא יכולה להעניק לכם השראה בכל בוקר עם תמונה חדשה לרקע של כרטיס האיחול. האפליקציה משתמשת ב-Periodic Background Sync API כדי לעשות זאת.

השלב הראשון הוא register של אירוע סנכרון תקופתי ברישום של קובץ השירות (service worker). הוא מחכה לתג סנכרון שנקרא 'image-of-the-day', עם מרווח זמן מינימלי של יום אחד, כדי שהמשתמש יוכל לקבל תמונה חדשה לרקע כל 24 שעות.

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

השלב השני הוא להקשיב לאירוע periodicsync ב-service worker. אם תג האירוע הוא 'image-of-the-day', כלומר זה שרשום קודם, התמונה של היום מאוחזרת באמצעות הפונקציה getImageOfTheDay() והתוצאה מועברת לכל הלקוחות כדי שיוכלו לעדכן את המפות והמטמון שלהם.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

שוב, מדובר בשיפור הדרגתי, כך שהקוד נטען רק כשהדפדפן תומך ב-API. הכלל הזה חל גם על קוד הלקוח וגם על קוד ה-service worker. בדפדפנים שאינם תומכים, אף אחד מהם לא נטען. שימו לב שבקובץ השירות, במקום import() דינמי (שעדיין לא נתמך בהקשר של קובץ שירות), השתמשתי ב-importScripts() הקלאסי.

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

באפליקציית Fugu Greetings, לחיצה על הלחצן Wallpaper (טפט) חושפת את התמונה של כרטיס האיחול של היום, שמתעדכנת מדי יום באמצעות Periodic Background Sync API.

אפליקציית Fugu Greetings עם תמונה חדשה של כרטיס ברכה ליום.
לחיצה על הלחצן טפט מציגה את תמונת היום.

Notification Triggers API

לפעמים, גם אם יש לכם הרבה השראה, אתם צריכים דחיפה כדי לסיים כרטיס ברכה שכבר התחלתם ליצור. זוהי תכונה שמופעלת על ידי Notification Triggers API. כמשתמש/ת, אני יכול/ה להזין שעה שבה רוצים לקבל הנחיה לסיים את כרטיס הברכה. כשהזמן הזה יגיע, אקבל התראה על כך שכרגע יש כרגע כרטיס ברכה בשבילי.

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

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

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

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

כשמסמנים את התיבה תזכורת ב-Fugu Greetings, מופיעה בקשה לבחור מתי רוצים לקבל תזכורת לסיום הכרטיס.

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

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

מרכז ההתראות של macOS שמציג התראה מופעלת מ-Fugu Greetings.
ההתראה שהופעל תופיע במרכז ההתראות של macOS.

ממשק ה-API של Wake Lock

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

השלב הראשון הוא לקבל את חסימת המצב הפעיל באמצעות navigator.wakelock.request method(). מעבירים לו את המחרוזת 'screen' כדי לקבל נעילה של מצב שינה במסך. לאחר מכן מוסיפים מאזין לאירועים כדי לקבל הודעה כשנעילת ההתעוררות משוחררת. מצב כזה יכול לקרות, לדוגמה, כשהחשיפה של הכרטיסייה משתנה. אם זה יקרה, כשהכרטיסייה תהיה שוב גלויה, אוכל לקבל מחדש את נעילת ההתעוררות.

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

כן, זוהי שיפור הדרגתי, ולכן צריך לטעון אותו רק כשהדפדפן תומך ב-API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

ב-Fugu Greetings יש תיבת סימון Insomnia (נדודי שינה). אם מסמנים אותה, המסך לא יכבה.

אם התיבה של &#39;נדודי שינה&#39; מסומנת, המסך לא יכבה.
תיבת הסימון אינסומניה משאירה את האפליקציה במצב פעיל.

ה-API לזיהוי מצב חוסר פעילות

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

אחרי שמוודאים שהתקבלה הרשאת ההתראות, יוצרים מופע של הגלאי לפעילות ריקה. אני רושם מאזין לאירועים שמאזין לשינויים במצב חוסר הפעילות, כולל המשתמש והמסך. המשתמש יכול להיות פעיל או לא פעיל, והמסך יכול להיות נעול או לא נעול. אם המשתמש לא פעיל, הלוח נמחק. מגדירים לסף של גלאי חוסר הפעילות 60 שניות.

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

וכמו תמיד, אני טוען את הקוד הזה רק כשהדפדפן תומך בו.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

באפליקציה Fugu Greetings, הלוח נמחק כשתיבת הסימון זמני מסומנת והמשתמש לא פעיל במשך זמן רב מדי.

אפליקציית Fugu Greetings עם לוח נקי אחרי שהמשתמש לא היה פעיל במשך זמן רב מדי.
אם התיבה זמנית מסומנת והמשתמש לא היה פעיל במשך יותר מדי זמן, אזור העריכה נמחק.

סגירה

אוף, איזו נסיעה. כל כך הרבה ממשקי API באפליקציית דוגמה אחת בלבד. חשוב לזכור: אף פעם לא מחייבים את המשתמש בתשלום על הורדה של תכונה שהדפדפן שלו לא תומך בה. באמצעות שיפור הדרגתי, אני מוודא שרק הקוד הרלוונטי נטען. ומכיוון שבקשות הן זולות, ב-HTTP/2, הדפוס הזה אמור לפעול היטב באפליקציות רבות. עם זאת, כדאי להשתמש ב-bundler לאפליקציות גדולות מאוד.

החלונית &#39;רשת כלי הפיתוח ל-Chrome&#39;, שבה מוצגות רק בקשות לקבצים עם קוד שנתמך בדפדפן הנוכחי.
כרטיסיית Network (רשת) בכלי הפיתוח של Chrome, שבה מוצגות רק בקשות לקבצים עם קוד שהדפדפן הנוכחי תומך בו.

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

Fugu Greetings פועל ב-Android Chrome, ומוצגות בו תכונות רבות.
Fugu Greetings פועל ב-Chrome ל-Android.
Fugu Greetings פועל ב-Safari במחשב, ומוצגות בו פחות תכונות זמינות.
Fugu Greetings פועל ב-Safari במחשב.
אפליקציית Fugu Greets פועלת ב-Chrome למחשב, ומציגה תכונות זמינות רבות.
Fugu Greetings פועל ב-Chrome למחשב.

אם אתם מעוניינים באפליקציה Fugu Greetings, תוכלו למצוא אותה וללפצל אותה ב-GitHub.

המאגר של Fugu Greetings ב-GitHub.
אפליקציית Fugu Greetings ב-GitHub.

צוות Chromium עובד קשה כדי לשפר את ממשקי ה-API המתקדמים של Fugu. כשאני משתמש בשיפור הדרגתי בפיתוח האפליקציה, אני מוודא שכולם מקבלים חוויית שימוש בסיסית טובה, אבל שאנשים שמשתמשים בדפדפנים שתומכים ב-APIs נוספים של פלטפורמות אינטרנט מקבלים חוויה טובה עוד יותר. אני מחכה לראות איך תשתמשו בשיפור הדרגתי באפליקציות שלכם.

תודות

אני רוצה להודות לכריסטיאן ליבל ולHemanth HM שתרמו לברכות Fugu. המאמר הזה נבדק על ידי ג'ו מדלי וקייס בסקי. Jake Archibald עזר לי להבין את המצב של import() דינמי בהקשר של שירות עובד.