[译] 浏览器中的 ECMAScript 模块

809 阅读7分钟

原文链接:ECMAScript modules in browsers

作者:Jake Archibald

浏览器现在可以使用 ES 模块(module)了!它们是:

  • Safari 10.1
  • Chrome 61
  • Firefox 60
  • Microsoft Edge 16
<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);
}

在线演示

您只需要在 script 元素上添加 type=module,浏览器就会将内联脚本或外部脚本作为 ECMAScript module 处理。

关于模块(module)已经有一些很棒的文章,但是我想分享一些在我测试和阅读规范的时候学到的浏览器特有的内容。

目前还不支持的某些 import 用法

// 已支持:
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';

// 不支持:
import {foo} from 'bar.mjs';
import {foo} from 'utils/bar.mjs';

有效的模块路径说明符必须符合下列条件之一:

  • 一个完整的非相对URL,这样在将其传给 new URL(moduleSpecifier) 的时候才不会报错。
  • / 开头的。
  • ./ 开头的。
  • ../ 开头的。

其他形式的说明符保留供将来使用,例如导入内置模块。

使用 nomodule 来向后兼容

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

在线演示

支持 type=module 的浏览器会忽略属性为 nomodule 的脚本。这意味着您可以给支持模块的浏览器提供模块树,同时给其他浏览器提供一个降级版本。

浏览器问题

  • Firefox 浏览器不支持 nomodule (issue)。已在 Firefox nightly 中修复!
  • Edge 浏览器不支持 nomodule (issue)。已在 Edge 16 中修复!
  • Safari 浏览器不支持 nomodule。已在 Safari 11 中修复!对于 10.1,这里有一个非常聪明的替代办法

默认情况下延迟执行

<!-- 这个脚本的执行会晚于… -->
<script type="module" src="1.mjs"></script>

<!-- …这个脚本… -->
<script src="2.js"></script>

<!-- …但是会在这个脚本之前执行。 -->
<script defer src="3.js"></script>

在线演示

执行的顺序是:2.js1.mjs3.js

script 在获取期间会阻塞 HTML 解析器,简直太糟糕了。对于常规脚本,您可以使用 defer 来避免阻塞,当然这也会推迟脚本的执行,直到文档完成解析,并与其他延迟脚本一起维护执行顺序。模块脚本的默认表现行为就像 defer ——当它正在获取时,没有办法让一个模块脚本阻塞 HTML 解析器。

模块脚本使用和添加了 defer 的常规脚本相同的执行队列。

内联脚本也是延时的

<!-- 这个脚本的执行会晚于… -->
<script type="module">
  addTextToBody("Inline module executed");
</script>

<!-- …这个脚本… -->
<script src="1.js"></script>

<!-- …和这个脚本… -->
<script defer>
  addTextToBody("Inline script executed");
</script>

<!-- …但是会在这个脚本之前执行。 -->
<script defer src="2.js"></script>

在线演示

执行顺序是1.js ,内联脚本,内联脚本,2.js

常规的内联脚本会忽略 defer ,然而内联模块脚本却总是被延迟,无论它们有没有导入任何东西。

Async 对内联、外部模块同样适用

<!-- 一旦获取了导入,就会执行此操作 -->
<script async type="module">
  import {addTextToBody} from './utils.mjs';

  addTextToBody('Inline module executed.');
</script>
<!-- 一旦获取了脚本和它的导入,就会执行此操作 -->
<script async type="module" src="1.mjs"></script>

在线演示

快速下载的脚本会在慢速下载的脚本之前执行。

与常规脚本一样,async 会让脚本在下载过程中不会阻塞 HTML 解析器,并且尽快地执行。与常规脚本不同,async 也适用于内联模块。

与往常的 async 一样,脚本不会按照它们出现在 DOM 中的顺序执行。

浏览器问题

  • Firefox 浏览器不支持内联模块脚本上的 async (issue)。已在 Firefox 59 中修复!

模块仅执行一次

