JavaScript模块化底层原理—CommonJS与ESM的加载机制差异|新宇宙博客Back to list模块化底层原理
Site Owner
Published on 2026-05-21
系统讲解CommonJS值拷贝与循环依赖处理、ESM实时绑定与静态分析、Tree Shaking条件限制及代码分割策略
模块化底层原理
JavaScript 模块化经历了从 IIFE → CommonJS → AMD → UMD → ESM 的演进。ESM(ES Modules)是语言规范级别的模块系统,具有静态分析能力和 Tree-Shaking 支持。本文深入解析模块加载、解析、链接和求值的底层流程,以及 CJS 与 ESM 的互操作性。
目录
- 模块化发展历史
- CommonJS 运行机制
- ESM 的三阶段加载
- 静态分析与 Tree-Shaking
- 循环依赖处理
- CJS 与 ESM 互操作
- 动态 import() 与代码分割
- 实战案例
- 深度追问
- 总结表格
1. 模块化发展历史
var Module = (function() {
var private = 'secret';
return { getSecret: function() { private; } };
})();
fs = ();
. = { : fs. };
([, ], () {
{ };
});
(() {
( define === && define.) ([], factory);
( === ) . = ();
root. = ();
})(, () { {}; });
{ readFile } ;
() { }
return
const
require
'fs'
module
exports
readFile
readFile
define
'dep1'
'dep2'
function
dep1, dep2
return
function
root, factory
if
typeof
'function'
amd
define
else
if
typeof
module
'object'
module
exports
factory
else
MyLib
factory
this
function
return
import
from
'fs/promises'
export
function
processFile
path
2. CommonJS 运行机制
(function(exports, require, module, __filename, __dirname) {
const dep = require('./dep');
exports.foo = 'bar';
});
function require(id) {
const filepath = resolveModule(id);
if (require.cache[filepath]) {
return require.cache[filepath].exports;
}
const module = { exports: {}, id: filepath, loaded: false };
require.cache[filepath] = module;
const code = fs.readFileSync(filepath, 'utf-8');
const wrapper = new Function('exports', 'require', 'module', '__filename', '__dirname', code);
wrapper(module.exports, require, module, filepath, path.dirname(filepath));
module.loaded = true;
return module.exports;
}
require.cache = {};
3. ESM 的三阶段加载
export let count = 0;
export function increment() { count++; }
import { count, increment } from './counter.mjs';
console.log(count);
increment();
console.log(count);
Live Binding vs Value Copy
let count = 0;
module.exports = { count, increment() { count++; } };
const { count, increment } = require('./counter.cjs');
console.log(count);
increment();
console.log(count);
4. 静态分析与 Tree-Shaking
import { used } from './utils.js';
const utils = require('./utils');
import * as all from './utils.js';
const fn = condition ? a : b;
5. 循环依赖处理
import { b } from './b.mjs';
export const a = 'A(' + b + ')';
import { a } from './a.mjs';
export const b = 'B(' + a + ')';
exports.loaded = false;
const b = require('./b.cjs');
exports.loaded = true;
6. CJS 与 ESM 互操作
import pkg from './cjs-module.cjs';
import { named } from './cjs-module.cjs';
const esm = await import('./esm-module.mjs');
7. 动态 import() 与代码分割
const locale = navigator.language;
const messages = await import(`./i18n/${locale}.js`);
const routes = {
'/home': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/dashboard': () => import('./pages/Dashboard.js'),
};
async function loadRoute(path) {
const loader = routes[path];
if (!loader) throw new Error('404');
const module = await loader();
return module.default;
}
function prefetchRoute(path) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = routes[path].toString().match(/import\('(.+)'\)/)?.[1];
document.head.appendChild(link);
}
8. 实战案例
实战案例 1:模块联邦(Module Federation 简化版)
async function loadRemoteModule(url, moduleName) {
const container = await import( url);
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(moduleName);
return factory();
}
const RemoteHeader = await loadRemoteModule(
'https://cdn.example.com/header/remoteEntry.js',
'./Header'
);
实战案例 2:自定义模块加载器
class ModuleLoader {
#modules = new Map();
#baseUrl;
constructor(baseUrl) {
this.#baseUrl = baseUrl;
}
async resolve(specifier, referrer) {
if (specifier.startsWith('./') || specifier.startsWith('../')) {
return new URL(specifier, referrer || this.#baseUrl).href;
}
return `https://esm.sh/${specifier}`;
}
async load(url) {
if (this.#modules.has(url)) return this.#modules.get(url);
const promise = import( url);
this.#modules.set(url, promise);
return promise;
}
}
实战案例 3:Import Maps 使用
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom": "https://esm.sh/react-dom@18",
"lodash/": "https://esm.sh/lodash-es/",
"#internal/": "./src/internal/"
}
}
</script>
<script type="module">
import React from 'react';
import { debounce } from 'lodash/debounce';
import { utils } from '#internal/utils.js';
</script>
9. 深度追问
Q1:为什么 ESM 不能用在同步条件加载中?
ESM 的设计目标是允许静态分析(在编译阶段确定依赖图),这要求 import 声明在文件顶层且不能在条件分支中。动态 import() 作为补充方案解决了运行时加载需求。
Q2:import.meta 包含什么?
import.meta.url — 当前模块的 URL(浏览器)或 file:// 路径(Node.js)
import.meta.resolve(spec) — 解析模块说明符为完整 URL(ES2024)
Q3:Top-level await 如何影响模块加载?
使用 top-level await 的模块在求值阶段会暂停,所有依赖它的模块也会等待。这可能导致瀑布式加载延迟,应谨慎使用。
10. 总结表格
| 特性 | CommonJS | ESM |
|---|
| 加载方式 | 同步(运行时) | 异步(编译时解析) |
| 绑定方式 | 值拷贝 | Live Binding |
| Tree-Shaking | ❌ | ✅ |
| 顶层 await | ❌ | ✅ |
| 动态导入 | require(expr) | import(expr) |
| 循环依赖 | 部分导出 | 未初始化绑定 |
| this 指向 | module.exports | undefined |
| 文件扩展名 | .js/.cjs | .js/.mjs |