聊一聊原生浏览器中的模块

自从ES2015定稿以来,我们通过 Babel 等转换工具可以在项目中直接使用【模块】。前端模块化开发已经是不可逆转,在 ECMAScript module 之前我们通过 requirejsseajsLABjs,甚至最早的时候我们通过闭包来实现模块化开发。目前一些主流的的浏览器厂商已经在他们新版的浏览器中原生支持了【模块】,今天我们就来原生浏览器中的模块到底如何。

目前原生支持模块用法的浏览器有:

  • Safari 10.1
  • Chrome 61
  • Firefox 60
  • Edge 16

要使用原生浏览器的模块,你只需要在 script 标签上添加一个 type=module 属性, 浏览器就会把这个脚本(内联脚本或者外联脚本)当作模块来处理。

  1. <script type="module">
  2. import {addTextToBody} from './utils.mjs';
  3. addTextToBody('Modules are pretty cool.');
  4. </script>
  1. // utils.mjs
  2. export function addTextToBody(text) {
  3. const div = document.createElement('div');
  4. div.textContent = text;
  5. document.body.appendChild(div);
  6. }

在线Demo

不支持裸导入(不能通过模块名直接导入)

一个合格的模块标识符必须满足下列条件之一:

  • 一个完整的非相对URL。通过 new URL(moduleSpecifier) 使用时不会报错。
  • / 开头。
  • ./ 开头。
  • ../ 开头。

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

  1. // 支持:
  2. import {foo} from 'https://jakearchibald.com/utils/bar.mjs';
  3. import {foo} from '/utils/bar.mjs';
  4. import {foo} from './bar.mjs';
  5. import {foo} from '../bar.mjs';
  6. // 不支持:
  7. import {foo} from 'bar.mjs';
  8. import {foo} from 'utils/bar.mjs';

通过 nomodule 向后兼容

如果当前浏览器支持 type=module 标签的话会自动忽略 nomodule 标签。这意味着你可以将模块暴露给支持模块的浏览器,同时可以给不支持模块的浏览器提供兼容方案。

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

在线Demo

默认延迟加载

当网络状况不好的时候,脚本加载会阻塞浏览器解析 HTML。通常我们可以通过在 script 标签上使用 defer 属性来解决阻塞问题,但是这也会造成脚本只有在文档解析完成后才执行,同时还要兼顾其他延迟脚本的执行顺序。默认情况下模块脚本的表现类似于 defer — 它不会阻塞 HTML 的解析。

模块脚本的执行队列与使用了 defer 的常规脚本一致。

  1. <!-- 这个脚本执行滞后于… -->
  2. <script type="module" src="1.mjs"></script>
  3. <!-- …这个脚本… -->
  4. <script src="2.js"></script>
  5. <!-- …但是先于这个脚本 -->
  6. <script defer src="3.js"></script>

在线Demo

内联模块也是延迟加载的

常规内联脚本会忽略 defer 然而内联模块总是 defer 的,不管它是否引入了东西。

  1. <!-- 这个脚本执行滞后于… -->
  2. <script type="module">
  3. addTextToBody("Inline module executed");
  4. </script>
  5. <!-- …这个… -->
  6. <script src="1.js"></script>
  7. <!-- …还有这个… -->
  8. <script defer>
  9. addTextToBody("Inline script executed");
  10. </script>
  11. <!-- …但是先于这个. -->
  12. <script defer src="2.js"></script>

在线Demo

内联/外联 模块都支持异步加载

在普通脚本中,async 能让脚本的下载不阻塞HTML的解析并在下载完成后尽快执行。和普通脚本不同,内联模块脚本支持异步加载的。

同样的,异步加载的模块可能不会按照它们在DOM中出现的顺序执行。

  1. <!-- 这个会在它引入的脚本加载完成后立即执行 -->
  2. <script async type="module">
  3. import {addTextToBody} from './utils.mjs';
  4. addTextToBody('Inline module executed.');
  5. </script>
  6. <!-- 这个会在其本身以及其引入的脚本加载完成后立即执行 -->
  7. <script async type="module" src="1.mjs"></script>

在线Demo

模块只执行一次

