CommonJS与ES模块:新手完全指南
0 人阅读 # CommonJS与ES模块:新手完全指南
如果你是JavaScript新手,一定会对代码中时而出现的require()
,时而出现的import
感到困惑。为什么JavaScript会有两套不同的模块导入方式?它们有什么区别?什么时候该用哪一个?本文将用最通俗易懂的方式为你解答这些问题。
# 🎯 什么是模块化?
在深入了解CommonJS和ES模块之前,我们先来理解什么是模块化。
想象一下,你在搭建乐高积木。每个积木块都有特定的功能,你可以把它们组合起来创造出复杂的作品。JavaScript的模块化就是这个道理——我们把代码分割成一个个独立的"积木块"(模块),每个模块负责特定的功能,然后通过导入导出的方式把它们组合起来。
# 模块化的好处
- 代码复用:写一次,到处使用
- 维护性:每个模块职责单一,容易维护
- 协作开发:团队成员可以并行开发不同模块
- 避免命名冲突:每个模块有自己的作用域
# 📚 历史背景:为什么会有两套规范?
# JavaScript的模块化之路
在很久很久以前(大约2009年之前),JavaScript是没有官方模块系统的。开发者只能通过全局变量或者立即执行函数表达式来组织代码(如果你写过jquery,一定印象深刻),但是这也带来了很多问题。
// 古老的方式:全局变量(容易冲突)
var myLibrary = {
add: function(a, b) {
return a + b;
}
};
// 或者使用IIFE(立即执行函数表达式)组织代码(写法复杂)
(function() {
var privateVar = 'secret';
window.myLibrary = {
add: function(a, b) {
return a + b;
}
};
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# CommonJS的诞生(2009年)
2009年,Node.js诞生了。Node.js需要在服务器端运行JavaScript,而服务器端需要一个模块系统来组织代码。于是,Node.js建立并采用了CommonJS规范。
CommonJS的设计理念:
- 专为服务器端设计
- 同步加载模块(因为服务器端文件都在本地)
- 简单直观的语法
# ES模块的出现(2015年)
2015年,ECMAScript 2015(ES6)正式引入了官方的模块系统——ES模块(ESM)。这是JavaScript语言层面的标准,不再依赖于特定的运行环境(即浏览器或Node.js中都可以执行)。
ES模块的设计理念:
- 语言层面的标准
- 支持静态分析(编译时就能确定依赖关系)
- 异步加载(适合浏览器环境)
- 更好的Tree Shaking支持
# 🔍 基础语法对比
让我们通过实际例子来看看两种模块系统的语法差异。
# CommonJS语法
// math.js - 导出模块
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 方式1:逐个导出
exports.add = add;
exports.subtract = subtract;
// 方式2:整体导出
module.exports = {
add,
subtract
};
// 方式3:导出单个函数
module.exports = add;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.js - 导入模块
const math = require('./math'); // 导入整个模块
const { add, subtract } = require('./math'); // 解构导入
const add = require('./math'); // 如果模块只导出一个函数
console.log(math.add(2, 3)); // 5
console.log(add(2, 3)); // 5
2
3
4
5
6
7
# ES模块语法
// math.js - 导出模块
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 方式1:命名导出
export { add, subtract };
// 方式2:直接导出
export function multiply(a, b) {
return a * b;
}
// 方式3:默认导出
export default function divide(a, b) {
return a / b;
}
// 方式4:混合导出
export { add, subtract };
export default divide;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// main.js - 导入模块
import { add, subtract } from './math.js'; // 命名导入
import divide from './math.js'; // 默认导入
import divide, { add, subtract } from './math.js'; // 混合导入
import * as math from './math.js'; // 导入所有
console.log(add(2, 3)); // 5
console.log(divide(10, 2)); // 5
console.log(math.add(2, 3)); // 5
2
3
4
5
6
7
8
9
# ⚡ 核心差异详解
CommonJS和ES模块不仅仅是语法上的差异,它们就像大黄蜂和蜜蜂一样,长的很像,但是原理却完全不同。下面,将简要介绍二者的差异:
# 1. 加载机制
CommonJS:同步加载
console.log('开始');
const math = require('./math'); // 这里会阻塞,直到模块加载完成
console.log('模块加载完成');
math.add(1, 2);
2
3
4
ES模块:异步加载
console.log('开始');
import { add } from './math.js'; // 这里不会阻塞
console.log('继续执行');
// 模块会在后台异步加载
2
3
4
# 2. 导出值的性质
CommonJS:导出的是值的拷贝
// counter.js
let count = 0;
function increment() {
count++;
}
module.exports = { count, increment };
// main.js
const { count, increment } = require('./counter');
console.log(count); // 0
increment();
console.log(count); // 还是0!因为count是拷贝的值
2
3
4
5
6
7
8
9
10
11
12
ES模块:导出的是值的引用
// counter.js
let count = 0;
export function increment() {
count++;
}
export { count };
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1!因为count是引用
2
3
4
5
6
7
8
9
10
11
12
# 3. 循环依赖处理
CommonJS:可能导致部分加载
// a.js
const b = require('./b');
console.log('a.js:', b.name);
module.exports = { name: 'module-a' };
// b.js
const a = require('./a'); // 这时a.js还没执行完
console.log('b.js:', a.name); // undefined
module.exports = { name: 'module-b' };
2
3
4
5
6
7
8
9
ES模块:更好的循环依赖处理
// a.js
import { name as bName } from './b.js';
console.log('a.js:', bName);
export const name = 'module-a';
// b.js
import { name as aName } from './a.js';
console.log('b.js:', aName); // 可以正确获取到值
export const name = 'module-b';
2
3
4
5
6
7
8
9
# 📁 文件扩展名:.js、.cjs、.mjs
这是很多新手困惑的地方。让我们来理清楚:
# .js文件
- 在Node.js中:默认被当作CommonJS模块
- 在浏览器中:需要通过
<script type="module">
来使用ES模块 - 在package.json中设置
"type": "module"
:.js文件会被当作ES模块
# .cjs文件
- 明确表示:这是一个CommonJS模块
- 无论package.json如何设置:始终使用CommonJS语法
# .mjs文件
- 明确表示:这是一个ES模块
- 无论package.json如何设置:始终使用ES模块语法
# 实际例子
// package.json
{
"type": "module"
}
2
3
4
// math.js - 现在被当作ES模块
export function add(a, b) {
return a + b;
}
// utils.cjs - 明确指定为CommonJS
function helper() {
return 'helper';
}
module.exports = { helper };
// main.mjs - 明确指定为ES模块
import { add } from './math.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { helper } = require('./utils.cjs');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 🌍 使用场景
# 什么时候使用CommonJS?
- Node.js服务器端项目
// 典型的Node.js应用
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
// ...
2
3
4
5
6
7
- 需要动态导入
// 根据条件动态加载模块
const moduleName = process.env.NODE_ENV === 'production' ? './prod' : './dev';
const config = require(moduleName);
2
3
- 与旧项目兼容
// 大量现有的npm包仍然使用CommonJS
const lodash = require('lodash');
const moment = require('moment');
2
3
# 什么时候使用ES模块?
- 现代前端项目
// React项目
import React from 'react';
import { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
export default App;
2
3
4
5
6
7
8
9
10
11
- 需要Tree Shaking
// 只导入需要的函数,减少打包体积
import { debounce } from 'lodash-es';
import { format } from 'date-fns';
2
3
- 现代Node.js项目
// package.json中设置"type": "module"
import express from 'express';
import { readFile } from 'fs/promises';
const app = express();
2
3
4
5
# 🔧 常见问题与解决方案
# 问题1:Cannot use import statement outside a module
错误示例:
// main.js
import { add } from './math.js'; // 报错!
2
解决方案:
// package.json
{
"type": "module"
}
2
3
4
或者使用.mjs扩展名:
// main.mjs
import { add } from './math.js'; // 正确!
2
# 问题2:require is not defined
错误示例:
// 在ES模块中使用require
const fs = require('fs'); // 报错!
2
解决方案:
// 方法1:使用ES模块语法
import fs from 'fs';
// 方法2:创建require函数
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const fs = require('fs');
2
3
4
5
6
7
# 问题3:混合使用CommonJS和ES模块
在ES模块中导入CommonJS:
// commonjs-module.cjs
module.exports = { name: 'CommonJS Module' };
// es-module.mjs
import cjsModule from './commonjs-module.cjs'; // 正确!
console.log(cjsModule.name);
2
3
4
5
6
在CommonJS中导入ES模块:
// es-module.mjs
export const name = 'ES Module';
// commonjs-module.cjs
// const esModule = require('./es-module.mjs'); // 错误!
// 正确的方式:使用动态import
(async () => {
const esModule = await import('./es-module.mjs');
console.log(esModule.name);
})();
2
3
4
5
6
7
8
9
10
11
# 问题4:__dirname和__filename在ES模块中不可用
解决方案:
// ES模块中获取当前文件路径
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname);
console.log(__filename);
2
3
4
5
6
7
8
9
# 🎯 总结与建议
# 核心要点回顾
- CommonJS:Node.js的传统模块系统,同步加载,适合服务器端
- ES模块:JavaScript的官方标准,异步加载,适合现代开发
- 文件扩展名:.cjs明确表示CommonJS,.mjs明确表示ES模块
- 互操作性:ES模块可以导入CommonJS,反之需要使用动态import
# 新手建议
- 新项目优先选择ES模块:这是未来的趋势
- 学会两种语法:现实中你会遇到两种模块系统
- 理解加载机制:这有助于理解性能和行为差异
- 掌握互操作:知道如何在两种系统间转换
# 选择指南
场景 | 推荐 | 原因 |
---|---|---|
新的前端项目 | ES模块 | 现代标准,工具链支持好 |
新的Node.js项目 | ES模块 | 面向未来,功能更强大 |
维护旧项目 | 保持现状 | 避免不必要的迁移成本 |
发布npm包 | 双格式 | 最大化兼容性 |
学习阶段 | 两者都学 | 理解差异,应对各种场景 |
记住,模块化的目标是让代码更好维护、更易复用。无论选择哪种方式,保持代码的清晰和一致性才是最重要的!
希望这篇文章能帮助你理解CommonJS和ES模块的区别。如果你还有疑问,欢迎在评论区讨论!