逐步強化漸進式網頁應用程式

為新式瀏覽器建構應用程式,並逐步改善,就像 2003 年一樣

2003 年 3 月,Nick FinckSteve Champeon 提出漸進式增強概念,震驚了整個網頁設計界。這項網頁設計策略強調先載入核心網頁內容,然後再逐步在內容上方加入更精細且技術嚴謹的呈現層和功能。在 2003 年,漸進式改善是指使用當時的現代 CSS 功能、不顯眼的 JavaScript,甚至只是可調整向量圖形。2020 年及以後的漸進式改善功能,主要是使用新式瀏覽器功能

透過漸進式改善,打造未來的無障礙網頁設計。Finck 和 Champeon 原始簡報的標題投影片。
投影片:透過漸進增強,打造無障礙的網頁設計。 (來源)

新潮 JavaScript

談到 JavaScript,瀏覽器支援最新的 ES 2015 JavaScript 核心功能的情況非常理想。新標準包含承諾、模組、類別、範本文字常值、箭頭函式、letconst、預設參數、產生器、結構重組指派、rest 和 spread、Map/SetWeakMap/WeakSet 等等。所有語言都支援

CanIUse 支援表格:ES6 功能支援表格,顯示所有主要瀏覽器的支援情形。
ECMAScript 2015 (ES6) 瀏覽器支援表格。(來源)

非同步函式是 ES 2017 的功能,也是我個人最喜歡的功能之一,可在所有主要瀏覽器中使用asyncawait 關鍵字可讓您以更簡潔的風格編寫非同步的承諾行為,避免明確設定承諾鏈結。

CanIUse 支援表格:顯示所有主要瀏覽器支援的非同步函式。
非同步函式瀏覽器支援表格。(來源)

甚至連最近新增的 ES 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 Greetings

在本文中,我會使用名為 Fugu Greetings 的簡易 PWA (GitHub)。這個應用程式的名稱是向 Project Fugu 致敬 🐡?,Project Fugu 是為了讓網頁擁有 Android/iOS/電腦應用程式的所有功能。如要進一步瞭解這項專案,請前往到達網頁

Fugu Greetings 是一款繪圖應用程式,可讓您製作虛擬賀卡,並傳送給親朋好友。這項範例展現了 PWA 的核心概念。這項服務可靠且完全支援離線使用,因此即使沒有網路,您還是可以使用這項服務。這個應用程式也可以安裝到裝置的主畫面,並以獨立應用程式的形式與作業系統完美整合。

Fugu Greetings PWA,圖片與 PWA 社群標誌相似。
Fugu Greetings 範例應用程式。

漸進增強

說明完這點,我們就來談談漸進式強化。MDN 網路文件字典定義這個概念如下:

漸進式增強是一種設計理念,可為盡可能多的使用者提供基本內容和功能,同時只為可執行所有必要程式碼的最新瀏覽器使用者提供最佳體驗。

功能偵測通常用於判斷瀏覽器是否可處理較新式功能,而polyfill通常用於透過 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 屬性建立錨點連結,並將 blob 網址設為 href。您也可以透過程式輔助方式「點選」該按鈕,觸發下載作業。此外,為了避免記憶體流失,請務必記得撤銷 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 開發人員工具網路分頁。

不過,在支援 API 的 Chrome 瀏覽器上,系統只會載入新的指令碼。這項功能之所以能順利實現,要歸功於所有新式瀏覽器都支援動態 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 應用程式。
將修改過的圖片儲存到新檔案。

Web Share 和 Web Share Target API

除了永久儲存之外,我可能還想分享賀卡。 這正是 Web Share APIWeb Share Target API 可讓我執行的操作。行動版和最近的電腦作業系統已獲得內建的分享機制。舉例來說,以下是 macOS 電腦版 Safari 的分享頁面,是由我的網誌上的文章觸發。點選「分享文章」按鈕後,你可以透過 macOS 訊息應用程式等方式,與朋友分享文章連結。

在 macOS 上,透過文章的「分享」按鈕觸發的 Safari 電腦版分享頁面
macOS 電腦版 Safari 上的 Web Share API。

實現這項功能的程式碼相當簡單。我呼叫 navigator.share(),並在物件中傳遞選用的 titletexturl。但如果我想附加圖片,該怎麼做?Web Share API 的第 1 級目前不支援這項功能。好消息是,Web Share Level 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 物件,其中包含由一個 Blob 組成的 files 陣列,然後再準備 titletext。接下來,我會使用新的 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 物件中,我才會繼續透過動態 import() 載入 share.mjs。在行動版 Safari 等只符合上述兩個條件之一的瀏覽器上,我不會載入這項功能。

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

在 Fugu Greetings 中,如果我在 Android 上支援的瀏覽器 (例如 Chrome) 中輕觸「Share」按鈕,內建的分享工作表就會開啟。舉例來說,我可以選擇 Gmail,電子郵件撰寫工具小工具就會彈出,並附上圖片。

作業系統層級的 Sharesheet,顯示可用來分享圖片的各種應用程式。
選擇要共用檔案的應用程式。
附加圖片的 Gmail 電子郵件撰寫小工具。
檔案會附加至 Gmail 編輯器中的新電子郵件。

Contact Picker API

接下來,我想談談聯絡人,也就是裝置的電話簿或聯絡人管理應用程式。在寫賀卡時,不一定能正確寫出對方的姓名。舉例來說,我有一位朋友 Sergey,他偏好使用西里爾字母拼寫自己的名字。我使用德文 QWERTZ 鍵盤,不知道該如何輸入名稱。這正是 Contact Picker API 可以解決的問題。由於我將朋友儲存在手機的聯絡人應用程式中,因此我可以透過「Contacts Picker」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 中,當我輕觸「聯絡人」按鈕並選取兩位好友 Сергей Михайлович Брин劳伦斯·爱德华·"拉里"·佩奇 時,您可以看到聯絡人挑選器只會顯示他們的姓名,而不會顯示電子郵件地址或電話號碼等其他資訊。然後將他們的名字繪製在賀卡上。

