CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Node.js 后端入门/课时/模块与 npm
RawBlame
课时目录12 节

Node.js 后端入门

  1. 01Node.js 是什么
  2. 02运行时与 process
  3. 03模块与 npm
  4. 04文件系统与 path
  5. 05http 模块
  6. 06Express 入门
  7. 07事件循环与异步
  8. 08环境变量与配置
  9. 09Streams 基础
  10. 10错误处理
  11. 11测试 node:test
  12. 12综合练习与选型

模块与 npm

CommonJS、ESM、package.json、npx、依赖安装。

35 分钟

模块与 npm

这一课你将能做到

  1. 分别用 CommonJS 和 ESM 编写、导入模块,并说清两者的核心差异
  2. 用 npm 完成初始化、安装依赖、配置脚本,理解 package.json 与 lockfile 的分工
  3. 读懂语义化版本号 ^1.2.3,预判升级风险

为什么要学

代码一多就必须拆文件,拆完就要解决「文件之间怎么互相引用」——这就是模块系统。而 npm 是 Node 世界的「应用商店」:生态里 200 多万个包,装一个包就能少写几百行代码。

实际开发里最常见的三个场景全和这课有关:接手项目先看 package.json(知道用了什么、怎么跑起来);装依赖(npm install);把通用逻辑抽成模块复用。CommonJS 和 ESM 并存的现状会持续很多年,两种都得会读会写——看老项目和看新项目用的可能不是同一套。

先建立图像

一个 Node 项目的依赖世界:

my-app/
├── package.json          ← 项目身份证:名字、依赖清单、脚本
├── package-lock.json     ← 依赖的精确版本锁(提交到 git)
├── node_modules/         ← 依赖的实际代码(不提交 git)
│   ├── dayjs/
│   └── lodash/
└── src/
    ├── index.js          ── require ──►  ./utils/format.js
    └── utils/
        └── format.js
CommonJS(CJS)ESM
导入require()import
导出module.exportsexport / export default
载入时机运行时,同步编译期静态分析
文件后缀.js(默认).mjs 或 package.json 声明 "type": "module"
取目录__dirnameimport.meta.url
江湖地位老项目、大量存量代码官方标准、新项目主流

跟着做

步骤 1:第一个 CommonJS 模块

新建 cjs-demo 目录,npm init -y,新建 utils.js:

// utils.js
function formatDate(date) {
  const d = new Date(date);
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}

function slugify(text) {
  return text.trim().toLowerCase().replace(/\s+/g, '-');
}

module.exports = { formatDate, slugify };   // 对外暴露一个对象

新建 index.js 使用它:

// index.js
const { formatDate, slugify } = require('./utils');   // 相对路径必须以 ./ 开头

console.log(formatDate('2026-10-06'));   // 2026-10-06
console.log(slugify('Hello Node World')); // hello-node-world
node index.js

要点:module.exports 导出什么,require 就拿到什么;require 的参数写相对/绝对路径时必须以 ./ 或 ../ 开头,只写名字则去 node_modules 找。

步骤 2:export 的两种姿势

// 导出多个:用对象打包
module.exports = { formatDate, slugify };

// 导出单个:直接给值
module.exports = class Config { /* ... */ };

新手常见错法是混用 exports.xxx = ... 和 module.exports = ...:exports 只是 module.exports 的引用,一旦给 module.exports 整体赋值,之前挂在 exports 上的东西全丢。建议只用 module.exports,少踩一个坑。

步骤 3:第一个 ESM 模块

新建 esm-demo 目录,npm init -y 后,编辑 package.json 加一行:

{
  "type": "module"
}

新建 utils.mjs(或直接叫 utils.js,因为声明了 type:module,.js 也会按 ESM 处理):

// utils.mjs
export function formatDate(date) {
  const d = new Date(date);
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}

export const APP_NAME = 'esm-demo';

export default function banner() {
  return `== ${APP_NAME} ==`;
}

新建 index.js:

// index.js
import banner, { formatDate, APP_NAME } from './utils.mjs';
//       ↑ 默认导出    ↑ 命名导出(名字必须对得上)

console.log(banner());
console.log(formatDate('2026-10-06'), APP_NAME);
node index.js
# == esm-demo ==
# 2026-10-06 esm-demo

