主题
模块的路径问题与作业讲解(Day8 1/4)
本页对应 Day08_Node,共 6 段视频,约 1 小时 37 分。
学习目标
- 能分别使用 CommonJS 的
module.exports/exports与require、ESM 的export/import完成模块暴露与导入,并通过.mjs或package.json的type配置运行验证。 - 能说明并验证模块路径相对于当前脚本文件所在目录解析,而文件读写相对路径相对于命令行当前工作目录解析,并能据此修正路径错误。
- 能使用
npm init/npm init -y初始化项目,识别并解释package.json中包名、版本、入口文件、依赖等字段的作用。 - 能使用
npm install、npm i、-D、-g安装包,并区分本地产品依赖、开发依赖与全局命令行工具的使用场景。 - 能通过
require('包名')使用第三方包,并说明 Node 查找第三方模块时依次检查内置模块、当前及上级node_modules、全局目录的规则。 - 能完成全局命令行工具的安装与排错,包括配置环境变量
Path、修改 Windows PowerShell 执行策略,并验证nodemon可运行及文件变更后自动重启。
知识点
CommonJS:先想清楚“暴露什么、怎么引入”
为什么需要:Node 里一个文件就是一个模块,模块之间不能靠全局变量串来串去,必须有明确的暴露与引入规则。
怎么想:只问两件事——模块暴露什么?别的文件怎么拿到?
怎么做:module.exports 的值就是模块暴露的数据,默认是空对象;exports 与 module.exports 指向同一个对象,所以可以给 exports 加属性。require() 的返回值就是对方模块的 module.exports。
js
// add.js
module.exports = function (a, b) {
return a + b;
};
// use.js
const add = require('./add');
console.log(add(100, 200));易错点:直接给 exports 重新赋值会断开与 module.exports 的引用,之后再用 exports 暴露数据就无效;自定义模块必须写 ./ 或 ../,否则 Node 会按内置模块或第三方包去找。
模块路径与文件路径不是一回事
为什么需要:同样写相对路径,require() 和 fs.readFile() 的参照物不同,换目录运行就会一个正常、一个报错。
怎么想:require('./add') 的相对路径参照“当前 JS 文件所在目录”;fs.readFile('./data.txt') 的相对路径参照“命令行当前所在目录”。
怎么做:用 cd .. 换到上级目录再运行脚本观察。模块路径省略扩展名时,会按 .js、.json、目录 index.js 的顺序找;目录入口也可用 package.json 的 main 改。
js
const fs = require('fs');
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) return console.log(err.message);
console.log(data);
});易错点:不要以为模块路径也跟命令行目录走;require('./add') 的 ./ 不能省;扩展名、目录入口的查找顺序要心里有数。
ESM:单值用 default,多值用命名导出
为什么需要:Node 文档示例越来越多使用 ESM,看到 import/export 不能陌生。
怎么想:暴露单个数据用 export default;暴露多个数据用 export const/function;导入时对应使用默认导入、命名导入或 import * as。
怎么做:ESM 文件可用 .mjs,或在 package.json 配 "type": "module"。
js
// convertByte.mjs
export default function convertByte(bytes, type = 0) {
return bytes / 1024 ** type;
}
// use.mjs
import convertByte from './convertByte.mjs';易错点:命名导入必须和暴露的名字一致;开启 type: module 后,同一个项目里不能随手再用 require,两种规范互斥;内置模块可写 import * as fs from 'node:fs'。
npm init:项目本身也是一个包
为什么需要:项目要用第三方包,项目自己也要有 package.json 记录包名、入口、依赖等信息。
怎么做:npm init 逐项填写,npm init -y 全部用默认值快速生成。包名默认取目录名,要求小写、不能用中文、不能和内置模块重名。
易错点:package.json 里的 type: module 会影响整个项目模块规范;package-lock.json 用来锁定实际安装版本,不要随手删。
本地依赖、开发依赖、全局安装
为什么需要:能复用别人写好的轮子,就不要重复造。
怎么想:代码里 require 用到的装本地;只在开发工具链用到的装 -D;命令行工具装 -g。
bash
npm -v
npm i lodash
npm i lodash -D
npm i nodemon -g怎么做:本地安装后,第三方包可直接 require('lodash'),不用写 ./;Node 查找顺序是内置模块 → 当前目录 node_modules → 上级目录 node_modules → 全局目录。
js
const _ = require('lodash');
console.log(_.chunk([1, 2, 3, 4, 5], 2));易错点:全局包不能直接在代码里 require;^ 表示锁大版本,package-lock.json 才锁实际版本;nodemon 这类全局命令要配好 Path,Windows 下还可能遇到 PowerShell 执行策略限制。
调试片段:异步回调为什么后输出
读文件是异步的,readFile 的回调要等主线程空闲才执行;后面的加法是同步代码,所以会先输出。易错点:不要按代码书写顺序判断执行顺序,异步结果要放在回调里处理。
先看本节回顾清单
下面保留本节笔记的回顾结构,并把明显同音字/拼写纠正为 CommonJS、ESM、module.exports、exports、require()、package.json、export default 等。
text
1. CommonJS
1.1 模块中暴露数据
module.exports
exports
1.2 如何导入模块(引用模块)
require() 返回值就是模块暴露的数据
1.3 模块文件的扩展名
.js .json .node 其他 目录
模块路径省略扩展名
2. ESM
2.1 暴露数据
① 暴露单个数据
export default 数据;
② 暴露多个数据
export 变量声明语句
或者
export {变量名,变量名,变量名}
2.2 导入模块
① 如果模块暴露单个数据
import 变量名 from '模块名'
② 如果模块暴露多个数据
import {变量名,变量名} from '模块名'
import * as 变量名 from '模块名'
2.3 开启 ESM 模块规则
方式一:脚本扩展名改为 .mjs
方式二:package.json 配置 {"type":"module"}
3. 模块的路径 文件的路径
文件的路径,使用相对路径,参照命令行所在的目录
模块的路径,使用相对路径,参照所在文件的目录,跟命令行所在目录无关!CommonJS:暴露与导入
为什么需要:Node 里一个文件就是一个模块,模块之间不能直接看到彼此的变量和函数,必须通过“暴露数据”和“导入模块”建立联系。
怎么想:CommonJS 的核心只有两个:module.exports 决定“我暴露什么”,require() 决定“我拿到什么”。require() 的返回值就是模块暴露的数据。
怎么做:
js
// add.js
module.exports = function add(a, b) {
return a + b;
};js
// main.js
const add = require('./add');
console.log(add(1, 2)); // 3也可以用 exports 添加属性:
js
// user.js
exports.name = 'Tom';
exports.age = 18;js
const user = require('./user');
console.log(user.name);易错点:module.exports 默认是一个空对象,所以不写暴露语句时,require() 得到的是 {}。exports 与 module.exports 一开始指向同一个对象,因此 exports.name = ... 可以;但如果直接写 exports = { ... },就会断开与 module.exports 的引用关系,模块最终暴露的仍可能是空对象。自定义模块导入时,路径必须以 ./ 或 ../ 开头,即使同目录也不能省略 ./。
CommonJS 的扩展名与省略规则
为什么需要:真实项目里模块文件不一定都叫 .js,导入时也常省略扩展名,需要知道 Node 按什么顺序找。
怎么想:CommonJS 支持 .js、.json、.node,也支持目录;省略扩展名时 Node 会自动补全并查找。
怎么做:
js
const data = require('./data'); // 可能找 data.js、data.json、data.node、data 目录
const config = require('./config'); // config.json 会被解析成对象目录默认找 index.js,也可以通过 package.json 配置入口文件。
易错点:如果同名存在 mod.js、mod.json、mod 目录,省略扩展名时会有查找优先级,容易加载到不是你以为的那个文件。需要精确时写全路径或扩展名。.node 是 C++ 扩展,初学阶段基本不用自己写。
ESM:两种暴露与两种导入
为什么需要:ESM 是 JavaScript 原生模块规范,Node 文档和现代第三方包越来越多使用 import/export。
怎么想:先看模块暴露的是“单个数据”还是“多个数据”,再决定导入写法。
怎么做:
js
// 暴露单个数据
export default function add(a, b) {
return a + b;
}js
import add from './add.mjs';js
// 暴露多个数据
export const name = 'Tom';
export const age = 18;
// 或者:export { name, age };js
import { name, age } from './user.mjs';
import * as user from './user.mjs';易错点:export default 一个模块只能有一个;import { name } 中的名字必须与暴露的名字一致;import * as user 拿到的是包含所有命名导出的模块命名空间对象。ESM 中自定义模块路径同样要以 ./ 或 ../ 开头。
开启 ESM 的两种方式
为什么需要:Node 默认按 CommonJS 处理 .js 文件,直接写 import/export 会报错。
怎么想:要么改文件扩展名,要么在项目配置里声明模块类型。
怎么做:
text
方式一:把文件扩展名改为 .mjs
方式二:package.json 中配置
{
"type": "module"
}易错点:配置 "type": "module" 后,该范围内的 .js 就不能再用 require 和 module.exports,两种规范互斥。转写稿里演示过:ESM 文件里导入内置模块可写成 import * as fs from 'node:fs',而 CommonJS 里是 const fs = require('fs')。
模块路径与文件路径的区别
为什么需要:读文件也要路径,导入模块也要路径,但两者相对路径的参照物不同,混用会在切换命令行目录时出错。
怎么想:文件路径的相对路径参照“命令行所在目录”;模块路径的相对路径参照“当前代码文件所在目录”。
怎么做:
js
const fs = require('fs');
const add = require('./add');
fs.readFile('./data.txt', (err, data) => {
if (err) {
console.log(err.message);
return;
}
console.log(data.toString());
});
console.log(add(100, 200));转写稿中的调试现象是:在脚本所在目录运行正常;切换到上一级目录再运行同一个脚本,模块 require('./add') 仍能加载,因为它是相对脚本文件找的;但 fs.readFile('./data.txt') 报“找不到文件或目录”,因为文件路径相对命令行目录。
易错点:不要以为模块路径和文件路径的“相对”是一回事。模块路径相对稳定,文件路径会随执行目录变化;需要稳定读文件时,通常要基于 __dirname 等构造路径,而不是直接依赖命令行当前目录。
课堂画面






