如何特征检测浏览器是否支持动态 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 }
导出的想法
背景
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
分别是
尝试
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 }
导出的想法