JavaScript模块化

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的 require、Python 的 import,甚至就连 CSS 都有 @import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

Immediately Invoked Function Expression

在早期,JavaScript 程序本来很小,它们大多被用来执行独立的脚本任务,在 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境。于是,一系列问题逐渐暴露出来:

  • 全局变量灾难
  • 函数命名冲突
  • 复杂依赖关系

最初的模块定义是通过 IIFE,将模块的代码包裹在一个函数中,这样模块的变量就无法暴露在全局中。曾经风靡的封装风格就是大名鼎鼎的 jQuery ,这种风格虽然灵活了些,但并未解决根本问题,所需依赖还是得外部提前提供,还是增加了全局变量。

  • 如何安全的包装一个模块的代码,不污染模块外的任何代码?
  • 如何唯一标识一个模块?
  • 如何优雅的把模块的 API 暴露出去,不增加全局变量?
  • 如何方便的使用所依赖的模块?

近年来,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。在 ES2015 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES2015 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 等规范,成为浏览器和服务器通用的模块解决方案。

CommonJS Modules/1.x

CommonJS 是一个标准化 Web 浏览器之外的 JavaScript 模块生态系统的项目。CommonJS 关于模块如何工作的规范如今广泛用于 Node.js 的服务器端 JavaScript。它也用于浏览器端 JavaScript,但该代码必须使用转译器打包,因为浏览器不支持 CommonJS。CommonJS 可以通过使用 require() 函数和 module.exports 来识别。

js 复制代码
// 导出模块
exports.fn = function() {}
module.exports = {
	// The module code goes here
}
// 导入模块
const fn = require('./module')

模块标识符

  1. 模块标识符是由正斜杠分隔的"术语"字符串。
  2. 术语必须是驼峰命名法标识符、. 或 .. 。
  3. 模块标识符可能没有像 .js 这样的文件扩展名。
  4. 模块标识符可以是"相对的"或"顶级的"。如果第一项是 . 或者 .. 则模块标识符是"相对的"。
  5. 顶级标识符是从概念模块名称空间根解析出来的。
  6. 相对标识符是相对于编写和调用 require 的模块的标识符来解析的。

模块上下文

  1. 在模块中,有一个自由变量 require ,即一个函数。

    • require 函数接受模块标识符。
    • require 返回外部模块导出的 API。
    • 如果存在依赖循环,外部模块可能在其传递依赖之一需要时尚未完成执行;在这种情况下, require 返回的对象必须至少包含外部模块在调用 require 导致当前模块执行之前准备好的导出。
    • 如果无法返回请求的模块, require 必须抛出错误。
    • require 函数可以具有 main 属性。
    • require 函数可以具有 paths 属性,即到顶级模块目录的路径的优先级路径字符串数组,从高到低。
  2. 在模块中,有一个名为 exports 的自由变量,它是模块在执行时可以添加其 API 的对象

    • 模块必须使用 exports 对象作为唯一的导出方式。
  3. 在一个模块中,必须有一个自由变量 module,即一个 Object。

    • "模块"对象必须具有 id 属性,该属性是模块的顶级 id 。 id 属性必须使得 require(module.id) 将返回 module.id 源自的导出对象。
    • "模块"对象可能有一个 uri 字符串,它是创建模块的资源的完全限定 URI。沙箱中不得存在 uri 属性。

Asynchronous Module Definition

异步模块定义 (AMD) API 指定了一种定义模块的机制,以便可以异步加载模块及其依赖项。这特别适合于同步加载模块会带来性能、可用​​性、调试和跨域访问问题的浏览器环境。AMD 的实施具有以下优势:

  • 网站性能改进:AMD 实现仅在需要时才加载较小的 JavaScript 文件。
  • 更少的页面错误:AMD 实现允许开发人员定义在执行模块之前必须加载的依赖项,因此模块不会尝试使用尚不可用的外部代码。
js 复制代码
define('alpha', [
	'require', 
	'exports', 
	'beta'
], function(require, exports, beta) {
  // The module code goes here
})

API规范

该规范定义了单个函数"define",可用作自由变量或全局变量。函数的签名: define(id?, dependencies?, factory)。

  1. 参数 id 是一个字符串文字。它指定正在定义的模块的 id。此参数是可选的,如果不存在,则模块 id 应默认为加载程序为给定响应脚本请求的模块的 id。如果存在,模块 id 必须是"顶级"或绝对 id(不允许相对 id)。

