逐步增强您的渐进式 Web 应用

像 2003 年那样为现代浏览器构建并逐步增强

发布日期:2020 年 6 月 29 日

早在 2003 年 3 月,Nick FinckSteve Champeon 就提出了渐进增强这一概念,令网页设计界为之震惊。这是一种网页设计策略,强调先加载核心网页内容,然后在内容之上逐步添加更精细、技术上更严谨的展示层和功能层。 虽然在 2003 年,渐进增强是指使用当时的新式 CSS 功能、不显眼的 JavaScript,甚至只是可缩放矢量图形。2020 年及以后的渐进增强功能旨在利用现代浏览器功能

通过渐进增强实现面向未来的包容性网页设计。 Finck 和 Champeon 的原始演示文稿中的标题幻灯片。

现代 JavaScript

说到 JavaScript,浏览器对最新的核心 ES 2015 JavaScript 功能的支持情况非常理想。新标准包括 Promise、模块、类、模板字面量、箭头函数、letconst、默认参数、生成器、解构赋值、rest 和 spread、Map/SetWeakMap/WeakSet 等等。所有设备均受支持

CanIUse 支持表,其中显示了 ES6 功能在所有主要浏览器中的支持情况。
ECMAScript 2015 (ES6) 浏览器支持情况表。(来源

异步函数是 ES 2017 的一项功能,也是我个人最喜欢的功能之一,可用于所有主流浏览器。借助 asyncawait 关键字,您可以采用更简洁的风格编写基于 Promise 的异步行为,而无需明确配置 Promise 链。

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 (GitHub) 的 PWA。此应用的名称是对 Project Fugu 🐡 的致敬,该项目旨在赋予 Web Android、iOS 和桌面应用的所有功能。 如需详细了解此项目,请访问其着陆页

Fugu Greetings 是一款绘画应用,可让您制作虚拟贺卡,并将其发送给亲朋好友。它体现了 PWA 的核心概念。它可靠且完全支持离线使用,因此即使没有网络,您也可以使用它。它还可以安装到设备的主屏幕,并作为独立应用与操作系统无缝集成。

Fugu Greetings PWA,其中包含一个类似于 PWA 社区徽标的绘画。
Fugu Greetings 示例应用。

采用渐进增强的方式

解决了上述问题后,接下来我们来谈谈渐进增强。 MDN Web 文档词汇表对该概念的定义如下:

渐进增强是一种设计理念,旨在为尽可能多的用户提供基本的重要内容和功能,同时仅为能够运行所有必需代码的最新浏览器用户提供尽可能出色的体验。

功能检测通常用于确定浏览器是否可以处理更现代的功能,而 polyfill 通常用于通过 JavaScript 添加缺失的功能。

[…]

渐进增强是一种实用技术,可让 Web 开发者专注于开发尽可能出色的网站,同时确保这些网站能够在多个未知用户代理上正常运行。 优雅降级与之相关,但并非同一概念,通常被视为与渐进增强相反。 实际上,这两种方法都是有效的,并且通常可以相互补充。

MDN 贡献者

从头开始制作每张贺卡可能非常麻烦。 那么,为什么不提供一项功能,让用户可以导入图片,然后从那里开始呢? 如果采用传统方法,您需要使用 <input type=file> 元素来实现此目的。 首先,您需要创建元素,将其 type 设置为 'file',并向 accept 属性添加 MIME 类型,然后以编程方式“点击”该元素并监听更改。选择图片后,系统会直接将其导入到画布上。

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'),
    ]);
  }
};

但在深入探讨文件系统访问 API 的细节之前,我先快速重点介绍一下此处的渐进增强模式。在不支持 File System Access API 的浏览器上,我加载旧版脚本。

Safari Web Inspector 显示正在加载的旧版文件。
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

attempt-right

除了永久保存之外,也许我还想分享我的贺卡。 Web Share APIWeb Share Target API 让我能够做到这一点。移动操作系统(以及最近的桌面操作系统)已经获得了内置的分享机制。

例如,当用户点击我的博客上的分享文章时,系统会触发 macOS 上的桌面版 Safari 分享表单。您可以使用 macOS“信息”应用与好友分享文章链接。

