ESM
上下文不一致
上述提到的任意一种工具,我们现在能够将 TypeScript 作为单一的真相之源来编写,并使整个代码库更易于维护。然而,仍然有一些需要注意的地方。
在 ECMAScript 模块(ESM)中,不存在 __dirname、__filename、require、require.resolve 等变量。相反,您需要使用 import.meta.url,并且还需要进行一些转换以获取文件路径字符串。
因此,由于我们的代码将被编译为 CommonJS(CJS)和 ESM,最好在可能的情况下避免使用那些特定于环境的上下文。如果确实需要它们,您可以参考我关于等效 __dirname 的说明:
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const _dirname = typeof __dirname !== 'undefined'
? __dirname
: dirname(fileURLToPath(import.meta.url))对于 require 和 require.resolve,您可以使用:
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)好消息是,如果您使用的是 unbuild,您可以打开 cjsBridge 标志,unbuild 将自动为您在 ESM 中模拟这些 CJS 上下文!
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
cjsBridge: true, // <--
})另一方面,如果您使用的是 tsup,它将为您在 CJS 中模拟 ESM 的 import.meta.url。
模块模式
大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API。
模块模式需要具备两个必要条件。
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
function Math() {
function add(a, b) {
return a + b
}
function sub(a, b) {
return a - b
}
return { add, sub }
}
const math = Math()
math.add(1, 2) // 3
math.sub(3, 2) // 1Math() 只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
一个具有函数属性的对象本身并不是真正的模块。一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
模块参数
模块也是普通的函数,因此可以接受参数:
单例模式
Math() 的独立的模块创建器,可以被调用任意多次, 每次调用都会创建一个新的模块实例。
当只需要一个实例时,我们将模块函数转换成了 IIFE 来实现单例模式
const math = (function Math() {
function add(a, b) {
return a + b
}
function sub(a, b) {
return a - b
}
return { add, sub }
})()现代模块
const ModernModule = (() => {
// 存储所有定义的模块
const modules = {}
/**
* 定义模块
* @param {string} name 模块名称
* @param {Array} deps 依赖模块名称
* @param {Function} impl 模块
*/
const define = (name, deps, impl) => {
const depModules = deps.map(depName => modules[depName])
modules[name] = impl.apply(impl, depModules)
}
const get = (name) => {
return modules[name]
}
return { define, get }
})()这段代码的核心是modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装 函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管 理的模块列表中。 定义模块
// 定义一个 math 模块
ModernModule.define('math', [], () => {
function add(a, b) {
return a + b
}
return { add }
})
ModernModule.define('index', ['math'], (math) => {
const result = math.add(1, 2)
})现有模块标准
CJS 是 CommonJS 的缩写。只适用于 node 端:
const _ = require('lodash')
module.exports = function doSomething(n) {}AMD 代表异步模块定义。在浏览器端有效:使用 requireJS 来编写模块化,特点:依赖必须提前声明好。
define(['dep1', 'dep2'], (dep1, dep2) => {
return function () {}
})CMD: 使用 seaJS 来编写模块化,特点:支持动态引入依赖文件。
define((require, exports, module) => {
const indexCode = require('./index.js')
})UMD 代表通用模块定义(Universal Module Definition)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD。注册为匿名模块
define(['moduleB'], factory)
}
else if (typeof module === 'object' && module.exports) {
// Node。不支持严格CommonJS
// 但可以在 Node 这样支持 module.exports 的
// 类 CommonJS 环境下使用
module.exports = factory(require(' moduleB '))
}
else {
// 浏览器全局上下文(root 是 window)
root.returnExports = factory(root.moduleB)
}
})(this, (moduleB) => {
// 以某种方式使用moduleB
// 将返回值作为模块的导出
// 这个例子返回了一个对象
// 但是模块也可以返回函数作为导出值 return {};
})兼容 AMD,CommonJS 模块化语法。
ES6
模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。 IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。
(function () {
return {
data: [],
}
})()浏览器端 ESM 执行流程
开启服务,托管资源(ES 源码)
加载入口文件,浏览器模块化解析

构建
遍历依赖树,先解析文件,然后找出依赖,最后又定位并加载这些依赖,如此往复。(下载所有的 js)

模块映射
当加载器要从一个 URL 加载文件时,它会把 URL 记录到模块映射中,并把它标记为正在下载的文件。然后它会发出这个文件请求并继续开始获取下一个文件。

解析模块
所有的模块都按照严格模式来解析的。不同文件类型按照不同的解析方式称。在浏览器中,通过 type="module" 属性告诉浏览器这个文件需要被解析为一个模块。不过在 Node 中,我们并不使用 HTML 标签,所以也没办法通过 type 属性来辨别。社区提出一种解决办法是使用 .mjs 拓展名。
运行
采用深度优先的后序遍历方式,顺着关系图到达最底端没有任何依赖的模块,然后设置它们的导出。模块映射会以 URL 为索引来缓存模块,以确保每个模块只有一个模块记录。这保证了每个模块只会运行一次。
Reference