模块 id 可用于标识正在定义的模块,它们也可用于依赖项数组参数中。 AMD 中的模块 id 是 CommonJS 模块标识符中允许的超集。引用该页面:

  • 模块标识符是由正斜杠分隔的"术语"字符串。
  • 术语必须是驼峰命名法标识符、. 或 ..。
  • 模块标识符可能没有像 .js 这样的文件扩展名。
  • 模块标识符可以是"相对的"或"顶级的"。如果第一项是 . 或者 ..,则模块标识符是"相对的"。
  • 顶级标识符是从概念模块名称空间根解析出来的。
  • 相对标识符是相对于编写和调用 require 的模块的标识符来解析的。
  1. 参数 dependency 是模块 id 的数组文字,这些模块 id 是正在定义的模块所需的依赖项。必须在执行模块工厂函数之前解析依赖项,并且解析后的值应作为参数传递给工厂函数,参数位置对应于依赖项数组中的索引。

依赖项 id 可以是相对 id,并且应该相对于正在定义的模块进行解析。换句话说,相对 id 是相对于模块的 id 解析的,而不是相对于用于查找模块 id 的路径。

该规范定义了三个具有不同解析的特殊依赖项名称。如果 require、exports 或 module 的值出现在依赖项列表中,则该参数应解析为 CommonJS 模块规范定义的相应自由变量。

依赖项参数是可选的。如果省略,则应默认为 ["require", "exports", "module"]。但是,如果工厂函数的数量(长度属性)小于 3,则加载程序可能会选择仅使用与函数的数量或长度对应的参数数量来调用工厂。

  1. 参数 factory 是一个应该执行以实例化模块或对象的函数。如果工厂是一个函数,它应该只执行一次。如果工厂参数是一个对象,则应将该对象指定为模块的导出值。如果工厂函数返回一个值(对象、函数或任何强制为 true 的值),则应将该值指定为模块的导出值。

简化的 CommonJS 包装

如果省略依赖项参数,模块加载器可以选择以 require 语句的形式(字面意思是 require("module-id") 的形式)扫描工厂函数以查找依赖项。第一个参数必须按字面意思命名为 require 才能起作用。

在某些情况下,由于代码大小限制或函数缺乏 toString 支持(众所周知,Opera Mobile 缺乏对函数的 toString 支持),模块加载器可能会选择不扫描依赖项。

如果存在依赖项参数,则模块加载器不应扫描工厂函数内的依赖项。

define.amd 属性

为了清楚地表明全局定义函数(根据脚本 src 浏览器加载的需要)符合 ·,任何全局定义函数都应该有一个名为 amd 的属性,其值为一个对象。这有助于避免与可能定义不符合 AMD API 的 Define() 函数的任何其他现有 JavaScript 代码发生冲突。

此时,define.amd 对象内部的属性尚未指定。想要了解实现支持的基本 API 之外的其他功能的实现者可以使用它。

具有对象值的 define.amd 属性的存在表明与此 API 一致。如果有另一个版本的 API,它可能会定义另一个属性,例如 define.amd2,以指示符合该版本的 API 的实现。

AMD 提供了一些 CommonJS 互操作性。它允许在代码中使用类似的 exports 和 require() 接口,尽管它自己的 define() 接口更加基础和首选。AMD 规范由 Dojo Toolkit、RequireJS 和其他库实现。

Common Module Definition

通用模块规范(CMD)解决了如何编写模块以便在基于浏览器的环境中进行互操作。言下之意,该规范定义了模块系统必须提供的最少功能,以支持可互操作的模块。

  • 模块是单例的。
  • 不应在模块范围内引入新的自由变量。
  • 执行必须是惰性的。
js 复制代码
define(function(require, exports, module) {
  // The module code goes here
})

模块定义

模块是用 define 关键字定义的,它是一个函数:define(factory);。

  1. define 函数接受一个参数,即模块工厂。
  2. factory 可以是函数或其他有效值。
  3. 如果 factory 是一个函数,则该函数的前三个参数(如果指定)必须按顺序为 require 、 exports 和 module 。
  4. 如果 factory 不是函数,则模块的导出将设置为该对象。

模块上下文

