針對新式瀏覽器建構應用程式,並像 2003 年一樣逐步強化
回歸 2003 年 3 月,Nick Finck 和 Steve Champeon 為網頁設計世界揭曉 也就是 漸進式強化、 著重於先載入核心網頁內容的網頁設計策略 然後循序漸進地將更加細緻 技術上嚴密的展示與功能層層疊加在內容上方 在 2003 年,漸進式增強是當時關於使用現代的科技 CSS 功能、不突兀的 JavaScript,甚至是可擴充的向量圖形。 2020 年及未來,持續提升的要素就是使用 新型瀏覽器功能。
新版 JavaScript
說到 JavaScript,最新核心 ES 2015 JavaScript 的瀏覽器在支援範圍內
功能很好
新標準包含承諾、模組、類別、範本常值、箭頭函式、let
和 const
,
預設參數、產生器、解構的指派作業、放置及傳播、Map
/Set
、
WeakMap
/WeakSet
,以及其他多個項目。
全面支援。
非同步函式、ES 2017 功能和我最喜歡的一項功能
是否能夠用於
多數主要瀏覽器。
async
和 await
關鍵字可實現以承諾為基準的非同步行為
採用更簡潔的樣式編寫,不必明確設定承諾鏈。
甚至近在 2020 年新增的 ES 課題,例如: 選用鏈結 空值凝結 很快就能與他們聯絡請參考下方的程式碼範例。 JavaScript 的核心功能不那麼環保 今天。
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah',
},
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
範例應用程式:Fugu Greetings
本文中我使用 PWA 的簡易 PWA Fugu 問候語 (GitHub)。 這款應用程式的名稱是 Project Fugu 🐡? 的結論,致力於為網路提供一切 Android/iOS/電腦應用程式的強大功能 如要進一步瞭解專案,請前往 到達網頁:
Fugu Greetings 是一款繪圖應用程式,可讓你建立虛擬問候卡,以及 與親朋好友分享具體化 PWA 的核心概念。是 應用程式,而且完全離線啟用,因此 如有網路,您仍然可以使用此外也選擇可安裝 裝置主畫面,而且能與作業系統完美整合 做為獨立應用程式
漸進增強
先告一段落,接著就來談談漸進式增強功能。 MDN Web Docs 詞彙表定義 概念如下:
漸進式強化屬於設計理念 提供給更多使用者的重要內容和功能 僅為最新世代的使用者提供最佳體驗 能夠執行所有必要程式碼的瀏覽器
特徵偵測 通常用於判斷瀏覽器是否能處理更新型的功能 而 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();
});
};
如果有「匯入」功能,表示該功能應該具有「匯出」功能
讓使用者可以將問候卡儲存在本機。
建立檔案的傳統方式是建立錨點連結
使用 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 的網路分頁。
不過,在 Chrome 上,只有支援這個 API 的瀏覽器會載入新的指令碼。
而這要做到這點
動態 import()
,支援所有新式瀏覽器
支援團隊。
就像我之前說的,現在草地很綠色。
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 使用漸進式增強功能。 我可以照常開啟檔案 匯入的檔案會直接顯示在畫布上。 進行編輯,最後將編輯過的對話方塊儲存為 [儲存] 對話方塊 我可以在這裡選擇檔案的名稱和儲存位置 現在檔案已準備好進行永久保存。
網路分享與網路分享目標 API
除了儲存實習性以外,我也想分享自己的問候卡。 這就是 Web Share API 的功能 Web Share Target API 允許我協助。 行動裝置及其他電腦作業系統內建分享功能 機制 舉例來說,以下是從 我的網誌。 只要按一下「分享文章」按鈕,即可和好友分享文章連結,例如: 例如透過 macOS Messages 應用程式
用來執行這類程式碼的程式碼非常簡單。我撥打 navigator.share()
,
在物件中傳遞選用的 title
、text
和 url
。
但如果想附加圖片,該怎麼辦?Web Share API 第 1 級尚未支援這項功能。
好消息是,網路共用等級 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 Greeting 卡應用程式。
首先,我需要準備 data
物件,其中包含含有一個 blob 的 files
陣列,然後
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);
}
};
和先前一樣,我使用漸進式增強功能,
如果 navigator
物件中同時存在 'share'
和 'canShare'
,那麼我只會
透過動態 import()
載入 share.mjs
。
在行動版 Safari 等只符合兩項條件的瀏覽器上,我不會載入
相關性
const loadShare = () => {
if ('share' in navigator && 'canShare' in navigator) {
import('./share.mjs');
}
};
在 Fugu Greetings 中,如果我在 Android 版 Chrome 等支援瀏覽器中輕觸「分享」按鈕, 系統隨即會開啟內建分享工作表。 比如,我可以選擇 Gmail 這樣電子郵件編輯器小工具就會彈出 已附加圖片。
聯絡人挑選器 API
接著來談談聯絡人,也就是裝置的通訊錄 或聯絡人管理員應用程式 撰寫賀卡時,要正確寫出這件事並不容易 某人的姓名 舉例來說,我有一個朋友阿吉,他偏好用西里爾字母拼讀他的名字。我 使用德文的 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 中,當我輕觸「Contacts」按鈕,並選兩個最佳好友時, 「WAер目光ей Broadcastий Бовий Брин」和國際信 可以看到 聯絡人選擇器只能顯示他們的姓名 ,但不是對方的電子郵件地址或其他資訊,例如對方的電話號碼。 隨後他們的名字繪製在我的問候卡上。
非同步剪貼簿 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);
}
};
如要貼上內容,我需要透過呼叫
navigator.clipboard.read()
。
這是因為剪貼簿中可能有多個剪貼簿項目
不同的表示法
每個剪貼簿項目都有一個 types
欄位,用於說明可用的 MIME 類型
再複習一下,機構節點
是所有 Google Cloud Platform 資源的根節點
我呼叫剪貼簿項目的 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 應用程式接著詢問我 是否允許應用程式查看剪貼簿中的文字和圖片。
最後,接受權限要求後,圖片就會貼到應用程式中。 相反地,另一回合也能運作 讓我將問候卡複製到剪貼簿。 接著開啟「預覽」並依序點選「檔案」和「從剪貼簿新增」, 即可將問候卡貼到新的未命名圖片中。
徽章 API
另一個實用的 API 是 Badging API。
課程的 Fugu Greetings 做為可安裝的 PWA 都有
放置在應用程式座架或主畫面中
在 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');
}
在這個範例中,我用筆觸動作繪製一到七的數字 。 圖示上的徽章計數器現在是 7 個。
定期背景同步處理 API
想要以嶄新的方式展開每一天嗎? Fugu Greetings 應用程式有個很棒的功能,可以讓你每天早上都能獲得啟發 開啟您的問候卡。 應用程式使用 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);
}
};
第二個步驟是在 Service 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 中 (而非動態 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 中,按下「Wallpaper」按鈕可顯示當日的問候卡圖片 並透過 Periodic Background Sync API 每天更新。
通知觸發條件 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 中觸發時 內容就像任何其他通知一樣,但就像我先前撰寫的一樣 也不需要網路連線
Wake Lock API
我也想要加入 Wake Lock API。 有時你只是想在螢幕中長時間拍攝,直到尋找靈感為止 親親。 最糟的情況就是螢幕關閉了。 Wake Lock API 可以避免這個情況發生。
第一步是使用 navigator.wakelock.request method()
取得 Wake Lock。
我傳送字串 'screen'
,以取得螢幕 Wake Lock。
然後新增事件監聽器,以便在 Wake Lock 釋放時收到通知。
舉例來說,如果分頁瀏覽權限有所變更,就可能發生這種情況。
如果發生這種情況,我可以在分頁再次顯示時重新取得 Wake Lock。
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」核取方塊會在勾選後 螢幕喚醒。
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」核取方塊,畫布就會清除 ,且使用者閒置時間過長。
Closing
呼,你好!單一範例應用程式中就含有許多 API。 提醒您,我絕對不會讓使用者支付下載費用 以便瞭解瀏覽器不支援的功能 透過漸進式增強功能,我可確保系統只會載入相關的程式碼。 而且由於 HTTP/2 的要求很便宜,因此,此模式應適用於許多 這個原則也希望確保攸關安全的 應用項目都必須以有效方式加以監控和測試 但您可能需要考慮採用適用於大型應用程式的套裝組合方案。
由於並非所有平台都支援所有功能,應用程式在不同瀏覽器中的外觀可能略有不同。 但核心功能一直以來都在 — 根據特定瀏覽器的功能逐步強化。 請注意,即使在同一瀏覽器和瀏覽器中,這些功能也可能有所變更 取決於應用程式是以安裝版應用程式還是瀏覽器分頁執行。
如果你對 Fugu Greetings 應用程式感興趣, ,並在 GitHub 上找出並分支。
Chromium 團隊正在努力讓先進的 Fugu API 更臻完美。 藉由在開發應用程式的過程中,我們以循序漸進的方式加以改進 我會確保大家都獲得良好可靠的基準體驗 但使用者如果使用支援更多 Web Platform API 的瀏覽器,就能獲得更好的使用體驗。 期待看到各位在應用程式中運用漸進式強化功能締造佳績。
特別銘謝
在此感謝 Christian Liebel 的支持,
Hemanth HM 也對 Fugu Greeting 大有貢獻。
本文經過 Joe Medley 和
Kayce Basques。
Jake Archibald 幫我搞定這個情況
Service Worker 環境中有動態 import()
的情況。