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

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

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

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

JavaScript מודרני

ואם כבר מדברים על JavaScript, התמיכה של הדפדפן בתכונות הליבה העדכניות ביותר של ES 2015 JavaScript היא נהדר. התקן החדש כולל הבטחות, מודולים, מחלקות, ליטרלים של תבניות, פונקציות חיצים, let ו-const, פרמטרים שמוגדרים כברירת מחדל, מחוללים, ההקצאה ההרסנית, מנוחה ומרווח, Map/Set, WeakMap/WeakSet ועוד רבים. כל הנכסים נתמכים.

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

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

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

אפילו התוספות החדשות ביותר של שפות ב-ES 2020, כמו Optional chain ו-nullishing, קיבלו תמיכה במהירות. בהמשך תוכלו לראות דוגמת קוד. בכל הנוגע לתכונות הליבה של 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. היא אמינה ומופעלת באופן לא מקוון, כך שגם אם אין לכם רשת, עדיין תוכלו להשתמש בה. אפשר גם להתקין במסך הבית של המכשיר, ומשתלב בצורה חלקה עם מערכת ההפעלה כאפליקציה עצמאית.

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

שיפור הדרגתי

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

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

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

כשיש תכונת import, סביר להניח שתהיה תכונת import כדי שהמשתמשים יוכלו לשמור את כרטיסי הברכה שלהם באופן מקומי. הדרך המסורתית לשמור קבצים היא ליצור קישור מקושר עם המאפיין 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 Web Inspector.
כלים למפתחים ב-Firefox שמציגים את הקבצים מהדור הקודם שנטענים.
כרטיסיית הרשת 'כלים למפתחים' ב-Firefox.

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

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

ממשק ה-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);
  }
};

ייצוא התמונה כמעט זהה, אבל הפעם אני צריך להעביר פרמטר סוג של 'save-file' ל-method chooseFileSystemEntries(). כאן מופיעה תיבת דו-שיח לשמירת קובץ. כשהקובץ פתוח, לא היה צורך בכך כי '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 עם התמונה ששונתה.
שמירת התמונה ששונתה בקובץ חדש.

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

מלבד האפשרות לשמור לתמיד, אולי אני רוצה לשתף את כרטיס הברכה שלי. מה ש-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);
  }
};

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

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

ב-Fugu Greetings, אם מקישים על הלחצן Share בדפדפן תומך כמו 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 של המשאבים הזמינים. אני קורא לשיטה 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');
}

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

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

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

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

ממשק ה-API של התגים

API שימושי נוסף הוא Badging API. כ-PWA שאפשר להתקין, ל-Fugu Greetings יש כמובן סמל של אפליקציה שהמשתמשים יכולים להציב ברשימת האפליקציות או במסך הבית. אחת הדרכים הכי טובות ומהנות להדגים את ה-API היא (ab) להשתמש בו ב-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 מצוירים על כרטיס הברכה, וכל אחד מהם במשיכת עט אחת בלבד.
שרטוט המספרים מ-1 עד 7, ב-7 משיכות עט.
סמל תג באפליקציית Fugu Greetings, שמציג את המספר 7.
מונה העט בצורת תג של סמל האפליקציה.

ממשק API לסנכרון תקופתי ברקע

רוצה להתחיל כל יום מחדש עם משהו חדש? אחת התכונות המגניבות של אפליקציית Fugu Greetings היא שבעזרתה תוכלו לעורר בכם השראה בכל בוקר עם תמונת רקע חדשה כדי לפתוח כרטיס ברכה. כדי לעשות זאת, האפליקציה משתמשת ב-Periodic Background Sync API (ממשק ה-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). בדפדפנים שאינם תומכים, אף אחד מהם לא נטען. שימו לב איך ב-Service Worker, במקום import() דינמי (שאין תמיכה בהקשר של קובץ השירות (service worker) עדיין), אני משתמש בגרסה הקלאסית 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, בלחיצה על הלחצן טפט אפשר לראות את תמונת כרטיס הברכה של היום שמתעדכנת כל יום באמצעות Periodic Background Sync API.

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

ממשק 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 שמברכת את המשתמש עם הנחיה לשאול את המשתמש מתי הוא רוצה לקבל תזכורת לסיים את כרטיס הברכה.
תזמון התראה מקומית לקבלת תזכורת לגבי סיום כרטיס ברכה.

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

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

ממשק API של Wake Lock

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

השלב הראשון הוא להשיג נעילת מצב שינה באמצעות 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; מסומנת, המסך לא נכנס למצב שינה.
תיבת הסימון Insomnia מאפשרת לאפליקציה להיכנס למצב שינה.

ממשק API לזיהוי לא פעיל

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

אחרי שמוודאים שההרשאה לשליחת התראות ניתנה, יוצרים את מזהה חוסר הפעילות. אני רושם event listener שמאזין לשינויים ללא פעילות, כולל המשתמש ומצב המסך. המשתמש יכול להיות פעיל או לא פעיל, ואפשר לבטל את נעילת המסך או לנעול אותו. אם המשתמש לא פעיל, ההדפסה על קנבס תימחק. הערך המינימלי של לגלאי חוסר הפעילות הוא 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 לאפליקציות גדולות מאוד.

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

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

ברכות Fugu שפועלות ב-Android Chrome עם הרבה תכונות זמינות
Fugu Greetings פועלת ב-Android Chrome.
ברכות Fugu שפועלות ב-Safari במחשב עם פחות פיצ&#39;רים זמינים.
פתיחים ל-Fugu פועלים ב-Safari במחשב.
ברכות Fugu שפועלות ב-Chrome במחשב עם הרבה תכונות זמינות.
הודעות פתיחה ל-Fugu פועלות ב-Chrome במחשב.

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

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

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

אישורים

אני רוצה להודות לכריסטיאן ליבל ולHemanth HM, ששניהם תרמו לפתיחות של Fugu. המאמר הזה נכתב על ידי Joe Medley ו-Kayce Basques. ג'ייק ארצ'יבלד עזר לי לגלות את המצב עם import() הדינמי בהקשר של קובץ שירות (service worker).