在浏览器中使用 ECMAScript Modules
原文链接:ECMAScript Modules in Browsers
译者:张弈
校对:余博伦
转载请注明出处。
ES Modules 正在登陆各大浏览器!下列浏览器版本均已支持:
- Safari 10.1。
- Chrome Canary 60 – 在 chrome:flags 设置页面中 the Experimental Web Platform flag的相关参数设置。
- Firefox 54 – 在 about:config 设置页面中 dom.moduleScripts.enabled的相关参数设置。
- Edge 15 – 在 about:flags页面中the Experimental JavaScript Features的相关参数设置。
<script type="module">
import {addTextToBody} from './utils.js';
addTextToBody('Modules are pretty cool.');
</script>
// utils.js export function addTextToBody(text) {
const div = document.createElement('div');
div.textContent = text;
document.body.appendChild(div);
}
你需要做的只是为 script 标签添加 type=module 属性, 这样浏览器就会将你引入或编写的脚本视为 ECMAScript module 处理。
网络上已经有了一些介绍 module 的精彩文章, 但是我仍然想要分享一些读到且尝试过的,浏览器相关的 modules 特性:
import 引入路径支持还不完善
// 支持如下路径格式: import {foo} from 'https://jakearchibald.com/utils/bar.js';
import {foo} from '/utils/bar.js';
import {foo} from './bar.js';
import {foo} from '../bar.js';
// 下列格式不支持: import {foo} from 'bar.js';
import {foo} from 'utils/bar.js';
引入模块的路径必须符合以下条件之一:
- 一个完整的绝对路径,可以通过 new URL(moduleSpecifier) 不报错。
- 以 / 开头。
- 以 ./ 开头。
- 以 ../ 开头。
其他的路径格式可能在将来会有支持,例如导入内置模块。
引入 nomodule 属性向后兼容
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
支持 type=module 属性的浏览器可以忽略附带 nomodule属性的脚本标签。在上面这个示例中,如果浏览器支持载入模块则会加载前一个标签的脚本,而其他不支持模块的浏览器则会载入后一个名为 fallback.js 的脚本。
浏览器遗留问题
- Firefox不支持 nomodule (issue). 在其nightly版本中已经被修复!
- Edge 不支持 nomodule (issue)。
- Safari 10.1 不支持 nomodule, 但它在最新的技术预览中修复了。 10.1版本中有一个 很棒的支持方案。
默认延迟
<!-- This script will execute after… -->
<script type="module" src="1.js"></script>
<!-- …this script… -->
<script src="2.js"></script>
<!-- …but before this script. -->
<script defer src="3.js"></script>
演示页面顺序应该是 2.js, 1.js, 3.js。
脚本阻塞HTML加载是非常糟糕的情况。 你可以为脚本标签添加一个 defer 属性来防止页面阻塞发生,同时也会使得这段脚本在文档完成解析之后才会运行。默认的,module 类型的脚本的加载也类似于添加 defer 属性的脚本 – 毕竟我们毫无理由让这些脚本阻塞页面的加载。
module 脚本与添加 defer 属性的脚本使用相同的执行队列。
内联脚本同样会延迟加载
<!-- This script will execute after… -->
<script type="module">
addTextToBody("Inline module executed");
</script>
<!-- …this script… -->
<script src="1.js"></script>
<!-- …and this script… -->
<script defer>
addTextToBody("Inline script executed");
</script>
<!-- …but before this script. -->
<script defer src="2.js"></script>
演示页面加载顺序为 1.js, 内联脚本, 内联模块, 2.js。
通常内联脚本会忽略 defer 属性,与此同时,不管内联的 module 脚本是否有引入什么,都会被延迟加载。
外部和内联 module 的 async 如何工作
<!-- This executes as soon as its imports have fetched -->
<script async type="module">
import {addTextToBody} from './utils.js';
addTextToBody('Inline module executed.');
</script>
<!-- This executes as soon as it & its imports have fetched -->
<script async type="module" src="1.js"></script>
演示页面 下载的较快的脚本会先运行。
async 属性可以使脚本不阻塞HTML加载并且在下载完成后立即运行。与常规脚本不同,async也适用于内联 module.
我们使用 async时,脚本可能不会按照它在DOM中的书写的顺序执行。
浏览器遗留问题
- Firefox 在内联模块脚本上不支持 async (issue)。
Module 只执行一次
<!-- 1.js only executes once -->
<script type="module" src="1.js"></script>
<script type="module" src="1.js"></script>
<script type="module">
import "./1.js";
</script>
<!-- Whereas normal scripts execute multiple times -->
<script src="2.js"></script>
<script src="2.js"></script>
如果你对ES Modules比较了解,你应该知道我们可以多次使用 import 导入,但 module 只会执行一次。这一特性同样适用于HTML中的 module 脚本,从一个 URL 引入的 module 脚本在一个页面中只会执行一次。
浏览器遗留问题
*Edge 可以多次执行模块 (issue).
module 均以跨域资源共享方式载入
<!-- This will not execute, as it fails a CORS check -->
<script type="module" src="https://….now.sh/no-cors"></script>
<!-- This will not execute, as one of its imports fails a CORS check -->
<script type="module">
import 'https://….now.sh/no-cors';
addTextToBody("This will not execute.");
</script>
<!-- This will execute as it passes CORS checks -->
<script type="module" src="https://….now.sh/cors"></script>
与常规脚本不同,module 脚本都是通过跨域资源共享的方式载入的。这意味着载入的 module 脚本必须返回正确的CORS头,例如 Access-Control-Allow-Origin: * .
浏览器遗留问题
- Firefox 无法加载演示页面 (issue)。
- Edge 加载演示页面无需CORS头 ([issue[(https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11865934/))。
不带凭据
<!-- Fetched with credentials (cookies etc) -->
<script src="1.js"></script>
<!-- Fetched without credentials -->
<script type="module" src="1.js"></script>
<!-- Fetched with credentials -->
<script type="module" crossorigin src="1.js?"></script>
<!-- Fetched without credentials -->
<script type="module" crossorigin src="https://other-origin/1.js"></script>
<!-- Fetched with credentials-->
<script type="module" crossorigin="use-credentials" src="https://other-origin/1.js?"></script>
如果是相同的来源的请求,大多数基于CORS的API都会发送凭据(cookie等),但 fetch() 和 module 脚本是例外 —— 默认是不携带凭据的。
你可以通过添加 crossorigin 属性,来使同源 module 携带凭据(对此我感到有点奇怪,在规范讨论中我已经提出质疑)。如果是非同源的情况,你可以设置 crossorigin=”use-credentials” 属性。注意,请求的来源必须有包涵 Access-Control-Allow-Credentials: true头部信息的响应。
另外,还有一个与“module 只执行一次”规则有关的疑难杂症。每个 module 都是根据引入它的URL来标记的,因此如果你先请求来不带凭据的模块,再使用携带凭据的请求方式依然会获得之前的无凭据的 module,这就是为什么我在上面的示例中,在URL后面加上了“?”来确保载入 module 的唯一性。
浏览器遗留问题
- Chrome 要求有凭据且相同来源的模块 (issue)。
- Safari 要求没有凭据且相同来源的模块,即使你使用crossorigin属性 (issue)。
- Edge 默认情况下,它会将凭据发送到相同的源,但是如果添加了crossorigin 属性,则不会发送(issue)。
Firefox是唯一做得好的。
Mime-types
与常规脚本不同,module 脚本必须使用其中一种有效的JavaScript MIME类型 否则它们将不会执行。
浏览器遗留问题
- Edge 使用无效的MIME类型执行脚本(issue).
以上就是我迄今为止所了解到的有关浏览器对 ES Modules 支持的情况,不用说为此我有多兴奋了吧!