Skip to content

模块的路径问题与作业讲解(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 等构造路径,而不是直接依赖命令行当前目录。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一: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.js

main.js 里写了 require('./util') 和 fs.readFileSync('./data.txt')。请判断哪一句会失败、为什么;再给出不改变启动目录的修复方案。

4. npm 操作流水 在一个新目录里完成:初始化项目 → 把 lodash 装为生产依赖 → 把 nodemon 装为全局命令 → 手动删除 node_modules → 一条命令恢复环境。写出每一步的命令,并说明为什么删除后还能精确还原到原来的版本。

5. 故障排查 同学反馈:全局安装 nodemon 成功,但终端提示"无法将 nodemon 项识别为 cmdlet",或提示"因为在此系统上禁止运行脚本"。请分别说明这两条报错各自的成因和对应的处理步骤。

原视频对照 ​

#视频时长
1模块的路径问题22:30
2作业讲解16:20
3npm介绍11:24
4npm-安装包25:18
5如何使用第三方包05:24
6npm-全局安装包16:27

小结 ​

  1. module.exports 是唯一出口,exports 只是它的别名;"整体替换"和"挂属性"是两种不同操作,混用必踩坑。
  2. 自定义模块必须带 ./ 或 ../,这是 Node 区分"本地文件"与"包"的唯一依据,与文件在不在同级目录无关。
  3. 文件路径跟着命令行走、模块路径跟着文件走——这条差异决定了工程里凡涉及 IO 路径,都应基于 __dirname 拼绝对路径。
  4. CommonJS 与 ESM 是两套互斥的解析规则,靠 .mjs/.cjs 或 package.json 的 type 切换;二者不能在同一文件内混用。
  5. 现实开发中 CommonJS 仍是 Node 侧的主力选择,遇到只支持 ESM 的新版包,优先降级到该包的 CommonJS 版本,而不是全项目切换规范。
  6. package.json 记"要什么",package-lock.json 记"要哪个版本",node_modules 只是可再生的产物——三者职责分清,依赖管理就不会乱。
  7. 本地安装与全局安装的分界线是"代码里 require 的"还是"命令行里敲的";装不上全局命令,八成是 Path 或 PowerShell 执行策略的问题,而非安装失败。
  8. 判断一个包值不值得用,看它是否被广泛使用、有无官网与文档、维护是否活跃——检索和筛选现成方案的能力,本身就是 Node 开发的核心竞争力。