为此,我调用了 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 Greeting Card 应用实现此功能。 首先,我需要准备一个 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);
  }
};

与之前一样,我使用渐进式增强。 如果 navigator 对象上同时存在 'share''canShare',我才会继续前进,并使用动态 import() 加载 share.mjs。 对于仅满足这两个条件之一的浏览器(例如移动版 Safari),我不会加载相应功能。

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

在 Fugu Greetings 中,如果我在支持的浏览器(例如 Android 上的 Chrome)中点按分享按钮,系统会打开内置的分享表单。 例如,我可以选择 Gmail,然后电子邮件撰写器 widget 会弹出,其中附有相应图片。

操作系统级分享表单,显示可将图片分享到的各种应用。
选择要将文件分享到的应用。
Gmail 的电子邮件撰写微件,其中附有图片。
该文件会附加到 Gmail 的撰写器中的新电子邮件。

Contact Picker API

接下来,我想谈谈联系人,也就是设备的通讯簿或联系人管理应用。写贺卡时,可能并不总是能正确写出对方的名字。例如,我的朋友 Sergey 喜欢用西里尔字母拼写自己的名字。我使用的是德语 QWERTZ 键盘,不知道如何输入他们的名字。 联系人选择器 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 按钮并选择我的两位好朋友谢尔盖·米哈伊洛维奇·布林劳伦斯·爱德华·“拉里”·佩奇时,您可以看到联系人选择器仅显示他们的姓名,而不显示他们的电子邮件地址或其他信息(例如电话号码)。然后,我将他们的名字画在贺卡上。

联系人选择器,显示了通讯录中两位联系人的姓名。
使用联系人选择器从地址簿中选择两个名称。
之前在贺卡上绘制的两个所选联系人的姓名。
然后,这两个名称会被绘制到贺卡上。

异步剪贴板 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,每个数字仅用一笔画成。
使用七笔画绘制数字 1 到 7。
Fugu Greetings 应用上的标记图标,显示数字 7。
以应用图标标记的形式显示的笔画数计数器。

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);
  }
};

第二步是在服务工作线程中监听 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 上下文尚不支持)。

// 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 进行更新。

壁纸按钮会显示当天的图片。

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 通知中心内。

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 复选框可使应用保持唤醒状态。

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 应用,在用户长时间处于闲置状态后,画布已清空。
如果选中了临时复选框,并且用户长时间处于空闲状态,系统会清除画布。

结束语

哇,这趟旅程真刺激。一个示例应用中就包含如此多的 API。 请注意,我绝不会让用户为浏览器不支持的功能支付下载费用。 通过使用渐进增强,我可以确保仅加载相关代码。 由于 HTTP/2 的请求成本较低,因此这种模式应该适用于许多应用,不过对于非常大的应用,您可能需要考虑使用打包程序。

Chrome 开发者工具“网络”标签页,仅显示浏览器支持的代码所对应的文件请求。

由于并非所有平台都支持所有功能,因此应用在每个浏览器上的外观可能略有不同,但核心功能始终存在,并且会根据特定浏览器的功能逐步增强。即使在同一浏览器中,这些功能也可能会发生变化,具体取决于应用是以已安装应用的形式运行还是在浏览器标签页中运行。

在 Android Chrome 上运行的 Fugu Greetings,其中显示了许多可用的功能。
在桌面版 Safari 上运行的 Fugu Greetings,显示的功能较少。
在桌面版 Chrome 上运行的 Fugu Greetings,显示了许多可用的功能。

您可以复刻 GitHub 上的 Fugu

Chromium 团队正在努力让高级 Fugu API 变得更加出色。在构建应用时,我通过应用渐进增强技术来确保每个人都能获得良好、可靠的基准体验,同时让使用支持更多 Web 平台 API 的浏览器的人获得更好的体验。期待看到您在应用中如何使用渐进增强功能。

致谢

感谢 Christian LiebelHemanth HM,他们都为 Fugu Greetings 做出了贡献。本文档由 Joe MedleyKayce Basques 审核。 Jake Archibald 帮助我了解了服务工作线程上下文中动态 import() 的情况。