模块

CommonJS

CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组 织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。 注意 一般认为,Node.js的模块系统使用了CommonJS规范,实际上并不完全正确。Node.js 使用了轻微修改版本的 CommonJS,因为 Node.js 主要在服务器环境下使用,所以不需要 考虑网络延迟问题。考虑到一致性,本节使用 Node.js 风格的模块定义语法。 CommonJS 模块定义需要使用 require()指定依赖,而使用 exports 对象定义自己的公共 API。 下面的代码展示了简单的模块定义:

var moduleB = require('./moduleB'); 
module.exports = {
  stuff: moduleB.doStuff();
};

在 CommonJS 规范中,模块加载和执行是同步的。也就是说,当一个模块被加载时,它会立即执行并返回一个对象,该对象包含了该模块所定义的函数、变量等内容。在这个过程中,如果该模块还依赖其他模块,则会在加载这些模块后再执行该模块的代码。因此 require()可以像下面这样以编程 方式嵌入在模块中:

console.log('moduleA');
if (loadCondition) {
  require('./moduleA');
}

AMD

CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD, Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。AMD 的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并 在依赖加载完成后立即执行依赖它们的模块。

// ID 为'moduleA'的模块定义。moduleA 依赖 moduleB,
// moduleB 会异步加载 
define('moduleA', ['moduleB'], function(moduleB) {
   return {
    stuff: moduleB.doStuff();
}; });

在 AMD 规范中,模块加载和执行是异步的。也就是说,当一个模块被加载时,它不会立即执行,而是先等待其他模块加载完毕后再执行。这种异步的模块加载和执行方式适用于浏览器环境,因为在浏览器环境中,文件的读取和加载速度比较慢,而且模块之间的依赖关系比较复杂,可能会出现循环依赖等问题。因此,AMD 规范引入了异步加载机制,能够更好地处理这些情况。

UMD

了统一 CommonJS 和 AMD 生态系统,通用模块定义(UMD,Universal Module Definition)规范 应运而生。UMD 可用于创建这两个系统都可以使用的模块代码。本质上,UMD 定义的模块会在启动时 检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE) 中。虽然这种组合并不完美,但在很多场景下足以实现两个生态的共存。

使用ES6模块

与传统脚本不同,所有模块都会像<script defer>加载的脚本一样按顺序执行。解析到<script type="module">标签后会立即下载模块文件,但执行会延迟到文档解析完成。无论对嵌入的模块代码, 还是引入的外部模块文件,都是这样。<script type="module">在页面中出现的顺序就是它们执行 的顺序。与<script defer>一样,修改模块标签的位置,无论是在<head>还是在<body>中,只会影 响文件什么时候加载,而不会影响模块什么时候加载。 下面演示了嵌入模块代码的执行顺序:

<!-- 第二个执行 -->
<script type="module"></script>
<!-- 第三个执行 -->
<script type="module"></script>
<!-- 第一个执行 --> <script></script>
<!-- 另外,可以改为加载外部 JS 模块定义:  -->
<!--  第二个执行 -->
<script type="module" src="module.js"></script>
<!-- 第三个执行 -->
<script type="module" src="module.js"></script>
 <!-- 第一个执行 --> 
<script><script>

也可以给模块标签添加 async 属性。这样影响就是双重的:不仅模块执行顺序不再与<script>标 签在页面中的顺序绑定,模块也不会等待文档完成解析才执行。不过,入口模块仍必须等待其依赖加载 完成。 与<script type="module">标签关联的 ES6 模块被认为是模块图中的入口模块。一个页面上有多少个入口模块没有限制,重复加载同一个模块也没有限制。同一个模块无论在一个页面中被加载多少次,也不管它是如何加载的,实际上都只会加载一次,如下面的代码所示:

<!-- moduleA 在这个页面上只会被加载一次 -->
<script type="module">
  import './moduleA.js'
<script>
<script type="module">
  import './moduleA.js'
<script>
<script type="module" src="./moduleA.js"></script>
<script type="module" src="./moduleA.js"></script>

嵌入的模块定义代码不能使用 import 加载到其他模块。只有通过外部文件加载的模块才可以使用 import 加载。因此,嵌入模块只适合作为入口模块。

模块加载

ECMAScript6模块这个过程与 AMD 风格的模块加载非常相似。模块文件按需加载,且后续模块的请求会因为每个依 赖模块的网络延迟而同步延迟。即,如果 moduleA 依赖 moduleB,moduleB 依赖 moduleC。浏览器在 对 moduleB 的请求完成之前并不知道要请求 moduleC。这种加载方式效率很高,也不需要外部工具, 但加载大型应用程序的深度依赖图可能要花费很长时间。

模块行为

ECMAScript 6 模块借用了 CommonJS 和 AMD 的很多优秀特性。下面简单列举一些。

  • 模块代码只在加载后执行。

  • 模块只能加载一次。

  • 模块是单例。

  • 模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互。

  • 模块可以请求加载其他模块。

  • 支持循环依赖。 ES6 模块系统也增加了一些新行为。

  • ES6 模块默认在严格模式下执行。

  • ES6 模块不共享全局命名空间。

  • 模块顶级 this 的值是 undefined(常规脚本中是 window)。

  • 模块中的 var 声明不会添加到 window 对象。

  • ES6 模块是异步加载和执行的。

浏览器运行时在知道应该把某个文件当成模块时,会有条件地按照上述 ECMAScript 6 模块行为来施 15 加限制。与<script type="module">关联或者通过 import 语句加载的 JavaScript 文件会被认定为模块。

向后兼容

<!-- 支持模块的浏览器会执行这段脚本 -->
<!-- 不支持模块的浏览器不会执行这段脚本 -->
<script type="module" src="module.js"></script>
 <!-- 支持模块的浏览器不会执行这段脚本 -->
 <!-- 不支持模块的浏览器会执行这段脚本 -->
<!-- nomodule 属性。此属性通 知支持 ES6 模块的浏览器不执行脚本。不支持模块的浏览器无法识别该属性,从而忽略这个属性的存在。 -->
<script nomodule src="script.js"></script>

Last updated