翻译自:https://jakearchibald.com/2017/es-modules-in-browsers/

目前大部分浏览器已支持ES module:

<script type="module">
  import {addTextToBody} from './utils.mjs';

  addTextToBody('Modules are pretty cool.');
</script>
// utils.mjs
export function addTextToBody(text) {
  const div = document.createElement('div');
  div.textContent = text;
  document.body.appendChild(div);
}

demo

只需要在script标签中添加type=module属性,浏览器就会该script(行内或外部)识别为ECMAScript module.

关于es module这里已有很多很好的文章,但是我还是想分享一些关于浏览器特性的内容:

目前import不支持加载内部模块的写法

// Supported:
import {foo} from 'https://jakearchibald.com/utils/bar.mjs';
import {foo} from '/utils/bar.mjs';
import {foo} from './bar.mjs';
import {foo} from '../bar.mjs';

// Not supported:
import {foo} from 'bar.mjs';
import {foo} from 'utils/bar.mjs';

目前加载的模块路径只支持以下形式:

其他路径符保留用于以后使用,比如用于支持加载内部模块。

向后兼容

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

demo

对于支持type=module的浏览器,会忽略指定nomodule属性的标签。从而对于不支持es模块的浏览器提供一种回退的方案。

默认延迟加载

<!-- This script will execute after… -->
<script type="module" src="1.mjs"></script>

<!-- …this script… -->
<script src="2.js"></script>

<!-- …but before this script. -->
<script defer src="3.js"></script>

demo

执行顺序为: 2.js,1.mjs,3.js

一般的script标签在加载的时候会阻塞html的解析,可以通过defer属性使script在文档解析完成之后再加载,从而避免阻塞,同时执行顺序为出现包含defer属性的标签顺序。

module script默认行为类似defer属性,并且没有办法让module script像普通的script那样在加载时阻塞html解析。

module script和存在defer属性的普通script使用想用的执行队列。

行内script也会延迟加载

<!-- 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>

demo

执行顺序为: 1.js,inline script, inline module, 2.js

一般的行内script标签会忽略defer属性,而行内module script则总是延迟加载的,不管有没有import相关资源。

在外部module和行内module上使用async

<!-- This executes as soon as its imports have fetched -->
<script async type="module">
  import {addTextToBody} from './utils.mjs';

  addTextToBody('Inline module executed.');
</script>

<!-- This executes as soon as it & its imports have fetched -->
<script async type="module" src="1.mjs"></script>

demo

执行顺序: 下载快的先执行,下载慢的后执行。

和一般的script一样,async使script不会阻塞html的解析,以及一旦下载完成就开始执行。
和一般的script的区别在于,async可以作用在行内module上。

wwl注:

  1. 对于一般的script,async只能作用在外部脚本上;
  2. defer在 parse html完成之后,DOMContentLoaded之前,按出现顺序执行。

模块仅执行一次

<!-- 1.mjs only executes once -->
<script type="module" src="1.mjs"></script>
<script type="module" src="1.mjs"></script>
<script type="module">
  import "./1.mjs";
</script>

<!-- Whereas classic scripts execute multiple times -->
<script src="2.js"></script>
<script src="2.js"></script>

demo

如果你已经了解了ES module,那你应该知道一个模块可以被import多次,但是只执行一次。同样的,对于module script,相同的url只会执行一次。

Browser issues

Edge executes modules multiple times (issue). Fixed, but not yet shipped (expect Edge 17 to ship with the fix).

CORS限制

<!-- 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>

demo

和一般的script不同,加载module script(以及import的脚本)会检查CORS,所以对于跨域的module script必须返回有效的CROS请求头,例如Access-Control-Allow-Origin: *

No credentials

<!-- Fetched with credentials (cookies etc) -->
<script src="1.js"></script>

<!-- Fetched without credentials -->
<script type="module" src="1.mjs"></script>

<!-- Fetched with credentials -->
<script type="module" crossorigin src="1.mjs?"></script>

<!-- Fetched without credentials -->
<script type="module" crossorigin src="https://other-origin/1.mjs"></script>

<!-- Fetched with credentials-->
<script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs?"></script>

demo

对于大部分基于CORS的API,如果请求来自同源则会发送凭证信息(例如cookie),但是fetch()和module script除外--默认它们不会发送凭证信息。

对于同源的模块可以通过添加crossorigin属性发送凭证信息(虽然属性名看起来有点奇怪)。如果想要发送凭证到其他域,则需要设置crossorigin="use-credentials",同时其他域的响应的头部应该带有Access-Control-Allow-Credentials: true

目前我们已经了解了"模块只会执行一次"的规则。URL作为key来区分不同模块,所以如果先请求了一个不带凭证信息的模块,然后又请求了带凭证信息的模块,那么代码得到的还是不带凭证信息的模块。这就是为什么在上面的示例中,我在URL的后面添加了一个?

Update: 目前已作调整,在同源的情况下,fetch()和module script会带上凭证信息。Issue

Mime-types

和一般的script不同,module script必须是合法的javascript MIME类型,否则不会被执行。 The HTML Standard推荐使用text/javascript

demo

这是目前我所了解到的。我对ES module登录浏览器真真感到贼拉兴奋。

Performance recommendations, dynamic import & more!

查看 article on Web Fundamentals ,学习更多关于模块的用法。