第11期 | npm入门:包管理的世界
🎯 学习目标
- 理解npm的本质:它不只是一个安装命令,而是一个完整的包管理生态系统
- 掌握npm install/uninstall的全套用法:本地安装、全局安装、开发依赖、生产依赖
- 理解node_modules的目录结构和查找机制
- 掌握npx的原理和使用场景
- 理解package-lock.json的作用和团队协作中的重要性
📖 核心知识
一、npm是什么
1.1 从"造轮子"说起
假设你要在项目里格式化一个日期。如果没有包管理器,你的选择是:
// 方案1:自己写一个(费时费力,容易出bug)
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}–${month}–${day}`;
}
// 方案2:去网上搜一段代码,复制粘贴(不知道代码质量,可能有坑)
// 方案3:找到一个别人写好的库,下载源码,放到项目里(手动管理更新和依赖)
有了npm之后:
# 一行命令搞定
npm install dayjs
const dayjs = require('dayjs');
console.log(dayjs().format('YYYY-MM-DD')); // 2026-07-10
这就是包管理器的价值:让你站在巨人的肩膀上,复用经过社区验证的代码。
1.2 npm的三重身份
npm不只是一个工具,它同时是三样东西:
| CLI工具 | 命令行包管理器 | npm install、npm run |
| 包仓库 | 全球最大的开源软件仓库 | npmjs.com 网站 |
| Registry | 包的存储和分发服务 | registry.npmjs.org |
截至2026年,npm Registry上有超过300万个包,每周下载量超过2000亿次。它是JavaScript生态的基石。
1.3 npm vs yarn vs pnpm
三大包管理器对比:
| 默认安装 | Node.js自带 | 需单独安装 | 需单独安装 |
| 安装速度 | 中等 | 快 | 最快 |
| 磁盘占用 | 大 | 大 | 最小(硬链接) |
| Monorepo支持 | workspaces | workspaces | workspace(最强) |
| 幽灵依赖 | 有 | 有 | 无(严格隔离) |
| 学习成本 | 低 | 中 | 中 |
新手建议:先用npm,它是最通用的。等你遇到monorepo或磁盘空间问题再换pnpm。本课程全程使用npm。
二、npm install全解
2.1 基本用法
# 安装一个包
npm install express
# install可以简写为i
npm i express
# 安装指定版本
npm i express@4.18.0
# 安装最新版本
npm i express@latest
# 安装指定大版本的最新
npm i express@4
安装后,包会被放到node_modules/目录,同时写入package.json的dependencies字段:
{
"dependencies": {
"express": "^4.18.0"
}
}
2.2 依赖类型
npm有四种依赖类型,理解它们的区别至关重要:
# 1. 生产依赖(运行时需要)
npm i express
# 等同于
npm i express –save
# 2. 开发依赖(只在开发时需要,不打包到生产)
npm i jest –save-dev
# 简写
npm i jest -D
# 3. 全局安装(命令行工具)
npm i -g nodemon
# 4. 可选依赖(安装失败不报错)
npm i fsevents –save-optional
# 简写
npm i fsevents -O
在package.json中的体现:
{
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"jest": "^29.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.0"
}
}
关键区别:
# 安装所有依赖(包括devDependencies)
npm install
# 只安装生产依赖(不装devDependencies)
npm install –production
# 或设置环境变量
NODE_ENV=production npm install
在Docker部署时,这个区别很重要:
# 构建阶段:安装所有依赖
RUN npm install
# 生产阶段:只装生产依赖
RUN npm install –production
2.3 安装行为详解
# 普通安装:从registry下载,放入node_modules,更新package-lock.json
npm i axios
# 如果package.json中有该包但node_modules里没有(比如git clone后)
npm i # 会根据package-lock.json安装所有依赖
# 强制重新安装(删除后重装)
npm i axios –force
# 清除缓存后重装(解决奇怪的缓存问题)
npm cache clean –force
npm i
一个常见的坑:修改了package.json手动加了一个依赖,但忘了在node_modules里安装:
# 只更新package.json,不实际安装
npm i axios –package-lock-only
# 正确做法:正常安装
npm i axios
三、node_modules的查找机制
3.1 目录结构
假设你安装了express,express本身又依赖body-parser等包,node_modules的结构是这样的:
node_modules/
├── express/
│ ├── node_modules/ # express自己的依赖(嵌套)
│ │ ├── body-parser/
│ │ └── …
│ └── …
├── axios/
│ └── …
└── lodash/
└── …
但实际你会看到很多包都被"提升"(hoist)到了顶层:
node_modules/
├── express/
├── axios/
├── lodash/
├── body-parser/ # 被提升到顶层
├── debug/ # 被提升到顶层(多个包的公共依赖)
└── …
这是npm的依赖提升机制,目的是减少重复安装。
3.2 模块查找算法
当你在代码里require('axios')时,Node.js的查找顺序是:
require('axios');
查找过程:
1. 当前目录的node_modules/axios
2. 上级目录的node_modules/axios
3. 再上一级…
4. 直到系统根目录
5. 最后找全局node_modules
用代码验证:
// 查看模块的完整查找路径
console.log(require.resolve.paths('axios'));
// [
// '/project/node_modules',
// '/node_modules',
// '/node_modules'
// ]
实际意义:
// 项目结构
project/
├── node_modules/
│ └── axios/ # 在这里找到
├── src/
│ └── utils/
│ └── helper.js // 在这里 require('axios')
即使helper.js在src/utils/目录下,也能找到project/node_modules/axios,因为Node.js会逐级向上查找。
3.3 幽灵依赖问题
// package.json 只声明了 express
{
"dependencies": {
"express": "^4.18.0"
}
}
// 但你能直接 require express的子依赖(因为它被提升到了顶层)
const bodyParser = require('body-parser'); // 能找到!但它不在你的package.json里
这就是幽灵依赖:你用了没在package.json中声明的包。风险是:
# 某天express升级,不再依赖body-parser了
npm i express@5.0.0
# 你的代码报错了:Cannot find module 'body-parser'
pnpm通过严格的依赖隔离解决了这个问题,但用npm的话,你需要自己注意不要直接require未声明的包。
四、npm uninstall与更新
4.1 卸载
# 卸载一个包
npm uninstall express
# 简写
npm un express
# 卸载开发依赖
npm un jest
# 卸载全局包
npm un -g nodemon
卸载会同时从node_modules和package.json中删除。
4.2 检查更新
# 检查哪些包有新版本
npm outdated
# 输出示例
# Package Current Wanted Latest Location
# express 4.17.0 4.18.2 5.1.0 my-app
# jest 29.0.0 29.7.0 30.0.0 my-app
四个版本号的含义:
- Current:当前安装的版本
- Wanted:根据semver规则允许升级到的版本
- Latest:registry上的最新版本(可能是大版本升级)
# 更新到Wanted版本(安全更新)
npm update
# 更新指定包
npm update express
# 更新到Latest版本(可能是大版本,需要手动指定)
npm i express@latest
4.3 npm-check-updates
如果想要批量检查大版本更新,使用npm-check-updates:
# 全局安装
npm i -g npm-check-updates
# 检查所有可更新的包
ncu
# 输出
# express ^4.17.0 → ^5.1.0
# jest ^29.0.0 → ^30.0.0
# 一次性更新package.json中的所有版本号
ncu -u
# 然后安装
npm i
⚠️ 注意:大版本升级通常有Breaking Changes,一定要看changelog和测试。
五、npx详解
5.1 npx是什么
npx是npm 5.2+自带的工具,用于执行包的命令,而不需要全局安装。
# 不用npx:全局安装再使用
npm i -g create-react-app
create-react-app my-app
# 用npx:直接执行,不需要全局安装
npx create-react-app my-app
npx的工作原理:
5.2 实用场景
# 1. 执行脚手架工具(只执行一次,不需要全局安装)
npx create-react-app my-app
npx create-next-app my-app
npx degit user/repo my-project
# 2. 执行本地安装的CLI工具
npm i eslint # 本地安装
npx eslint . # 执行(不需要全局安装)
# 3. 执行指定版本的包
npx cowsay@1.5.0 "Hello"
# 4. 执行GitHub上的脚本
npx github:piuccio/cowsay "Hello from GitHub"
# 5. 一次性执行命令,不污染全局
npx serve # 在当前目录启动一个静态服务器
npx http-server
5.3 –yes 标志
# 第一次执行未安装的包,npx会问你是否安装
npx cowsay "Hello"
# Need to install the following packages:
# cowsay@1.5.0
# Ok to proceed? (y)
# 跳过确认
npx –yes cowsay "Hello"
# 简写
npx -y cowsay "Hello"
5.4 npm exec
npm 7+引入了npm exec,功能类似npx但有细微差别:
# npx
npx prettier –write .
# npm exec
npm exec — prettier –write .
在npm scripts中,推荐使用npm exec,因为它更明确。
六、package-lock.json
6.1 它是什么
package-lock.json记录了每个依赖的确切版本和完整的依赖树。
{
"name": "my-app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"express": "^4.18.0"
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-…"
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-…"
}
}
}
6.2 为什么需要它
场景:你开发了项目,package.json里写的是"express": "^4.18.0"。
# 你本地安装时,express最新是4.18.2
npm install
# 安装了 express@4.18.2
# 半年后,同事clone项目后安装
npm install
# express ^4.18.0 允许 4.19.0,安装了 express@4.19.0
# 你和同事的express版本不同!可能出bug
有了package-lock.json:
# 同事clone项目后
npm install
# 读取lock文件,安装 express@4.18.2(和你一样)
# 每个人安装的版本完全一致
6.3 团队协作规则
# 规则1:package-lock.json 必须提交到Git
git add package-lock.json
# 规则2:不要手动编辑lock文件
# 规则3:不要用 npm install 安装新依赖时改lock(用 npm i xxx)
# 规则4:CI/CD中使用 npm ci 而不是 npm install
6.4 npm ci vs npm install
# npm install:可能修改lock文件
npm install
# npm ci:严格按lock文件安装,不修改
npm ci
npm ci的特点:
- 删除整个node_modules后重新安装
- 严格按package-lock.json安装,不修改版本号
- 如果package.json和package-lock.json不一致,直接报错
- 速度比npm install快(不需要计算依赖树)
# GitHub Actions 中使用
– name: Install dependencies
run: npm ci # CI环境永远用ci
七、npm配置
7.1 .npmrc文件
npm的配置文件,可以放在三个位置:
# 1. 项目级:项目根目录/.npmrc
registry=https://registry.npmmirror.com/
@myorg:registry=https://npm.pkg.github.com
# 2. 用户级:~/.npmrc
registry=https://registry.npmjs.org/
init-author-name=Your Name
init-license=MIT
# 3. 全局级:npm配置目录
npm config get globalconfig # 查看路径
优先级:项目级 > 用户级 > 全局级。
7.2 常用配置
# 设置国内镜像(加速下载)
npm config set registry https://registry.npmmirror.com/
# 查看当前配置
npm config list
# 查看单个配置
npm config get registry
# 删除配置
npm config delete registry
# 使用nrm管理多个registry
npm i -g nrm
nrm ls # 列出所有镜像
nrm use taobao # 切换到淘宝镜像
nrm test # 测试各镜像速度
7.3 私有registry
# 企业内部registry
npm config set @mycompany:registry https://npm.company.com/
# 带认证的registry
npm config set //npm.company.com/:_authToken "your-token"
# 发布到私有registry
npm publish –registry https://npm.company.com/
🛠️ 实战练习
练习1:基础题——从零初始化项目
# 1. 创建项目目录
mkdir my-first-project && cd my-first-project
# 2. 初始化package.json
npm init -y
# 3. 安装生产依赖
npm i express
# 4. 安装开发依赖
npm i -D nodemon
# 5. 查看 node_modules 目录结构
ls node_modules/
# 6. 检查是否有更新
npm outdated
# 7. 查看express的版本
npm ls express
练习2:进阶题——理解依赖树
# 创建项目
mkdir dep-tree-demo && cd dep-tree-demo
npm init -y
# 安装express
npm i express
# 查看完整依赖树
npm ls
# 输出类似:
# dep-tree-demo@1.0.0
# └── express@4.18.2
# ├── accepts@1.3.8
# │ ├── mime-types@2.1.35
# │ │ └── mime-db@1.52.0
# │ └── negotiator@0.6.3
# ├── body-parser@1.20.1
# …
# 只看顶层依赖
npm ls –depth=0
# 查看某个包的所有依赖
npm ls –all express
# 解释为什么body-parser在顶层(依赖提升)
写一段代码验证幽灵依赖:
// app.js
// express依赖了body-parser,虽然你没直接安装
const bodyParser = require('body-parser');
console.log('bodyParser version:', require('body-parser/package.json').version);
// 能运行!但这是不安全的——它依赖express的内部依赖
// 检查你的package.json,没有body-parser
练习3:挑战题——搭建CI友好的项目
mkdir ci-friendly && cd ci-friendly
npm init -y
# 1. 安装依赖
npm i express cors
npm i -D jest eslint prettier
# 2. 生成lock文件
npm install
# 3. 模拟CI环境安装
rm -rf node_modules
npm ci # 应该和之前安装完全一致
# 4. 添加脚本
修改package.json:
{
"name": "ci-friendly",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"test": "jest",
"lint": "eslint .",
"ci": "npm ci && npm run lint && npm test"
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"eslint": "^8.0.0",
"jest": "^29.0.0",
"prettier": "^3.0.0"
}
}
// index.js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/', (req, res) => res.json({ status: 'ok' }));
if (require.main === module) {
app.listen(3000, () => console.log('Server on http://localhost:3000'));
}
module.exports = app;
// __tests__/index.test.js
const request = require('supertest');
const app = require('../index');
test('GET / returns ok', async () => {
const res = await request(app).get('/');
expect(res.body.status).toBe('ok');
});
# 运行CI流程
npm run ci
💡 面试考点
Q1: npm install 和 npm ci 的区别?
A: npm install会解析package.json,可能更新package-lock.json,适合开发环境。npm ci严格按package-lock.json安装,删除并重建node_modules,不会修改lock文件,适合CI/CD环境,速度更快且确保一致性。
Q2: 什么是幽灵依赖?怎么解决?
A: 幽灵依赖是指项目使用了未在package.json中声明的包,但因为依赖提升(hoisting)能在node_modules中找到。风险是当被依赖的包升级或移除该子依赖时,代码会报错。解决方案是使用pnpm(严格的依赖隔离),或者自己确保只require在package.json中声明的包。
Q3: npx的作用是什么?和npm install -g有什么区别?
A: npx用于执行包的命令而不需要全局安装。npm install -g会永久全局安装,占用磁盘空间。npx临时下载执行后不保留全局安装。适合执行脚手架工具(如create-react-app)或一次性命令。
Q4: package-lock.json需要提交到Git吗?
A: 必须提交。它锁定了所有依赖的确切版本,确保团队成员和CI环境安装的依赖完全一致。没有它,^和~语义版本范围会导致不同时间安装的版本不同。
Q5: dependencies和devDependencies的区别?
A: dependencies是运行时需要的包(如express),devDependencies是开发时才需要的包(如jest、eslint)。npm install –production或NODE_ENV=production时只安装dependencies。在Docker多阶段构建中,生产镜像只装dependencies以减小体积。
📌 本期要点
🔗 下期预告
第12期 | package.json深度解析:项目配置大全——我们将深入这个项目"身份证"的每一个字段,从name、version到scripts、engines,让你完全掌握项目配置的方方面面。
iPA上传工具 – IPA解析与AppStore提交


