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

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

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

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

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

שיפור הדרגתי

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

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

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

[…]

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

תורמים ל-MDN

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

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

עם זאת, ב-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);
  }
};

ייצוא תמונה הוא כמעט זהה, אבל הפעם עליי להעביר פרמטר מסוג '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);
  }
};

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

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

ממשקי ה-API לטירגוט של Web Share ו-Web Share

חוץ מאחסון למקרה לתמיד, אולי ארצה לשתף את כרטיס הברכה שלי. זה משהו ש-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, אם אני מקיש על הלחצן 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, כשאני מקישה על הלחצן אנשי קשר ובוחרת את שני החברים הכי טובים שלי, 中ергей Миайлович צירוףрин ו-劳䍦斯·爱德华·"拉里"··佩奇, אפשר לראות איך בוחר אנשי הקשר מוגבל להציג רק את השמות שלהם, אבל לא את כתובות האימייל שלהם או מידע אחר כמו מספרי הטלפון שלהם. לאחר מכן השמות שלהם מופיעים בכרטיס הברכה שלי.

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

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

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

כדי להעתיק משהו אל הלוח של המערכת, עליי לכתוב בו. השיטה 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);
  }
};

כדי להדביק, עליי להעביר בלולאה (loop) את הפריטים שבלוח העריכה שהשגתי 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 Greets שואלת אותי האם אני רוצה לאפשר לאפליקציה לראות טקסט ותמונות בלוח.

אפליקציית Fugu Greets שמציגה את בקשת ההרשאה ללוח.
בקשה למתן הרשאה ללוח העריכה.

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

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

Badging API

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

ה-API התקופתי לסנכרון ברקע

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

השלב הראשון הוא לרשום אירוע סנכרון תקופתי ברישום של קובץ השירות (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 Greets, לחיצה על הלחצן טפט חושפת את תמונת כרטיס הברכה היומית. שמתעדכן מדי יום באמצעות Periodic Background Sync API.

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

API של התראות טריגרים

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

אפליקציית Fugu Greets (הודעת ברכות) עם בקשה למשתמש לדעת מתי הוא רוצה לקבל תזכורת לסיים את כרטיס הברכה.
תזמון התראה מקומית לקבלת תזכורת על סיום כרטיס ברכה.

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

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

סגירה

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

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

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

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

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

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

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

אישורים

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