规则:命名导出用 { 花括号 } 且名字必须与导出方一致(可用 as 改名);默认导出每个文件最多一个,导入时不用花括号、名字随便起。

ESM 里取「脚本所在目录」用:

import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Node 20.11+ 也可以直接用 import.meta.dirname

步骤 4:npm install 一个依赖

在 esm-demo 里装 dayjs(日期处理库):

npm install dayjs

命令完成后观察三件事:package.json 多了 "dependencies": { "dayjs": "^1.11.x" };生成了 package-lock.json;多了 node_modules 目录。然后使用:

// index.js 顶部加上
import dayjs from 'dayjs';

console.log(dayjs().format('YYYY-MM-DD HH:mm:ss'));
node index.js
# 2026-10-06 14:30:25(当前时间)

对比第 1 步手写的 formatDate——这就是用包的价值:边界情况(时区、闰年)别人都替你踩过了。

步骤 5:理解 npm 的常用操作

npm init -y                    # 生成 package.json(-y 全部默认值)
npm install dayjs              # 装"运行时依赖"(生产也要用)
npm install -D nodemon         # 装"开发依赖"(只在开发时用),进 devDependencies
npm uninstall dayjs            # 卸载并从 package.json 移除
npm run <脚本名>               # 执行 package.json 里的 scripts
npm ls --depth=0               # 查看直接依赖

package.json 里的 scripts 是项目「命令别名」:

{
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js"
  }
}
npm run dev
# node --watch 是 Node 20 内置的文件监听重启,改完代码自动重启

约定:npm start、npm test 可以省略 run,其他自定义脚本必须 npm run xxx。

步骤 6:读懂语义化版本

package-lock.json 里锁的是精确版本,package.json 里写的是范围:

1.11.0
│ │ └── 补丁位:修 bug,装了绝对不炸(升级无风险)
│ └──── 次位  :加功能,向后兼容(升级小风险)
└────── 主位  :破坏性变更(升级可能要改代码)

^1.11.0  → 允许 1.x.x 内升级(最常见,npm 默认)
~1.11.0  → 只允许 1.11.x 补丁升级(更保守)
1.11.0   → 锁死精确版本(最严格)

所以 npm install 时 npm 按 ^ 范围装最新兼容版,而 lockfile 保证你和同事装到完全相同的版本——这就是 lockfile 必须提交 git 的原因:删掉 node_modules 重装,版本也不会漂移。

讲清楚要点

  1. CommonJS 是 Node 的历史方案,ESM 是语言标准:新项目优先 ESM(静态分析可做 tree-shaking、支持顶层 await);但 CJS 是存量现实,老项目和很多工具仍是 CJS。
  2. require 路径规则:./ ../ 开头找文件;纯名字先找内置模块、再找 node_modules。写错开头的 require('utils') 会报 Cannot find module。
  3. "type": "module" 决定 .js 的解读方式:不写就是 CJS,写了就是 ESM。混用会报 ERR_REQUIRE_ESM 或 Unexpected token 'export'——看到这两个报错先查 type 声明和后缀。
  4. dependencies 与 devDependencies:运行时要用的进前者(express),只在开发构建用的进后者(nodemon、eslint)。装的时候 -D 一键归位。
  5. lockfile 是团队协作的锚:package.json 写「范围」,lockfile 锁「精确值」。两者都提交 git;装依赖一律 npm install(或 npm ci 在 CI 中严格按 lockfile 装),别手改 lockfile。

你可能会懵的地方

  • 同一个项目里 CJS 和 ESM 能混用吗? 能有限混用:ESM 里 import 一个 CJS 包没问题(default 拿到 module.exports);CJS 里不能直接 require 一个真 ESM 文件,要用动态 await import()。所以库作者常同时发布两种格式。

  • node_modules 为什么那么大?能删吗? 依赖的依赖会递归安装,几十 MB 到几百 MB 都正常。可以随时删,npm install 会按 lockfile 完整重建。所以 git 里只提交 package.json + lockfile,node_modules 进 .gitignore。

  • npm install 和 npm ci 的区别? install 按 package.json 的版本范围装并更新 lockfile;ci 严格按 lockfile 装且先删掉 node_modules,不更新任何文件。CI 流水线和「完全还原环境」用 ci。

  • 为什么我 import 的包要用 import dayjs from 'dayjs' 而不是花括号? dayjs 是 CJS 包,module.exports 是一个函数,ESM 把它映射成默认导出。拿到对象再解构也可以:import dayjs from 'dayjs' 之后 dayjs().format(...)。每个包的导出形态看它的文档。

  • 什么时候把代码抽成模块? 同一段逻辑出现在第二个文件时就该抽了。抽的标准:一个文件只干一类事,导出面尽量小(不用的别导出)。

