针对现代浏览器进行构建,并像 2003 年一样逐步增强
2003 年 3 月,Nick Finck 和 Steve Champeon 震惊了网络设计界 其概念 渐进式增强、 一种网络设计策略,强调首先加载核心网页内容; 然后逐渐将更多细节 以及内容之上的严格技术层面的呈现方式和功能。 而 2003 年,渐进式增强是指在当时使用现代技术 CSS 功能、非侵扰性 JavaScript,甚至只有可缩放矢量图形。 2020 年及以后的逐步增强就是使用 新型浏览器功能。
<ph type="x-smartling-placeholder">现代 JavaScript
在 JavaScript 方面,最新核心 ES 2015 JavaScript 的浏览器支持情况
非常棒
新标准包括 promise、模块、类、模板字面量、箭头函数、let
和 const
。
默认参数、生成器、解构赋值、静态和展开、Map
/Set
、
WeakMap
/WeakSet
,等等。
全部都受支持。
异步函数是我个人最喜欢的一项 ES 2017 功能
可用于
。
async
和 await
关键字支持基于 promise 的异步行为
以更简洁的样式编写,无需明确配置 promise 链。
甚至还有 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,称为 Fugu 问候语 (GitHub)。 这款应用的名称是 Project Fugu 的秘诀 🐡?,这项计划旨在为网络提供一切 Android/iOS/桌面应用的强大功能 您可在 着陆页。
Fugu Greetings 是一款绘图应用,可让你创建虚拟贺卡 你所爱的人它体现了 PWA 的核心概念。时间是 可靠且完全可离线使用,即使您没有 但仍然可以使用它它还可安装 添加到设备的主屏幕上,并与操作系统无缝集成 作为独立应用程序使用。
<ph type="x-smartling-placeholder">采用渐进增强的方式
介绍完毕之后,接下来我们要介绍一下渐进式增强。 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 的网络标签页。
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">但是,在支持该 API 的浏览器 Chrome 上,系统只会加载新脚本。
这一切都要归功于
动态 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 使用渐进式增强, 我可以像以前一样打开文件。 导入的文件会直接绘制到画布上。 我可以进行修改,最后通过真实的“保存”对话框保存修改 我可以选择文件的名称和存储位置 现在,该文件可以永久保存。
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">Web Share 和 Web Share Target API
除了永久存储之外,我可能还想分享我的贺卡。 这是由 Web Share API 和 Web Share Target API可帮助我完成这项操作 移动设备以及最近的桌面操作系统都实现了内置的共享功能 机制。 例如,下面是 macOS 上桌面 Safari 的分享表单,是从 我的博客。 点击分享文章按钮后,您可以将指向该文章的链接分享给朋友, 例如,通过 macOS“信息”应用进行操作。
<ph type="x-smartling-placeholder">实现这一目标的代码非常简单。我致电 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 卡片应用实现此目的。
首先,我需要准备一个包含 files
数组(包含一个 blob 的)的 data
对象,然后
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 可以解决的问题。 因为我的朋友存储在我手机的“通讯录”应用中 联系人选择器 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(联系人)按钮并选择我的两个最好的 pal 后, 从下拉菜单中选择 Öрери\йайлови产品和 трин 和劳伦斯·爱德华·“拉里”·佩奇, 您可以看到 联系人选择器仅显示他们的姓名 而非其电子邮件地址或其他信息(如电话号码)。 然后,他们的名字会绘制在我的贺卡上。
异步剪贴板 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
字段,用于指明可用
资源。
我调用剪贴板项的 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 应用便会询问我 是否允许该应用查看剪贴板中的文字和图片。
<ph type="x-smartling-placeholder">最后,在接受权限后,图片将粘贴到该应用中。 反之亦然。 让我将贺卡复制到剪贴板。 然后,打开“预览”,依次点击文件和从剪贴板新建, 贺卡会粘贴到新的未命名图片中。
<ph type="x-smartling-placeholder">Badging API
另一个实用的 API 是 Badging API。
作为一款可安装的 PWA,Fugu Greetings 当然有应用图标
用户可以放置在快捷应用栏或主屏幕上
在 Fugu Greetings 中(滥用)使用此 API 是展示该 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');
}
在此示例中,我使用一根笔触画了一到七之间的数字 。 图标上的徽章计数器现在为七。
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">Periodic Background Sync 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 代码都是如此。
在不支持的浏览器上,系统不会加载它们。
请注意在 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 每天更新。
<ph type="x-smartling-placeholder">通知触发器 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 复选框时,系统会显示一条提示 当我想收到完成贺卡的提醒时。
<ph type="x-smartling-placeholder">在 Fugu Greetings 中触发定时通知时, 显示方式与其他任何通知都一样 它不需要网络连接。
<ph type="x-smartling-placeholder">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 复选框,选中该复选框后,它将保持 屏幕唤醒。
<ph type="x-smartling-placeholder">空闲检测 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 复选框处于 且用户闲置时间过长。
<ph type="x-smartling-placeholder">Closing
哎呀,这真是一次出行。一个示例应用中包含这么多 API。 请注意,我从不让用户支付下载费用 。 通过使用渐进式增强,我可以确保只加载相关代码。 由于使用 HTTP/2 时,请求的成本较低,这种模式应该适用于很多 应用 不过,对于超大应用,您可以考虑使用捆绑器。
<ph type="x-smartling-placeholder">应用在不同浏览器上的外观可能略有不同,因为并非所有平台都支持所有功能, 但核心功能始终存在,根据特定浏览器的功能逐渐增强。 请注意,即使在一个同一个浏览器中,这些功能也可能会发生变化, 具体取决于应用是作为已安装的应用运行,还是在浏览器标签页中运行。
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">如果您对 Fugu Greetings 应用感兴趣, 您可以在 GitHub 上找到它并复刻它。
<ph type="x-smartling-placeholder">Chromium 团队正努力让高级 Fugu API 使草地更加环保。 通过在应用开发过程中采用渐进式增强, 我希望每个人都能获得良好的基础体验, 不过,用户使用的浏览器可支持更多网络平台 API,从而获得更好的体验。 我期待看到您通过渐进式增强在应用中的效果。
致谢
感谢 Christian Liebel,
Hemanth HM 都是为 Fugu Greetings 贡献了力量。
本文由 Joe Medley 和
Kayce Basques。
Jake Archibald 帮我找出了情况
在 Service Worker 上下文中使用动态 import()
。