聯絡人挑選器顯示通訊錄中的兩個聯絡人名稱。
使用通訊錄中的聯絡人挑選工具選取兩個名稱。
在賀卡上繪製的兩位先前選取聯絡人的姓名。
接著,這兩個名字就會繪製到賀卡上。

非同步 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 類型。我呼叫剪貼簿項目的 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 應用程式顯示剪貼簿權限提示。
剪貼簿權限提示。

最後,在接受權限後,圖片就會貼到應用程式中。反之亦然。我要將賀卡複製到剪貼簿。接著,我開啟「預覽」並依序點選「檔案」和「從剪貼簿新增」,賀卡就會貼到新未命名的圖片中。

macOS 預覽應用程式,其中含有未命名且剛貼上的圖片。
貼到 macOS 預覽應用程式中的圖片。

Badging API

另一個實用的 API 是 Badging API。作為可安裝的 PWA,Fugu Greetings 當然有應用程式圖示,可供使用者放置在應用程式匣或主畫面上。您可以透過有趣又簡單的方式,在 Fugu Greetings 中使用 API 做為筆劃計數器。我已新增事件監聽器,只要 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 的數字。圖示上的徽章計數器現在顯示為 7。

在賀卡上畫上從一到七的數字,每個數字都只用一筆劃畫出。
使用七個筆劃繪製 1 到 7 的數字。
Fugu Greetings 應用程式上的徽章圖示,顯示數字 7。
以應用程式圖示徽章形式顯示的筆劃計數器。

Periodic Background Sync API

想每天都能有新鮮感嗎? Fugu Greetings 應用程式提供的實用功能,是每天早上都能為您帶來靈感,提供新的背景圖片,讓您開始製作賀卡。應用程式會使用 Periodic Background Sync API 達成這項目標。

第一步是在服務工作者註冊程序中register定期同步處理事件。它會監聽名為 '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);
  }
};

第二步是監聽服務 worker 中的 periodicsync 事件。如果事件代碼為 '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 中使用的是傳統的 importScripts(),而非動態 import() (Service Worker 目前不支援動態 import())。

// 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 中勾選「Reminder」核取方塊時,系統會顯示提示,詢問我要何時收到提醒,以便完成賀卡。

Fugu Greetings 應用程式,其中提示會詢問使用者何時要收到提醒,以便完成賀卡。
安排提醒您完成賀卡的本地通知。

當排定的通知在 Fugu Greetings 中觸發時,系統會顯示通知,就像其他通知一樣。但如同我先前所述,這項功能不需要網路連線。

macOS 通知中心顯示 Fugu Greetings 觸發的通知。
觸發的通知會顯示在 macOS 通知中心。

Wake Lock API

我也想加入 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」核取方塊,勾選後螢幕就會保持喚醒狀態。

如果勾選「insomnia」核取方塊,螢幕就會保持喚醒狀態。
「Insomnia」核取方塊可讓應用程式保持運作。

Idle Detection 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 應用程式中,如果勾選「Ephemeral」核取方塊,且使用者閒置太久,畫布就會清除。

使用者閒置過久後,Fugu Greetings 應用程式會清除畫布。
勾選「暫時」核取方塊,且使用者閒置太久,系統就會清除畫布。

Closing

呼,真是場精彩的比賽!一個範例應用程式就包含這麼多 API。請注意,我絕不會讓使用者為瀏覽器不支援的功能支付下載費用。透過漸進增強,我可以確保只載入相關程式碼。由於 HTTP/2 的請求成本低廉,因此這類模式應可適用於許多應用程式,但您可能會考慮為非常大型的應用程式使用 bundler。

Chrome 開發人員工具「網路」面板只會顯示目前瀏覽器支援的程式碼檔案要求。
Chrome 開發人員工具「Network」分頁只會顯示目前瀏覽器支援的程式碼檔案要求。

由於並非所有平台都支援所有功能,因此應用程式在各瀏覽器上的外觀可能略有不同,但核心功能始終存在,並會根據特定瀏覽器的功能逐步強化。請注意,即使在同一個瀏覽器中,這些功能也可能會因應用程式是以已安裝的應用程式或在瀏覽器分頁中執行而有所不同。

在 Android Chrome 上執行的 Fugu Greetings,顯示許多可用功能。
在 Android 版 Chrome 上執行的 Fugu Greetings
在電腦版 Safari 上執行的 Fugu Greetings,顯示較少可用功能。
Fugu Greetings 在電腦版 Safari 上執行。
在 Chrome 電腦版上執行的 Fugu Greetings,顯示許多可用功能。
Fugu Greetings 在電腦版 Chrome 上執行。

如果您對 Fugu Greetings 應用程式有興趣,請前往 GitHub 分支

GitHub 上的 Fugu Greetings 存放區。
GitHub 上的 Fugu Greetings 應用程式。

Chromium 團隊正努力改善進階 Fugu API 的效能。在應用程式開發過程中,我會套用漸進式強化功能,確保所有使用者都能享有良好且穩固的基本體驗,而使用支援更多網頁平台 API 的瀏覽器的使用者,則能享有更優質的體驗。期待看到您在應用程式中使用漸進式增強功能的成果。

特別銘謝

我要感謝 Christian LiebelHemanth HM,他們為 Fugu Greetings 做出了貢獻。本文由 Joe MedleyKayce Basques 審查。Jake Archibald 協助我找出服務工作者情境中動態 import() 的問題。