错法对照

错法问题正解
require('./utils') 但文件叫 util.js拼写不符,Cannot find module核对文件名(大小写、复数)
require('utils') 找本地文件没带 ./,会被当作包名去 node_modules 找require('./utils')
CJS 项目里写 exportUnexpected token 'export'用 module.exports,或改造成 ESM
ESM 项目里用 __dirnameReferenceErrorpath.dirname(fileURLToPath(import.meta.url))
把 node_modules 提交 git仓库爆炸、跨平台二进制冲突提交 package.json + package-lock.json
exports.xxx = a; module.exports = b前者全部丢失统一只用 module.exports
手动改 lockfile 或只提交 package.json团队成员装出版本漂移lockfile 与 package.json 一起提交

卡点急救

  1. Cannot find module 'xxx':三查——是不是本地文件却没加 ./;文件名拼写/大小写;是不是第三方包没 npm install。
  2. ERR_REQUIRE_ESM:你在 CJS 文件里 require 了一个 ESM。改用动态导入 const pkg = await import('pkg')(注意拿到的是 .default),或把整个项目声明为 "type": "module"。
  3. SyntaxError: Unexpected token 'export':你在按 CJS 解析的文件里写了 ESM 语法。给 package.json 加 "type": "module",或文件后缀改 .mjs。
  4. 装包很慢或超时:网络问题,配置国内镜像 npm config set registry https://registry.npmmirror.com,用完官方源再 npm config delete registry 还原。
  5. 依赖版本冲突、node_modules 状态诡异:删掉 node_modules 和 lockfile 重新 npm install(最后手段,平时别乱删 lockfile)。

面试常问

  • CommonJS 和 ESM 的区别? CJS 是 Node 的运行时方案:require/module.exports,同步加载,值是导出时的拷贝(基本类型);ESM 是语言标准:import/export,编译期静态分析(利于 tree-shaking),导出的是「活绑定」(引用),支持顶层 await。Node 通过 "type": "module" 或 .mjs 后缀启用 ESM。

  • package-lock.json 的作用?为什么必须提交? 锁定依赖树的精确版本和下载地址,保证任何人在任何机器 npm install 得到完全一致的依赖。不提交会导致「范围版本」随时间漂移,出现本地正常、线上报错。

  • 什么是语义化版本?^ 和 ~ 的区别? 主.次.补丁:主位破坏兼容、次位加功能、补丁位修 bug。^1.2.3 允许升到 <2.0.0,~1.2.3 只允许 <1.3.0。

  • 循环依赖会怎样? Node 能处理但结果可能出乎意料:CJS 下先完成的模块先返回「半成品」导出,后加载者拿到不完整的对象;ESM 通过活绑定通常能拿到最终值。正确做法是重构出公共的下层模块,消除循环。

自测三问

  1. ESM 里如何获得与 CJS __dirname 等价的值?
  2. npm install -D 和 npm install 生成的依赖分别写进 package.json 的哪个字段?各举一例。
  3. ^2.3.0 允许安装的最高版本是多少?2.9.9 可以吗?3.0.0 呢?

小结一句话

CJS 用 require/module.exports、ESM 用 import/export,两种都要会;package.json 声明范围、lockfile 锁定精确版本,node_modules 永远由 npm 重建而非手维护。

延伸

  • Node.js 官方文档 Modules
  • npm 官方文档 package.json
  • 下一课:文件系统与 path——用 fs 读写文件,用 path 拼出跨平台不出错的路径。
加载本节练习…
← 上一课运行时与 process下一课 →文件系统与 path