<!-- 1.mjs 仅执行一次 -->
<script type="module" src="1.mjs"></script>
<script type="module" src="1.mjs"></script>
<script type="module">
  import "./1.mjs";
</script>

<!-- 然而,普通的脚本却执行多次 -->
<script src="2.js"></script>
<script src="2.js"></script>

在线演示

如果您理解 ES 模块,您就会知道您虽然可以引入它们很多次,但是它们却仅仅会执行一次。当然,这同样适用于HTML中的脚本模块 - 特定URL的模块脚本每页只执行一次。

浏览器问题

  • Edge 执行多次模块 (issue)。已修复,但是还没发布(希望 Edge 17 会带上这个修复内容)。

总是使用 CORS

<!-- 该脚本不会执行, 因为它不能通过 CORS 检查 -->
<script type="module" src="https://….now.sh/no-cors"></script>

<!-- 该脚本不会执行, 因为它引入的脚本之一不能通过 CORS 检查 -->
<script type="module">
  import 'https://….now.sh/no-cors';

  addTextToBody("This will not execute.");
</script>

<!-- 该脚本会执行,因为它通过了 CORS 检查 -->
<script type="module" src="https://….now.sh/cors"></script>

在线演示

与常规脚本不同,模块脚本(及其引入的内容)是通过 CORS 获取的。这就意味着跨域的模块脚本必须返回有效的 CORS 响应头 ,比如 Access-Control-Allow-Origin: *

浏览器问题

  • Firefox 加载 Demo 页面失败 (issue)
  • Edge 加载没有 CORS header 的模块脚本 (issue)。 已在 Edge 16 中修复!

不携带凭据

<!-- 携带凭据获取(cookie 等) -->
<script src="1.js"></script>

<!-- 不携带凭据获取 -->
<script type="module" src="1.mjs"></script>

<!-- 携带凭据获取 -->
<script type="module" crossorigin src="1.mjs?"></script>

<!-- 不携带凭据获取 -->
<script type="module" crossorigin src="https://other-origin/1.mjs"></script>

<!-- 携带凭据获取 -->
<script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs?"></script>

在线演示

如果请求来自相同的源,大多数基于 CORS 的 API 会发送凭据(cookie 等),但是 fetch() 和模块脚本却是例外的——非您要求它们,否则它们不会发送凭据除。

您可以通过添加 crossorigin 属性来向同源模块添加凭据(这对我来说似乎有点奇怪,我在规范中对此提出质疑)。如果您打算向其他的源也发送凭据,使用 crossorigin="use-credentials"。注意其他源必须使用 Access-Control-Allow-Credentials:true 的响应头来响应。

此外,还有一个与“模块只执行一次”规则相关的问题。模块由其URL标记,因此如果首次请求了一个模块而不携带凭据,然后再次携带凭据请求该模块,那么第二次获得的依然是不携带凭证的模块。 这就是为啥我在上面的URL中使用 问号 ? 的原因,使它们成为唯一的。

更新: 上面的情况可能很快就会发生改变。fetch() 和模块脚本默认都会向同源的 URL 发送凭据。Issue

浏览器问题

  • Chrome 使用凭据请求同源模块(issue。已在 Chrome 61 中修复!
  • Safari 即使添加了 crossorigin 属性,也不使用凭据请求同源模块(issue)。
  • Edge 即使添加了 crossorigin 属性,也不使用凭据请求同源模块(issue。已在 Edge 16 中修复!
  • Edge 默认请求同源模块的时候携带了凭据(issue)。

MIME 类型

不同于常规脚本,模块脚本必须是有效的 JavaScript MIME 类型中的一种类型,否则模块就不会执行。HTML 标准建议使用 text/javascript

浏览器问题

  • Edge 执行无效的 MIME 类型脚本(issue

这就是我目前学到的内容啦。毋庸置疑,我对 ES 模块登陆浏览器感到非常兴奋!

性能建议,动态导入等等!

请查阅有关 Web Fundamentals 的文章,深入了解模块使用情况。