如何特征检测浏览器是否支持动态 ES6 模块加载?

How to feature-detect whether a browser supports dynamic ES6 module loading?

背景

JavaScript ES6规范支持module imports aka ES6 modules

静态导入使用起来非常明显,并且已经有了 quite a good browser support, but dynamic import is still lacking behind
因此,您的代码很有可能使用静态模块(当这些模块不受支持时,代码甚至不会执行),但浏览器可能会错过对 dynamic 导入的支持。因此,检测动态加载是否有效(在尝试实际加载代码之前)可能很有用。还有as browser detection is frowned upon,我当然想使用特征检测。

用例可能是显示错误,回退到其他一些 data/default/loading 算法,为开发人员提供在模块中动态加载(数据,例如惰性模式样式)的优势,同时允许直接传递数据等的回退。基本上,所有可能使用特征检测的常见用例。

问题

现在,由于动态模块是使用 import('/modules/my-module.js') 导入的,因此显然会尝试检测函数是否存在,如下所示:

// this code does NOT work
if (import) {
    console.log("dynamic import supported")
}

我想,对于每个(?)其他函数,这都可以工作,但问题似乎是:因为 import 分别是 reserved keyword in ECMAScript,现在显然也用于表示静态导入,不是真正的函数。正如 MDN 所说,它是 "function-like".

尝试

import() 会导致语法错误,因此这并不是真正可用的,import("") 会导致 Promise 被拒绝,这可能很有用,但看起来确实是 hackish/like 一种解决方法.此外,它需要一个异步上下文(await 等)仅用于功能检测,这不是很好。
typeeof import也直接失败,导致语法错误,因为关键字("unexpected token: keyword 'import'").

那么可靠地检测浏览器是否支持 dynamic ES6 模块的最佳方法是什么?

编辑: 正如我看到的一些答案,请注意解决方案当然应该尽可能普遍使用,即例如CSPs may prevent the use of eval 并且在 PWA 中你不应该假设你总是在线,所以仅仅尝试请求一些任意文件可能会导致不正确的结果。

想到了三种方法,所有方法都依赖于使用 import():

获取语法错误
  • eval 中(但与某些 CSP 发生冲突)
  • 插入 script 标签
  • 使用多个静态 script 标签

公共位

您有 import() 使用 foo 或类似的东西。这是一个无效的模块说明符,除非它在您的导入映射中,因此不应引起网络请求。使用 catch 处理程序来捕获加载错误,并在 import() "call" 周围使用 try/catch 来捕获有关模块说明符的任何同步错误,以避免弄乱您的错误控制台。请注意,在不支持它的浏览器上,我认为您无法避免控制台中的语法错误(至少,window.onerror 在 Legacy Edge 上不适合我)。

eval

...因为 eval 不一定是邪恶的;例如,如果 保证 使用您自己的内容(但同样,CSP 可能会限制):

let supported = false;
try {
    eval("try { import('foo').catch(() => {}); } catch (e) { }");
    supported = true;
} catch (e) {
}
document.body.insertAdjacentHTML("beforeend", `Supported: ${supported}`);

插入 script

let supported = false;
const script = document.createElement("script");
script.textContent = "try { import('foo').catch(() => { }); } catch (e) { } supported = true;";
document.body.appendChild(script);
document.body.insertAdjacentHTML("beforeend", `Supported: ${supported}`);

使用多个静态 script 标签

<script>
let supported = false;
</script>
<script>
try {
    import("foo").catch(() => {});
} catch (e) {
}
supported = true;
</script>
<script>
document.body.insertAdjacentHTML("beforeend", `Supported: ${supported}`);
</script>


那些都在 Chrome、Chromium Edge、Firefox 等上默默地报告 true;和 Legacy Edge 上的 false(语法错误)。

在更多的研究中,我发现 this gist script,动态特征检测的 JS 必不可少的部分:

function supportsDynamicImport() {
  try {
    new Function('import("")');
    return true;
  } catch (err) {
    return false;
  }
}
document.body.textContent = `supports dynamic loading: ${supportsDynamicImport()}`;

所有功劳归于 @ebidel 来自 GitHub!

总之,这有两个问题:

  • 使用 eval, which is evil, especially for websites that use a CSP.
  • 根据要点中的评论,Chrome 和 Edge(的某些版本)中确实存在 误报 。 (即 returns true 尽管这些浏览器实际上并不支持它)

以下代码检测动态导入支持而没有误报。该函数实际上从数据 uri 加载有效模块(因此即使在离线时它也能工作)。

函数 hasDynamicImport returns a Promise,因此它需要原生或 polyfilled Promise 支持。另一方面,动态导入 returns 一个 Promise。因此,如果不支持 Promise,则没有必要检查动态导入支持。

function hasDynamicImport() {
  try {
    return new Function("return import('data:text/javascript;base64,Cg==').then(r => true)")();
  } catch(e) {
    return Promise.resolve(false);
  }
}

hasDynamicImport()
  .then((support) => console.log('Dynamic import is ' + (support ? '' : 'not ') + 'supported'))
  • 优势 - 无误报
  • 缺点 - 使用 eval

这已经在最新的 Chrome、Chrome 62 和 IE 11(使用 polyfilled Promise)上进行了测试。

如何在 type='module' 脚本中加载你的 JS,否则使用 nomodule 属性加载下一个脚本,如下所示:

<script type="module" src="main.mjs"></script>
<script nomodule src="fallback.js"></script>

理解 type="module" 的浏览器会忽略带有 nomodule 属性的脚本。

参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import & https://v8.dev/features/modules#browser

我自己的解决方案,它需要 1 个额外的请求,但没有全局变量,没有 eval,并且严格符合 CSP:

进入你的HTML

<script type="module">
import('./isDynamic.js')
</script>
<script src="/main.js" type="module"></script>

isDynamic.js

let value

export const then = () => (value = value === Boolean(value))

main.js

import { then } from './isDynamic.js'

console.log(then())

备选

没有额外的请求,也没有eval/globals(当然),只需要DataURI支持:

<script type="module">
import('data:text/javascript,let value;export const then = () => (value = value === Boolean(value))')
</script>
<script src="/main.js" type="module"></script>
import { then } from 'data:text/javascript,let value;export const then = () => (value = value === Boolean(value))'

console.log(then())

它是如何工作的?很简单,因为它是相同的 URL,调用两次,它只调用动态模块一次...并且由于动态导入解析了 thenable 对象,它单独解析了 then() 调用。

感谢 Guy Bedford 关于 { then } 导出的想法