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

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

במרץ 2003, ניק פינק וסטיב צ'מפיון הדהימו את עולם עיצוב האינטרנט עם הרעיון של שיפור הדרגתי – אסטרטגיה לעיצוב אתרים שמתמקדת תחילה בטעינה של תוכן הליבה של דף האינטרנט, ולאחר מכן מוסיפים בהדרגה שכבות עשירות יותר של הצגה ותכונות, עם דגש על איכות טכנית גבוהה. בשנת 2003, שיפור הדרגתי התבסס על שימוש בתכונות מודרניות של CSS, ב-JavaScript לא פולשני ואפילו רק ב-SVG. שיפור הדרגתי בשנת 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);
 
}
};

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

ממשקי ה-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.

Contact Picker API

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

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

Asynchronous Clipboard 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 בתור מונה של משיכות עט. הוספתי מאזין לאירועים שמגדיל את המונה של משיכות העיפרון בכל פעם שמתרחש האירוע 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');
}

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

Wake Lock API

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

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

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

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

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

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

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

תודות

תודה ל-Christian Liebel ול-Hemanth HM על התרומה שלהם ל-Fugu Greetings. המאמר הזה נבדק על ידי Joe Medley ו-Kayce Basques. Jake Archibald עזר לי להבין את המצב של import() דינמי בהקשר של שירות עובד.