代码示例
示例一:CommonJS 暴露/引入 + 模块路径与文件路径的区别
js
// 文件:add.js(自定义模块)
function add(a, b) {
return a + b;
}
// 暴露数据:module.exports 的值就是模块对外暴露的数据,默认是 {}
module.exports = add;js
// 文件:01-模块路径.js
// 内置模块,直接写名字,不需要 ./ 或 ../
const fs = require('fs');
// 自定义模块,必须 ./ 开头,参照的是【当前文件所在目录】
const add = require('./add.js'); // 扩展名可省略
// 文件路径使用相对路径,参照的是【命令行所在目录】
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) {
console.log('文件读取失败');
console.log(err.message);
return; // 出错就不再往下执行
}
console.log('文件内容:', data);
});
console.log('两数之和:', add(100, 200));要点解释:
module.exports = add直接把函数本身暴露出去;若只写exports.xxx = ...是给默认对象加属性,两者指向同一个对象引用,但给exports重新赋值就会和module.exports断开关联,数据就暴露不出去。require('./add.js')的返回值就是模块暴露的数据;./不能省,省了会被当成内置模块或第三方包去找。- 省略扩展名时按
add.js→add.json→add.node→add/目录(默认找index.js)的顺序查找。 - 易错点:把命令行切到上一级目录再运行(如
node 01-作业回顾/01-模块路径.js),require('./add.js')依然正常(参照文件目录),但fs.readFile('./data.txt')会报“找不到文件和目录”(参照命令行目录)——这是两条完全不同的解析规则。 - 易错点:
add(100, 200)的输出会先于文件内容打印,因为readFile是异步回调,主线程先执行完同步代码。
示例二:ESM 暴露/引入(.mjs)
js
// 文件:convertByte.mjs
// 计算机容量单位转换:type 0 不转换,1→KB,2→MB,3→GB,4→TB
const convertByte = (bytes, type = 0) => bytes / Math.pow(1024, type);
// 暴露单个数据
export default convertByte;js
// 文件:03-使用ESM模块.mjs
// ESM 中不能再用 require,内置模块用 import 引入
import * as fs from 'node:fs';
import convertByte from './convertByte.mjs';
const fileName = './课程视频.wmv';
fs.stat(fileName, (err, stat) => {
if (err) {
console.log('文件读取失败');
console.log(err.message);
return;
}
console.log('字节数(B):', stat.size);
console.log('KB:', convertByte(stat.size, 1));
console.log('MB:', convertByte(stat.size, 2));
console.log('GB:', convertByte(stat.size, 3));
});要点解释:
- 模块暴露多个数据用
export 变量声明语句或export { a, b }(大括号里只能写变量名,不是对象字面量);暴露单个数据用export default 数据。 - 对应导入:单个数据
import 变量名 from '模块名';多个数据import { a, b } from '模块名'(名字必须和暴露的一致)或import * as 变量名 from '模块名'。 - 开启 ESM 的两种方式:扩展名改成
.mjs,或在package.json中配置{"type":"module"};配了type: module后该目录下.js就不能再用require,两种规范互斥。 - 易错点:
import ... from './convertByte.mjs'的相对路径同样参照当前文件目录,必须./开头;写错文件名会直接报模块不存在。 - 易错点:
Math.pow(1024, 0)结果为 1,所以type = 0(不转换)不会出现除以 0。 fs.stat用来取文件信息,stat.size单位是字节,配合转换函数才能显示成 MB/GB。
示例三:npm 初始化 + 本地安装并使用第三方包
json
// 文件:package.json(由 npm init -y 生成,节选)
{
"name": "project01",
"version": "1.0.0",
"main": "index.js",
"scripts": {},
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21"
}
}js
// 文件:index.js
// 第三方包直接写包名,不需要 ./ 或 ../
const _ = require('lodash');
const arr = [1, 2, 3, 4, 5, 6, 7, 8];
// chunk 把数组按指定长度分组
console.log(_.chunk(arr, 3));要点解释:
- 使用第三方包前先
npm init -y生成package.json,项目本身也是一个“包”;再npm i lodash(npm install lodash的简写)安装,包会被放进node_modules,同时dependencies里记一条"lodash": "^4.17.21"。 - 引入第三方包和内置模块一样,参数里直接写包名,不写路径;查找顺序是:内置模块 → 当前目录
node_modules→ 逐级向上找node_modules→ 全局目录。 ^表示锁定大版本,重装时大版本不变、其余取最新;npm install会生成package-lock.json精确锁版本,所以删掉node_modules后依据package.json重装即可恢复。- 开发依赖用
npm i 包名 -D(如打包/编译类工具),Node 后端项目里基本都是产品依赖。 - 易错点:命令行工具要全局装
npm i nodemon -g,装完若提示“无法加载/禁止运行脚本”,需把全局node_modules所在目录加入系统环境变量 Path,并以管理员身份在 PowerShell 执行Set-ExecutionPolicy RemoteSigned后回车确认。
常见错误
1. 用 exports = fn 暴露函数,别人 require 拿到 {}
- 现象:模块里明明写了暴露语句,引入方打印出来是空对象,调用直接
TypeError: xxx is not a function。 - 原因:
exports只是module.exports的一个引用副本,对它整体重新赋值等于把指针掰断,指向了新对象,而require只认module.exports。 - 修法:整体暴露一律用
module.exports = fn;只有在"往导出对象上挂属性"时才用exports.xxx = ...。
2. 引入自己的模块时漏写 ./,报 MODULE_NOT_FOUND
- 现象:文件明明就在同级目录,
require('add')却提示找不到模块;换成require('add.js')也一样报错。 - 原因:不以
./或../开头,Node 会判定为"内置模块 / 第三方包名",跑去node_modules里找,压根不去当前目录。 - 修法:自定义模块必须写成
require('./add');扩展名可省,./不可省。
3. 换个目录执行脚本,读文件突然报 ENOENT,但 require 依然正常
- 现象:在项目根目录
node src/index.js一切正常,cd到别的目录再执行,文件读取失败,模块导入却毫无问题。 - 原因:文件路径的相对基准是命令行所在目录(cwd),模块路径的相对基准是当前脚本文件所在目录,两者不是一回事。
- 修法:读写文件统一用
path.join(__dirname, '相对路径')拼出绝对路径;或在固定目录下启动脚本。
4. 加了 "type": "module" 之后 require is not defined
- 现象:配置完
package.json想用 ESM,结果原来能跑的require、__dirname全部报错。 - 原因:
"type": "module"把该目录下所有.js都按 ESM 解析,CommonJS 的语法和变量被整体禁用,两套规范在同一文件里互斥。 - 修法:要么去掉
type回到 CommonJS;要么用.mjs/.cjs后缀显式区分,并改用import、import.meta.url取目录。
5. export default 暴露的数据,用 import { xxx } 接收得到 undefined
- 现象:模块里写
export default fn,导入方写import { fn } from './m.js',调用时报 undefined。 - 原因:
default是默认导出,与具名导出属于两条通道;花括号形式只匹配具名导出,且变量名必须与导出名完全一致。 - 修法:默认导出用
import fn from './m.js';确需花括号,写import { default as fn };多数据导出则保持名字一致或用as重命名。
6. 手贱删了 node_modules,以为依赖全丢了
- 现象:删目录后项目跑不起来,有人开始从别的项目拷贝
node_modules甚至重新一个个装包。 - 原因:不清楚
package.json的dependencies记录了依赖清单、package-lock.json锁定了精确版本。 - 修法:直接在项目根目录
npm install一条命令还原;node_modules永远不要提交、不要拷贝、不要手动改。
7. 全局装了 nodemon 却用不了:一会儿"无法加载文件",一会儿"禁止运行脚本"
- 现象:
npm i -g nodemon显示成功,敲nodemon却报错,路径指向全局node_modules下的.ps1文件。 - 原因:两层问题叠加——全局 bin 目录没进系统
Path(系统找不到命令),以及 Windows PowerShell 默认执行策略禁止运行脚本。 - 修法:把 npm 全局目录(
npm config get prefix的结果,含.cmd那一层)追加到系统环境变量Path;再用管理员身份打开 PowerShell 执行Set-ExecutionPolicy RemoteSigned,之后重开终端。
8. npm init 时包名写了大写或中文,反复报错
- 现象:初始化过程中卡在 name 一项,输入
MyProject或中文名直接被打回。 - 原因:包名规范要求全小写、无中文、不能与内置模块同名、不能用
npm这类保留字。 - 修法:用纯小写英文加连字符(如
my-project);不想逐项填写就npm init -y,之后手动改package.json。
练习
1. 找错并修复 下面模块被 require('./calc') 引入后,calc.add(1,2) 报 "calc.add is not a function",请指出问题并给出两种正确写法。
js
exports = { add: (a, b) => a + b };2. 双规范改写 把实际中实现的字节单位转换模块(bytes, type = 0,按 1024 的 type 次方做除数)分别用 CommonJS 和 ESM 各写一遍,并各配一个使用文件;要求 ESM 版本不改动 package.json,说明你是怎么做到的。
3. 路径推演 目录结构如下,当前命令行位于 proj/ 根目录,执行 node src/main.js:
proj/
data.txt
src/
main.js
util.jsmain.js 里写了 require('./util') 和 fs.readFileSync('./data.txt')。请判断哪一句会失败、为什么;再给出不改变启动目录的修复方案。
4. npm 操作流水 在一个新目录里完成:初始化项目 → 把 lodash 装为生产依赖 → 把 nodemon 装为全局命令 → 手动删除 node_modules → 一条命令恢复环境。写出每一步的命令,并说明为什么删除后还能精确还原到原来的版本。
5. 故障排查 同学反馈:全局安装 nodemon 成功,但终端提示"无法将 nodemon 项识别为 cmdlet",或提示"因为在此系统上禁止运行脚本"。请分别说明这两条报错各自的成因和对应的处理步骤。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 模块的路径问题 | 22:30 |
| 2 | 作业讲解 | 16:20 |
| 3 | npm介绍 | 11:24 |
| 4 | npm-安装包 | 25:18 |
| 5 | 如何使用第三方包 | 05:24 |
| 6 | npm-全局安装包 | 16:27 |
小结
module.exports是唯一出口,exports只是它的别名;"整体替换"和"挂属性"是两种不同操作,混用必踩坑。- 自定义模块必须带
./或../,这是 Node 区分"本地文件"与"包"的唯一依据,与文件在不在同级目录无关。 - 文件路径跟着命令行走、模块路径跟着文件走——这条差异决定了工程里凡涉及 IO 路径,都应基于
__dirname拼绝对路径。 - CommonJS 与 ESM 是两套互斥的解析规则,靠
.mjs/.cjs或package.json的type切换;二者不能在同一文件内混用。 - 现实开发中 CommonJS 仍是 Node 侧的主力选择,遇到只支持 ESM 的新版包,优先降级到该包的 CommonJS 版本,而不是全项目切换规范。
package.json记"要什么",package-lock.json记"要哪个版本",node_modules只是可再生的产物——三者职责分清,依赖管理就不会乱。- 本地安装与全局安装的分界线是"代码里 require 的"还是"命令行里敲的";装不上全局命令,八成是
Path或 PowerShell 执行策略的问题,而非安装失败。 - 判断一个包值不值得用,看它是否被广泛使用、有无官网与文档、维护是否活跃——检索和筛选现成方案的能力,本身就是 Node 开发的核心竞争力。