条件式异步依赖项

Web 功能: 顶级 await

顶级 await 允许模块充当异步函数,这意味着它们可以暂停模块执行以等待 promise。这对于有条件地加载异步依赖项(例如 polyfill 或大型辅助库)非常有用,因为只有在浏览器需要时才会加载。

通过利用顶级 await,您可以将条件加载逻辑封装在单个模块中,从而有效防止下游使用者模块在依赖项完全加载并准备就绪之前执行。

条件 polyfill 模式

虽然顶级 await 可用于有条件地加载任何作为模块的异步依赖项,但有条件依赖项加载模式的一个良好用途是为不支持特定功能的浏览器有条件地加载 polyfill。此方法将特征检测和动态导入封装在单个依赖项模块中。

在以下情况下,如果 HTMLElement.prototype 上不存在 popover 属性 polyfill,则会按条件加载该 polyfill:

// conditionally-load-polyfill.js

// Check if the feature is missing before doing work.
// MANDATORY: Prefer checking HTMLElement.prototype over window or document
// when checking for a global DOM attribute or property like popover.
if (!('popover' in HTMLElement.prototype)) {
  // Use top-level await to pause the execution of any module that imports this file 
  // until the polyfill finishes downloading and executing.
  await import('/path/to/popover-polyfill.js');
}

// Export a marker if needed by your application
export const polyfillLoaded = true;
// main.js

// MANDATORY: Because conditionally-load-polyfill.js uses top-level await, 
// this import will block execution of main.js until the polyfill is ready.
import './conditionally-load-polyfill.js';

// Now it is safe to use the feature (e.g., showing a popover)
const myPopover = document.getElementById('my-popover');
if (myPopover) {
  myPopover.showPopover();
}

对模块导入进行排序并避免 Safari 27 之前的 bug

强制性要求:您必须构建模块图,以便从单个入口点导入顶级 await 模块,并在下游模块求值之前完成:

  • 请勿在入口文件(例如 import './conditionally-load-polyfill.js'; import './app.js';)中放置同级静态导入。根据 ECMAScript 模块评估规范,顶级 await 仅会暂停导入它的父级模块,而不会暂停同级静态导入,因此在 polyfill 完成加载之前,./app.js 将在所有浏览器中并发评估。
  • 请勿同时从多个同级模块导入顶级 await 模块(例如,让 a.js 和 b.js 都静态导入 ./conditionally-load-polyfill.js)。虽然 Safari 27 修复了 WebKit 的模块加载器,以支持按照规范并发导入顶级 await 模块,但当多个模块同时导入包含顶级 await 的模块时,Safari 15-26.6 会失败(请参阅 MDN 浏览器兼容性)。

而是根据您的模块结构选择以下两种模式之一:

  1. 单入口模块(如上文中的 main.js 所示):如果入口模块直接运行依赖项初始化逻辑,请在该模块顶部静态导入 ./conditionally-load-polyfill.js,并在其主体中运行代码。
  2. 引导单独的应用模块:如果您的入口模块委托给下游应用模块(例如 ./app.js),请将动态 import() 调用与顶级 await 链接起来,以便仅在 polyfill 模块解析后提取和评估 ./app.js:
// main.js

// MANDATORY: Chain dynamic imports with top-level await when bootstrapping separate
// application modules so './app.js' waits for the polyfill to finish loading.
// DO NOT use sibling static imports (`import './conditionally-load-polyfill.js'; import './app.js';`),
// which evaluate concurrently and execute './app.js' before the polyfill is ready.
await import('./conditionally-load-polyfill.js');
await import('./app.js');

后备策略

虽然自 2021 年起,各大主流浏览器(Chrome 89、Edge 89、Firefox 89 和 Safari 15)就已支持基本的顶级 await,但直到 Safari 27 才完全符合规范(请参阅 MDN 浏览器兼容性)。

  • Safari 15–26.6:只要您遵循上一部分中的单入口点模式,确保多个同级模块不会同时导入顶级 await 模块,您就可以安全地依赖顶级 await,而无需额外的后备方案。
  • 旧版浏览器(2021 年之前):如果您的应用必须支持不具备任何顶级 await 支持的浏览器,请避免使用顶级 await,并将条件式 import() 编排封装在标准 async 函数或 .then() promise 链中。