主题
CommonJS 模块规范与ES6 模块规范(Day7 3/3)
本页对应 Day07_Node,共 6 段视频,约 1 小时 40 分。
学习目标
- 能编写 CommonJS 模块,分别通过给
module.exports赋值、给module.exports添加属性、给exports添加属性暴露单个或多个数据,并解释exports重新赋值后失效的原因。 - 能使用
require导入自定义模块与内置模块,说明自定义模块路径必须以./或../开头的规则,并判断省略扩展名时对.js、.json、目录的查找顺序。 - 能实现将整个目录作为 CommonJS 模块导入,正确配置
package.json的main入口或使用默认index.js,并说明缺少入口文件时的报错原因。 - 能使用 ES 模块语法导入 Node 内置模块,如
import { readFile } from 'node:fs'或import * as fs from 'node:fs',并说明.mjs与package.json中type: "module"的启用条件。 - 能编写 ES 自定义模块,使用
export default暴露单个数据、使用export声明或统一暴露多个数据,并按具名导入、别名导入或命名空间导入正确引入。 - 能区分 CommonJS 与 ES 模块在暴露/导入语法、扩展名解析和目录模块解析上的差异,并依据报错定位常见模块配置问题。
知识点
模块为什么要封闭
为什么需要:模块通常只对外提供功能,内部变量不应污染使用方;使用方只需要知道“能拿到什么数据”。
怎么想:CommonJS 中模块默认封闭,require 得到的是 module.exports 的值;不暴露时默认是空对象。
怎么做:
js
// 01-mod.js
console.log('我是一个模块');
let address = '上海';js
// index.js
require('./01-mod.js');
const mod01 = require('./01-mod.js');
console.log(mod01); // {}实际中运行后能看到:01-mod.js 的 console.log 只执行一次;mod01 是空对象。
易错点:模块里的 address 外部不能用;require 多次不会重复执行模块,后面拿的是缓存结果。
用 module.exports 暴露一个数据
为什么需要:让外部拿到模块的计算结果或对象。
怎么想:module.exports 的值就是模块暴露的数据,默认是空对象;给它赋值就是替换暴露数据。
怎么做:
js
// 02-mod.js
module.exports = 100;
// 后写会覆盖前写
// module.exports = 'hello';
// module.exports = [1, 2, 3];js
// index.js
const mod02 = require('./02-mod.js');
console.log(mod02); // 100也可以暴露对象:
js
const obj = {
name: '小段',
age: 19,
say() { console.log(this.name); }
};
module.exports = obj;易错点:多次给 module.exports 赋值,只有最后一次有效;赋值不会“结束模块”,后面的代码仍会执行,通常把暴露语句放最后。
给 module.exports 添加属性暴露多个数据
为什么需要:一个模块常要暴露多个方法或字段。
怎么想:默认的 module.exports 是空对象,不替换它,而是给它加属性,就能暴露多个数据;require 得到整体对象。
怎么做:
js
// 03-mod.js
module.exports.getMsg = () => {
console.log('MOD03 getMessage');
};
module.exports.setMsg = () => {
console.log('MOD03 setMessage');
};js
const mod03 = require('./03-mod.js');
mod03.getMsg();易错点:require 得到的是对象,暴露的多个数据是对象属性;可以用解构赋值取属性,但解构只是取值,不改变整体对象。
exports 别名与引用断开
为什么需要:少写 module.exports。
怎么想:exports 是 module.exports 的别名,初始指向同一对象;给 exports 加属性等于给 module.exports 加属性;但给 exports 重新赋值会断开引用。
怎么做:
js
// 04-mod.js
exports.username = '小段';
exports.password = '123123';等价于给 module.exports 加属性。若要整体替换,应写:
js
module.exports = [1, 2, 3];易错点:exports = [...] 后,module.exports 仍是原来的空对象,require 得到空对象。根因是对象引用:exports 重新赋值后不再指向 module.exports。
require 引入规则与扩展名
为什么需要:区分内置模块和自定义模块,并决定查找哪个文件。
怎么想:不以 ./ 或 ../ 开头,会被当作内置模块;自定义模块要写路径。省略扩展名时,依次查找 .js、.json、.node、目录。
怎么做:
js
const mod02 = require('./02-mod'); // 可省略 .jsJSON 文件也能作为模块,require 会自动解析成对象:
js
const mod05 = require('./05-mod.json');其他扩展名如 .sb 也能导入,但扩展名不能省略;转写稿中它按 JS 处理,写 module.exports = [1, 2, 3] 后能拿到数组。
易错点:同目录也要写 ./;省略扩展名不等于任意扩展名都能省;同名时 .js 优先于 .json。
目录作为模块
为什么需要:把多个文件组织成一个模块,对外只暴露入口文件。
怎么想:require 目录时,先找 package.json 的 main 字段;没有 main 就找 index.js;都没有就报错。
怎么做:
json
// 07-mod/package.json
{ "main": "01.js" }js
// 07-mod/01.js
module.exports = { msg: 'MOD 07' };js
const mod07 = require('./modules/07-mod');
console.log(mod07); // { msg: 'MOD 07' }没有 package.json 时,目录中写 index.js 作为默认入口。
易错点:空目录会报错;入口不是目录里任意文件;入口文件内部可以再 require 其他文件,但对外只认 main 或 index.js。
小结
CommonJS 暴露数据可归到底层一条:module.exports 的值就是模块暴露的数据,默认空对象;给它赋值就替换,给它加属性就暴露多个数据;exports 只是别名,不能重新赋值。require 多次只执行一次模块,后续使用缓存。
先理解模块要解决什么问题
模块的第一目标是“封装”:模块内部变量默认不对外暴露,使用模块的人只关心它给了什么。CommonJS 用 require 导入、用 module.exports / exports 暴露;ES6 模块则用 import / export 两个关键字。为什么要先对照?因为转写稿里大量调试都围绕“暴露什么、导入得到什么”展开。怎么想:先问模块暴露的是单个数据还是多个数据,再决定导入写法。怎么做:CommonJS 中 module.exports 默认是空对象,exports 是它的别名;给 module.exports 赋值会改变暴露数据,给 exports 重新赋值则会断开引用,无法暴露。易错点:require('./01-mod.js') 必须用 ./ 开头,否则会被当作内置模块;require 多次只执行一次,后续拿缓存。
js
// CommonJS 暴露
module.exports = { name: '小段', say() {} };
// 或
exports.name = '小段'; // 本质是给 module.exports 加属性ES6 导入内置模块:必须配合解构或整体命名
为什么需要:ES6 的 import 不是函数调用,而是语法关键字,不能像 require('fs') 那样直接得到整个对象。怎么想:内置模块暴露的是多个命名数据,所以要么按名字解构,要么用 * as 整体拿到。怎么做:
js
import { readFile, writeFile } from 'node:fs';
// 或整体导入
import * as FS from 'node:fs';
FS.writeFile('a.txt', 'hello', () => {});易错点:脚本中出现 import / export 后,Node 要求它是 ES 模块:文件用 .mjs,或在项目 package.json 中写 "type": "module"。一旦指定 type: module,同一范围里就不能再用 require,转写稿原话是“有我没他”。另外,本节演示中 ES6 直接导入 JSON 失败,所以不要照搬 CommonJS 的 JSON 导入习惯。
自定义模块:默认导出只能一个
为什么需要:很多模块只提供一个主功能,用默认导出最直接。怎么想:export default 在语法层面只允许写一次,因此它天然只暴露一个数据。怎么做:
js
// 01-mod.js
export default 100;
// 导入时可任意命名
import MOD01 from './modules/01-mod.js';易错点:不能写两个 export default;ES6 导入自定义模块时,转写稿实测扩展名不能像 CommonJS 那样随便省略,老老实实写 .js 更稳。
命名导出:多个数据必须“有名字”
为什么需要:一个模块可能暴露数组、函数、对象等多个数据。怎么想:export 后面必须跟变量声明语句,让每个数据有名字。怎么做:
js
// 02-mod.js
export const data = [1, 2, 3];
export const getUser = () => ({ name: '小段' });
// 统一暴露也可以
const n1 = 1, n2 = 2;
export { n1, n2 };导入多个命名数据时,名字必须与导出名对应;不喜欢可用 as 改名:
js
import { data, getUser as getU } from './modules/02-mod.js';
// 或整体导入为一个对象
import * as MOD02 from './modules/02-mod.js';易错点:import { data: d } 这种“对象解构式改名”不合法,必须写 data as d;export { n1, n2 } 不是暴露一个对象,而是暴露多个命名数据。
目录作为模块:入口要明确
CommonJS 中目录可依靠 package.json 的 main 或默认 index.js 作为入口。ES6 下转写稿做了调试:先写目录导入,报找不到模块;补 package.json 的 main,再补 "type": "module",仍然报错,最后还在排查,未给出成功结论。因此这里不要死记“ES6 目录一定能自动找 index”。更稳的做法是显式导入入口文件:
js
import MOD04 from './modules/04-mod/index.js';易错点:不要把 CommonJS 的目录查找规则直接套到 ES6;遇到目录导入失败,先检查入口文件、扩展名和 type 配置,再退回显式文件路径。
一句话总结
ES6 模块的核心不是背四种暴露规则,而是记住:export default 只给一个数据;命名导出给多个有名字的数据;import 按名字取,整体取用 * as;运行环境要明确是 ES 模块。调试时先看“模块到底暴露了什么”,再看“导入语法是否匹配”。
作业原文与目标
text
1. 封装容量单位转换函数,字节转为其他单位, 第二个参数指定 目标单位 0:不转换, 1:转为KB; 2:转为MB 3:转为GB 4:转为TB
2. 将定义好的函数封装成模块,分别封装成 CommonJS 模块和 ES6 模块这份作业表面是“写一个转换函数”,实际练的是两件事:先把纯函数写对,再把它放进 CommonJS 和 ES6 两种模块体系里,理解“模块内数据默认封闭、通过暴露规则对外提供数据、引入方按规则拿到数据”。转写稿里反复强调的 module.exports、exports 别名、require 路径、export default、export 多个数据、.mjs 与 package.json 的 type,正好对应这份作业的封装步骤。
一、容量单位转换函数:先写对再封装
为什么需要:字节数通常很大,直接展示不直观;统一用 1024 进制换算成 KB/MB/GB/TB,便于阅读和复用。
怎么想:第二个参数 unit 取 0~4,0 表示不转换,1~4 分别除以 1024 的 1~4 次方。先做参数校验,再计算,避免非法单位或负数进入业务。
怎么做:
js
function formatBytes(bytes, unit = 0) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
if (!Number.isFinite(bytes) || bytes < 0) {
throw new TypeError('bytes 必须是非负数字');
}
if (!Number.isInteger(unit) || unit < 0 || unit > 4) {
throw new RangeError('unit 必须是 0~4');
}
if (unit === 0) return bytes;
return bytes / Math.pow(1024, unit);
}
console.log(formatBytes(2048, 1)); // 2
console.log(formatBytes(1024 * 1024, 2)); // 1易错点:不要用 1000 代替 1024;unit 要限制在 0~4;返回值是数值还是带单位字符串要提前约定,作业只要求“转为其他单位”,返回数值最直接;负数、NaN 要拦截。
二、CommonJS 模块:module.exports 暴露,require 引入
为什么需要:函数写好后要被其他文件复用。转写稿说得很清楚:模块是封闭的,模块里的变量默认不能用,必须通过暴露规则对外提供数据。
怎么想:CommonJS 中 module.exports 的值就是模块暴露的数据;exports 是 module.exports 的别名,可以给 exports 加属性,但不能直接 exports = {...},否则引用断开。require('./xxx') 必须以 ./ 或 ../ 开头,否则会被当成内置模块;require 多次不会重复执行模块,后续拿的是缓存结果。
怎么做:
js
// commonjs/formatBytes.js
function formatBytes(bytes, unit = 0) {
if (unit === 0) return bytes;
return bytes / Math.pow(1024, unit);
}
module.exports = { formatBytes };js
// commonjs/index.js
const { formatBytes } = require('./formatBytes');
console.log(formatBytes(2048, 1)); // 2易错点:写成 exports = { formatBytes } 会拿到空对象;路径漏了 ./ 会报“找不到模块”;解构名字必须和暴露的属性名一致;如果 require 多次,模块代码只执行第一次,这是缓存机制,不是重复导入。
三、ES6 模块:export 暴露,import 引入
为什么需要:ES6 模块是标准语法,适合现代前端和 Node 的 ESM 体系。转写稿中强调:ES6 用 export 关键字暴露数据,用 import 导入;单个数据可用 export default,多个数据用 export 命名导出。
怎么想:export default 只能写一次;export 多个数据时,后面要跟变量声明,如 export const formatBytes = ...。导入单个默认导出可自己起名;导入多个命名导出必须按名字解构,或用 import * as mod 一步拿到对象。ES6 自定义模块导入时扩展名要写全;.js 里直接用 import 需要 package.json 的 "type": "module",或把文件改成 .mjs。
怎么做:
js
// esm/formatBytes.mjs
export function formatBytes(bytes, unit = 0) {
if (unit === 0) return bytes;
return bytes / Math.pow(1024, unit);
}js
// esm/index.mjs
import { formatBytes } from './formatBytes.mjs';
console.log(formatBytes(1024 * 1024, 2)); // 1易错点:export default 写两次会语法错误;export 多个数据不能写成对象字面量,要写变量声明;ES6 导入自定义模块不要随意省略扩展名;ES6 不能直接导入 JSON;若用目录作为模块,需要 package.json 的 main 指定入口,或默认 index.js,同时注意 type 字段与模块规范匹配。
四、联调与验收
先分别运行 node commonjs/index.js 和 node esm/index.mjs,检查 0~4 五个单位的结果。CommonJS 若拿到空对象,优先检查是否只写了 exports = ...;若报找不到模块,检查路径是否以 ./ 开头。ES6 若报 Cannot use import statement outside a module,检查文件是否为 .mjs 或 package.json 是否设置了 "type": "module"。转写稿中移动文件后路径要同步修改、require 多次走缓存、exports 重新赋值会断开引用,这些都是联调时最值得先排查的点。
课堂画面






