随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块”。

  • AMD - 最古老的模块系统之一,最初由 require.js 库实现。
  • CommonJS - 为 Node.JS 服务器创建的模块系统。
  • UMD - 另外一个模块系统,建议作为通用的模块系统,它与 AMD 和 CommonJS 都兼容。

现在,它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们。

(我表示最近才用到 CommonJS)


什么是模块

模块就是一个文件,一个脚本就是一个模块。就这么简单。

模块可以相互加载,并可以使用特殊的指令 export 和 import 来交互功能,从另一个模块调用一个模块的函数:

  • export 关键字标记了可以从当前模块外包访问的变量和函数
  • import 关键字允许从其他模块导入功能。
export function sayHi(use){
    alert(`hello`,${use}!`);
}

然后另一个文件可能导入并使用了这个函数:

import { sayHi } from './sayHi.js';
alert(sayHi);
sayHi("John");

由于模块支持特殊的关键字和功能,因此我们必须通过使用 script type=“module”> 特性(attribute)来告诉浏览器,此脚本应该被当作模块(module)来对待。

模块只通过 HTTP(s) 工作,而非本地


模块核心功能

始终使用 “use strict”

<script type="module">
  a = 5; // error
</script>

模块作用域

每个模块都有自己的顶级作用域,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。

在浏览器中,对于 HTML 页面,每个 <script type="module"> 都存在独立的顶级作用域。

下面是同一页面上的两个脚本,都是 type="module"。它们看不到彼此的顶级变量:

在浏览器中,我们可以通过将变量显式地分配给 window 的一个属性,使其成为窗口级别的全局变量。例如 window.user = "John"

这样所有脚本都会看到它,无论脚本是否带有 type="module"

也就是说,创建这种全局变量并不是一个好的方式。请尽量避免这样做。

模块代码仅在第一次导入时被解析

如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次。

顶层模块代码应该用于初始化,创建模块特定的内部数据结构。如果我们需要多次调用某些东西 —— 我们应该将其以函数的形式导出,就像我们在上面使用 sayHi 那样。

这正是因为该模块只执行了一次。生成导出,然后这些导出在导入之间共享,因此如果更改了 数据,在其他导入中也会看到。

这种行为实际上非常方便,因为它允许我们“配置”模块。

经典使用方法(你是不是在一些包见过)

  1. 模块导出一些配置方法,例如一个配置对象。
  2. 在第一次导入时,我们对其进行初始化,写入其属性。可以在应用顶级脚本中进行此操作。
  3. 进一步地导入使用模块。

import.meta

对象包含关于当前模块的信息。

它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL。

在一个模块中,“this” 是 undefined

模块脚本总是延迟的

模块脚本 总是 被延迟的,与 defer 特性 对外部脚本和内联脚本的影响相同。

  • 下载外部模块脚本 <script type="module" src="..."> 不会阻塞 HTML 的处理,它们会与其他资源并行加载。
  • 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比 HTML 加载速度更快),然后才会运行。
  • 保持脚本的相对顺序:在文档中排在前面的脚本先执行。

它的一个副作用是,模块脚本总是会“看到”已完全加载的 HTML 页面,包括在它们下方的 HTML 元素。

Async 适用于内联脚本

对于非模块脚本,async 特性(attribute)仅适用于外部脚本。异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档。

对于模块脚本,它也适用于内联脚本。

例如,下面的内联脚本具有 async 特性,因此它不会等待任何东西。

它执行导入(请求),并在导入完成时运行,即使 HTML 文档还未完成,或者其他脚本仍在等待处理中。

外部脚本

需要 CORS header

兼容性

旧时的浏览器不理解 type="module"。未知类型的脚本会被忽略。对此,我们可以使用 nomodule 特性来提供。

构建工具

在实际开发中,浏览器模块很少被以“原始”形式进行使用。通常,我们会使用一些特殊工具,例如 Webpack,将它们打包在一起,然后部署到生产环境的服务器。

使用打包工具的一个好处是 —— 它们可以更好地控制模块的解析方式,允许我们使用裸模块和更多的功能,例如 CSS/HTML 模块等。

构建工具做以下这些事儿:

  1. 从一个打算放在 HTML 中的 <script type="module"> “主”模块开始。
  2. 分析它的依赖:它的导入,以及它的导入的导入等。
  3. 使用所有模块构建一个文件(或者多个文件,这是可调的),并用打包函数(bundler function)替代原生的 import 调用,以使其正常工作。还支持像 HTML/CSS 模块等“特殊”的模块类型。
  4. 在处理过程中,可能会应用其他转换和优化:
    • 删除无法访问的代码。
    • 删除未使用的导出(“tree-shaking”)。
    • 删除特定于开发的像 consoledebugger 这样的语句。
    • 可以使用 Babel 将前沿的现代的 JavaScript 语法转换为具有类似功能的旧的 JavaScript 语法。
    • 压缩生成的文件(删除空格,用短的名字替换变量等)。

如果我们使用打包工具,那么脚本会被打包进一个单一文件(或者几个文件),在这些脚本中的 import/export 语句会被替换成特殊的打包函数(bundler function)。因此,最终打包好的脚本中不包含任何 import/export,它也不需要 type="module",我们可以将其放入常规的 <script>

<!-- 假设我们从诸如 Webpack 这类的打包工具中获得了 "bundle.js" 脚本 -->
<script src="bundle.js"></script>

关于构建工具说了这么多,但其实原生模块也是可以用的。