在模块中,有三个自由变量: require 、 exports 和 module 。

  • require 函数
    • require 是一个函数
      • require 接受模块标识符。
      • require 返回外部模块导出的 API。
      • 如果请求的模块无法返回, require 应该返回 null 。
    • require.async 是一个函数
      • require.async 接受模块标识符列表和可选的回调函数。
      • 回调函数接收模块导出作为函数参数,其列出顺序与第一个参数中的顺序相同。
      • 如果无法返回请求的模块,则回调应相应接收 null 。
  • exports 对象
    • 在模块中,有一个名为 exports 的自由变量,它是模块在执行时可以添加其 API 的对象。
  • module 对象
    • module.uri:模块的完整解析 uri。
    • module.dependencies:模块所需的模块标识符列表。
    • module.exports:模块导出的 API。它与 exports 对象相同。

模块标识符

  1. 模块标识符是并且必须是文字字符串。
  2. 模块标识符可能没有像 .js 这样的文件扩展名。
  3. 模块标识符应该是短划线连接的字符串。
  4. 模块标识符可以是相对路径。

Universal Module Definition

通用模块规范 (UMD) 定义了 API 的设计和实现,这些模块能够在任何地方工作,无论是在客户端、服务器还是其他地方。UMD 模式通常尝试提供与当今最流行的脚本加载器的兼容性。在许多情况下,它使用 AMD 作为基础,并添加特殊外壳来处理 CommonJS 兼容性。

js 复制代码
;(function (global, factory) {
	if (typeof define === 'function' && define.amd) {
		define(['module'], function (module) {
			return (root.returnExportsGlobal = factory(module))
		})
	} else if (typeof exports === 'object' && module.exports) {
		module.exports = factory(require('module'))
	} else {
		root.returnExportsGlobal = factory(root.module)
	}
})(typeof self !== 'undefined' ? self : this, function (module) {
	// The module code goes here
})

ECMAScript 2015 Modules

ES2015 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CJS 、 AMD 等规范,成为浏览器和服务器通用的模块解决方案。ESM 的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

js 复制代码
// 导出模块
export function fn () {}
// 导出默认模块
export default {}
// 导入模块
import module, { fn } from 'module'
// 动态导入模块
import('module').then((module) => {
	// Do something with the module.
})

.mjs 与 .js

V8 引擎推荐 .mjs 的做法:

  • 比较清晰,这可以指出哪些文件是模块,哪些是常规的 JavaScript。
  • 这能保证你的模块可以被运行时环境和构建工具识别,比如 Node.js 和 Babel。

注意:

  • 一些工具不支持 .mjs,比如 TypeScript。
  • <script type="module"> 属性用于指示引入的模块。

CJS VS ESM

  • CJS 使用 require('module') 语法导入其他模块,使用 module.exports = stuff;ESM 使用 import { module } from 'module' 语法进行导入,使用 export stuff 语法进行导出
  • CJS 文件可以使用 .cjs 扩展名告诉 Node 它们位于 CJS 中;ESM 文件可以使用 .mjs 扩展名告诉 Node 它们位于 ESM 中
  • CJS 导入是同步的;ESM 导入是异步的(这也允许顶级 await )
  • CJS 在 Node 中工作,但在浏览器中不起作用;ESM 受所有现代浏览器和最新版本的 Node 支持,但在低于 12 的 Node 版本中根本不起作用
  • 大量核心 JavaScript 生态系统工具是在 Node 中开发的,而 Node 最近才开始支持 ESM,因此现有 Node 项目的很大一部分都是在 CJS 中
相关推荐
Csvn6 小时前
并发模式:让渲染学会排队、插队和让路
前端
可乐鸡翅yeah_6 小时前
新手梳理:M3U8 线上问题,哪些是前端锅,哪些是后端锅
前端·ios·音视频·实时音视频·m3u8·音视频在线播放
Flynt6 小时前
Linear 用 1000 个 PR 换掉 styled-components,我写了 200 个按钮,把这笔账复现了一遍
前端·css·preact
JavaGuide7 小时前
NVIDIA 又开源了!这次给 AI Agent 加上权限管控
前端·后端
excel7 小时前
prisma 如何处理数据库竞态
前端·数据库·后端
莪_幻尘8 小时前
Skill 体检:30 个 Skill 全凭感觉?体检器先自曝了 8 个“假 0 分
前端·人工智能·llm
风骏时光牛马8 小时前
AI模型综合能力评测:性能、指令遵循与多场景实测对比
前端
Frag0ut8 小时前
Chrome与Chromium内核浏览器在Windows 11上的新特性全景解析
前端·chrome·windows·web安全·chromium·gemini ai·playready drm
hiahiahia1239 小时前
实现完整 Tool Dispatcher
开发语言·前端
IT_陈寒9 小时前
Java线程池这破玩意,差点让我周末加班排查到凌晨
前端·人工智能·后端