代码示例
js
// 文件:capacity.js
const BASE = 1024;
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
function bytesToUnit(bytes, targetUnit) {
if (!Number.isFinite(bytes)) {
throw new TypeError('bytes 必须是数字');
}
if (!Number.isInteger(targetUnit) || targetUnit < 0 || targetUnit > 4) {
throw new RangeError('targetUnit 必须是 0~4');
}
return bytes / Math.pow(BASE, targetUnit);
}
module.exports = {
bytesToUnit,
UNITS
};js
// 文件:index.js
const { bytesToUnit, UNITS } = require('./capacity');
console.log(bytesToUnit(1024, 1), UNITS[1]); // 1 'KB'
console.log(bytesToUnit(1024 * 1024, 2), UNITS[2]); // 1 'MB'
console.log(bytesToUnit(1024, 0), UNITS[0]); // 1024 'B'要点:CommonJS 自定义模块用 require('./capacity') 引入,必须以 ./ 或 ../ 开头,否则会被当作内置模块查找;module.exports 默认是空对象,这里暴露成对象后可用解构接收;扩展名 .js 可省略;require 多次不会重复执行模块代码,后面拿的是缓存结果。易错点:不要写 exports = { bytesToUnit },这样会断开 exports 与 module.exports 的引用;应写 module.exports = { ... } 或 exports.bytesToUnit = ...。
js
// 文件:capacity.mjs
const BASE = 1024;
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
export function bytesToUnit(bytes, targetUnit) {
if (!Number.isFinite(bytes)) {
throw new TypeError('bytes 必须是数字');
}
if (!Number.isInteger(targetUnit) || targetUnit < 0 || targetUnit > 4) {
throw new RangeError('targetUnit 必须是 0~4');
}
return bytes / Math.pow(BASE, targetUnit);
}
export { UNITS };js
// 文件:index.mjs
import * as capacity from './capacity.mjs';
console.log(capacity.bytesToUnit(1024, 1), capacity.UNITS[1]); // 1 'KB'
console.log(capacity.bytesToUnit(1024 * 1024, 2), capacity.UNITS[2]); // 1 'MB'要点:ES6 模块用 export 暴露、import 导入;import * as capacity 会把模块暴露的数据整体拿到一个对象里,类似 CommonJS 的整体导出;ES6 导入模块地址也要以 ./ 开头;转写稿中明确 ES6 导入时扩展名不能省略,所以这里写 ./capacity.mjs。易错点:如果文件是 .js 且使用 import/export,需要 .mjs 扩展名或在 package.json 中设置 "type": "module";import { bytesToUnit: fn } 这种“解构式改名”不行,要写 import { bytesToUnit as fn };export default 只能写一次。
js
// 文件:capacity-module/package.json
{
"name": "capacity-module",
"main": "./index.js"
}js
// 文件:capacity-module/index.js
const BASE = 1024;
function bytesToUnit(bytes, targetUnit) {
if (!Number.isInteger(targetUnit) || targetUnit < 0 || targetUnit > 4) {
throw new RangeError('targetUnit 必须是 0~4');
}
return bytes / Math.pow(BASE, targetUnit);
}
module.exports = { bytesToUnit };js
// 文件:app.js
const { bytesToUnit } = require('./capacity-module');
console.log(bytesToUnit(1024 * 1024, 2)); // 1要点:整个目录也可以作为模块,require('./capacity-module') 会加载该目录下 package.json 中 main 指定的入口文件;如果没有 main,默认找目录下的 index.js;如果既没有入口配置也没有 index.js,会报找不到模块。易错点:require('./capacity-module') 仍然必须以 ./ 开头;main 路径要写对;转写稿中的查找顺序是:先找 .js,再找 .json,最后才找目录。
常见错误
现象:
require('01-mod')报找不到模块。
原因:自定义模块未以./或../开头,被当作内置模块查找。
修法:改为require('./01-mod')或正确相对路径。现象:
require('./mod')得到{},调用不到模块内变量/方法。
原因:模块内没有暴露数据,CommonJS 默认暴露空对象。
修法:使用module.exports = ...,或给module.exports/exports添加属性。现象:模块里写了
exports = fn,入口仍拿到空对象。
原因:exports只是module.exports的初始别名,重新赋值会断开引用。
修法:改为module.exports = fn;若用exports,只添加属性,如exports.fn = fn。现象:多次
require('./mod'),以为模块代码会执行多次。
原因:CommonJS 模块首次加载后走缓存。
修法:把需要重复执行逻辑放到导出函数中,由调用方显式调用。现象:
.js文件中直接写import/export运行报错。
原因:Node 默认按 CommonJS 解析.js,未启用 ES 模块。
修法:改用.mjs,或在package.json中设置"type": "module"。现象:ES6 模块中写了多个
export default报语法错误。
原因:export default只能出现一次。
修法:单个默认导出用export default;多个数据改用export const/function等具名导出。现象:ES6 具名导入时随便改变量名,运行报错。
原因:具名导入必须匹配模块导出的名字。
修法:按导出名导入;需要改名时用import { data as d } from './mod.js'。现象:ES6 导入自定义模块省略
.js扩展名,报找不到模块。
原因:ES 模块自定义路径通常要求完整扩展名。
修法:写完整路径,如import { fn } from './modules/01-mod.js'。现象:
require('./dir')或导入目录模块报错。
原因:目录下既无package.json的main入口,也无默认index.js。
修法:补package.json的main,或创建index.js作为入口。
练习
容量单位转换函数:实现
formatBytes(bytes, targetUnit),targetUnit为0/1/2/3/4时分别返回字节、KB、MB、GB、TB;处理非法参数与小数精度,并写 3 组测试用例。CommonJS 封装:把第 1 题封装为
format-bytes.cjs,分别用“单个函数导出”和“对象属性导出”两种方式实现,并在入口文件中require后调用验证。ES6 封装:把第 1 题封装为
format-bytes.js,使用export default导出;另建units.js,用具名导出单位表与转换函数,在入口中分别用默认导入、具名导入和import * as导入验证。目录模块练习:创建
utils/目录,内含index.js和package.json,配置入口后分别用 CommonJS 和 ES6 方式导入该目录模块,观察并记录差异。排错练习:给出三段错误代码:
exports = fn、.js中直接import未配置type、ES6 导入省略.js。分别说明报错现象、原因和最小修改方案。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | CommonJS模块规范语法-模块暴露数据规则01 | 11:18 |
| 2 | CommonJS模块规范语法-模块暴露数据规则02 | 29:12 |
| 3 | CommonJS模块规范语法-require引入模块相关规则 | 08:26 |
| 4 | CommonJS模块规范语法-整个目录作为模块 | 09:55 |
| 5 | ES模块规范-导入内置模块 | 12:28 |
| 6 | ES模块规范-自定义模块 | 29:01 |
小结
- CommonJS 自定义模块必须写相对路径,
.//../是区分内置模块的关键。 - CommonJS 未暴露数据时入口得到空对象;模块代码首次加载后走缓存。
module.exports才是最终暴露值,exports只能添加属性,不能整体重新赋值。- ES6 模块使用
import/export关键字,运行环境需.mjs或"type": "module"。 export default只能一次;具名导出可多个,导入名必须匹配,必要时用as改名。- CommonJS 对扩展名、JSON、目录入口更宽松;ES6 自定义模块路径更严格,目录模块也需明确入口配置。
- 封装作业时先确定模块对外 API,再选择 CommonJS 或 ES6 规范,入口文件只消费暴露接口。