如果你使用过ES6的模块, 那么你肯定知道你可以多次引入同一模块但是他们只会执行一次。在Html中也一样, 一个URL模块脚本在一个页面中只会执行一次。

  1. <!-- 1.mjs 执行一次 -->
  2. <script type="module" src="1.mjs"></script>
  3. <script type="module" src="1.mjs"></script>
  4. <script type="module">
  5. import "./1.mjs";
  6. </script>
  7. <!-- 这个会执行多次 -->
  8. <script src="2.js"></script>
  9. <script src="2.js"></script>

在线Demo

遵循 CORS

不同于普通脚本,跨站引用模块脚本(及其引入)需要遵循CORS。 这意味着跨源模块脚本必须返回有效的CORS头,例如Access-Control-Allow-Origin:*。

  1. <!-- CORS检验失败,不会执行 -->
  2. <script type="module" src="https://….now.sh/no-cors"></script>
  3. <!-- 引入的模块CORS检验失败,不会执行 -->
  4. <script type="module">
  5. import 'https://….now.sh/no-cors';
  6. addTextToBody("This will not execute.");
  7. </script>
  8. <!-- CORS检验通过,会执行 -->
  9. <script type="module" src="https://….now.sh/cors"></script>

在线Demo

不需要凭证

针对同源请求,大部分基于CORS的API需要请求带上凭证(如:cookie),但是 fetch() 和模块脚本是个例外,他们默认不会带上相关凭证除非你明确指定。

如果你想在同源请求模块脚本时带上凭证,可以设置 crossorigin 属性。如果跨站请求也想带上的话,可以设置 crossorigin="use-credentials",需要注意的是跨站的站点需要在请求返回头中加上 Access-Control-Allow-Credentials: true

  1. <!-- 有凭证 (cookies) -->
  2. <script src="1.js"></script>
  3. <!-- 无凭证 -->
  4. <script type="module" src="1.mjs"></script>
  5. <!-- 有凭证 -->
  6. <script type="module" crossorigin src="1.mjs?"></script>
  7. <!-- 无凭证 -->
  8. <script type="module" crossorigin src="https://other-origin/1.mjs"></script>
  9. <!-- 有凭证 -->
  10. <script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs?"></script>

在线Demo

这里还有一个关于 模块只执行一次 的坑。当你通过一个URL引入一个模块时,如果一开始你以无凭证的方式请求,然后又以有凭证的方式再请求一次,你得到的返回都是无凭证请求的那次。这就是我为什么会在第二次请求时在URL后加上,用于区分两次请求。

更新:以上可能很快会改变。 默认情况下,fetch()和模块脚本都会向相同来源的URL发送凭据。

Mime-types

与普通脚本不同,模块脚本必须提供有效的JavaScript MIME类型,否则它们将不会执行。 HTML标准建议使用 text/javascript

目前原生浏览器对module 的支持基本上就是这些,更多信息大家可以关注后续的实现。

写于 2018年06月27日Web 3027

如非特别注明,文章皆为原创。

转载请注明出处: https://www.liayal.com/article/5b332b0453f6602cf86b0fa6

记小栈小程序上线啦~搜索【记小栈】【点击扫码】体验

你不想说点啥么?
😀😃😄😁😆😅😂🤣☺️😊😇🙂🙃😉😌😍😘😗😙😚😋😜😝😛🤑🤗🤓😎🤡🤠😏😒😞😔😟😕🙁☹️😣😖😫😩😤😠😡😶😐😑😯😦😧😮😲😵😳😱😨😰😢😥🤤😭😓😪😴🙄🤔🤥😬🤐🤢🤧😷🤒🤕😈👿👹👺💩👻💀☠️👽👾🤖🎃😺😸😹😻😼😽🙀😿😾👐👐🏻👐🏼👐🏽👐🏾👐🏿🙌🙌🏻🙌🏼🙌🏽🙌🏾🙌🏿👏👏🏻👏🏼👏🏽👏🏾👏🏿🙏🙏🏻🙏🏼🙏🏽🙏🏾🙏🏿🤝👍👍🏻👍🏼👍🏽👍🏾👍🏿👎👎🏻👎🏼👎🏽👎🏾👎🏿👊👊🏻👊🏼👊🏽👊🏾👊🏿✊🏻✊🏼✊🏽✊🏾✊🏿

评论

记小栈07-18 2018
@zionLu: 什么叫“唱过脚本”和“动西”

笔误,笔误

zionLu07-05 2018

什么叫“唱过脚本”和“动西”