第8章 8.4 npm 与 yarn 包管理
📖 这是「JavaScript 从入门到精通」第 39 / 45 章
⏱️ 预计学习时间:90 分钟
🎯 学习目标:从「会用别人写好的包」到「能独立管理项目依赖」
🎯 开场 3 分钟:为什么要学这个?
想象一下:你刚学做饭,买了菜谱书准备大展身手,结果发现——
- 「首先,你需要一口不粘锅」
- 「然后,你需要一个打蛋器」
- 「接着,你还需要一个量杯、电子秤、硅胶铲……」
你是不是头都大了? 难道做一道菜,还要先把五金店搬回家?
npm 和 yarn 就是 JavaScript 世界的「食材配送箱」。
上一章我们学会了 Node.js 环境能运行 JS 代码,但光有锅(运行环境)没有菜(第三方工具库)还是做不成饭。这一章我们就来搞定「去哪买菜」和「怎么保鲜」的问题。
🧱 基础 25 分钟:核心概念
8.4.1 npm 是什么?
npm = Node Package Manager(Node.js 的包\n\n
\n\n
\n\n管理器)
说白了,就是一个「JS 代码仓库 + 下载工具」。全世界的开发者把自己写的工具代码上传到 npm,别人用的时候一行命令就能「下载安装」到自己的项目里。
类比理解:就像手机应用商店。你不用关心微信是怎么写的,下载安装就能用。npm 就是 JavaScript 世界的「App Store」。
8.4.2 初始化项目:package.json
做任何项目之前,先给项目建个「档案袋」,记录「项目叫什么、有谁参与、用了哪些工具」。
mkdir my-project
cd my-project
npm init -y
运行完你会发现多了个 package.json 文件,打开看看:
{
"name": "my-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
解释一下关键字段:
| 字段 | 含义 | 类比 |
|---|---|---|
name |
项目名字 | 菜谱书名 |
version |
版本号 | 第几版 |
main |
入口文件 | 翻到哪页开始做菜 |
scripts |
快捷命令 | 书里的「小贴士」快捷方式 |
-y 参数的意思是「所有问题都 YES」,不填的话会一个个问你。
8.4.3 安装第一个包:lodash
假设你要处理一组数据,需要「深拷贝」功能(后面会详细讲)。不用自己写,去 npm 搜!
npm install lodash
安装完成后多了两样东西:
my-project/
├── package.json # 档案袋(多了 lodash 记录)
└── node_modules/ # 储物柜(实际下载的代码在这里)
└── lodash/ # 别人写好的工具
再看 package.json,多了 dependencies 字段:
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"lodash": "^4.17.21"
}
}
dependencies 是什么?「这个项目用了哪些工具」。就像菜谱书最后的「所需工具清单」。
现在你可以用 lodash 了!创建一个 index.js:
const _ = require('lodash')
// 深拷贝:创建一个完全独立的数据副本
const original = { name: '小明', scores: [90, 85, 88] }
const copy = _.cloneDeep(original)
copy.scores.push(100) // 修改副本
console.log('原数据:', original)
console.log('副本:', copy)
运行 node index.js,输出:
原数据: { name: '小明', scores: [ 90, 85, 88 ] }
副本: { name: '小明', scores: [ 90, 85, 88, 100 ] }
这就是深拷贝的意义:副本怎么折腾,原数据纹丝不动。
8.4.4 理解版本号:^4.17.21 到底啥意思?
package.json 里写的 "lodash": "^4.17.21" 不是乱写的,有国际标准,叫 SemVer(语义化版本)。
格式:主版本.次版本.修订号
| 符号 | 含义 | 例子 |
|---|---|---|
^ |
允许小版本更新 | ^4.17.21 → 可以用 4.18.x,但不能跨到 5.x |
~ |
允许补丁更新 | ~4.17.21 → 可以用 4.17.x,但不能跨到 4.18 |
| 无符号 | 锁死精确版本 | 4.17.21 → 只用这一个版本 |
* |
最新版本 | * → 永远用最新的(危险!) |
记忆口诀:^是大更新,~是小更新,没符号是锁死。
8.4.5 yarn:另一个包管理器
yarn 是 Facebook 出品的「npm 增强版」,速度快,支持离线缓存。
# 全局安装 yarn(macOS/Linux 用 sudo)
npm install -g yarn
# 在项目里用 yarn 替代 npm
yarn add lodash
yarn 的命令对照表:
| npm 命令 | yarn 命令 | 说明 |
|---|---|---|
npm install |
yarn |
安装所有依赖 |
npm install lodash |
yarn add lodash |
添加依赖 |
npm uninstall lodash |
yarn remove lodash |
卸载依赖 |
npm run dev |
yarn dev |
运行脚本 |
yarn 的优势:
1. 下载速度快(并行下载)
2. 离线也能用(缓存机制)
3. 每次安装结果一致(确定性)
💡 小提示:团队协作时,统一用 npm 或 yarn 就行,混用可能导致
node_modules混乱。
8.4.6 scripts:自定义快捷命令
package.json 里的 scripts 可以定义快捷命令,就像给常用操作起别名。
{
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js",
"build": "node build.js",
"clean": "rm -rf dist"
}
}
现在你可以这样运行:
npm run start
# 等价于执行:node index.js
注意:start 和 test 可以省略 run,直接 npm start。
8.4.7 开发依赖 vs 生产依赖
有些包只是「开发时用」,上线后就不需要了:
# 开发依赖:只在开发时用,上线不需要
npm install --save-dev eslint
# 生产依赖:上线也需要
npm install lodash
区别在 package.json 里的位置:
{
"dependencies": {
"lodash": "^4.17.21" // 上线必须用
},
"devDependencies": {
"eslint": "^8.0.0" // 开发工具,上线不需要
}
}
类比理解:
- dependencies = 做菜必须用的食材
- devDependencies = 切菜用的砧板、刀,做完饭就收起来了
🔥 实战 35 分钟:3 个递进的小项目
📦 项目 1:5 分钟 - 做一个「依赖检查器」
场景:你接手了一个旧项目,想知道它用了哪些工具。
// check-deps.js
const fs = require('fs')
// 读取 package.json
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'))
// 打印所有依赖信息
console.log('📦 项目名称:', packageJson.name)
console.log('📌 生产依赖:')
if (packageJson.dependencies) {
Object.entries(packageJson.dependencies).forEach(([name, version]) => {
console.log(` - ${name}: ${version}`)
})
} else {
console.log(' (无)')
}
console.log('🔧 开发依赖:')
if (packageJson.devDependencies) {
Object.entries(packageJson.devDependencies).forEach(([name, version]) => {
console.log(` - ${name}: ${version}`)
})
} else {
console.log(' (无)')
}
运行:
npm install lodash --save-dev eslint
node check-deps.js
预期输出:
📦 项目名称: my-project
📌 生产依赖:
- lodash: ^4.17.21
🔧 开发依赖:
- eslint: ^9.0.0
解释:这个脚本读取当前目录的 package.json,把所有依赖信息格式化打印出来。
📦 项目 2:15 分钟 - 做一个「版本检测器」
场景:你想知道项目里哪些包有新版本可以更新。
首先安装版本检测工具:
npm install semver
// check-updates.js
const fs = require('fs')
const semver = require('semver')
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'))
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
}
console.log('🔍 正在检查版本...\n')
// 模拟的「最新版本数据库」(实际应该调 npm API)
const latestVersions = {
'lodash': '4.17.21',
'eslint': '9.3.0',
'semver': '7.5.0'
}
let hasUpdate = false
Object.entries(allDeps).forEach(([name, current]) => {
const latest = latestVersions[name]
if (!latest) return
const currentVer = current.replace(/[\^~]/, '')
const hasNewer = semver.lt(currentVer, latest)
if (hasNewer) {
hasUpdate = true
console.log(`⚠️ ${name}`)
console.log(` 当前: ${current} → 最新: ${latest}`)
}
})
if (!hasUpdate) {
console.log('✅ 所有包都是最新版本!')
}
运行:
node check-updates.js
预期输出:
🔍 正在检查版本...
⚠️ eslint
前: ^9.0.0 → 最新: 9.3.0
⚠️ semver
前: ^7.5.0 → 最新: 7.5.0
✅ 所有包都是最新版本!
解释:semver 是专门处理版本号的工具,semver.lt(a, b) 判断 a 是否小于 b(Less Than)。
📦 项目 3:15 分钟 - 做一个「依赖关系图生成器」
场景:项目依赖越来越多,想知道它们之间的关系。
// deps-tree.js
const fs = require('fs')
const path = require('path')
function getPackageName(depPath) {
try {
const pkg = JSON.parse(fs.readFileSync(depPath, 'utf-8'))
return pkg.name || path.basename(depPath)
} catch {
return path.basename(depPath)
}
}
function scanDeps(basePath, deps = {}, depth = 0) {
const nodeModulesPath = path.join(basePath, 'node_modules')
if (!fs.existsSync(nodeModulesPath)) return deps
const packages = fs.readdirSync(nodeModulesPath)
packages.forEach(pkgName => {
// 跳过作用域包(如 @babel/xxx)
if (pkgName.startsWith('@')) {
const scopedPackages = fs.readdirSync(
path.join(nodeModulesPath, pkgName)
)
scopedPackages.forEach(scopedPkg => {
const fullPath = path.join(nodeModulesPath, pkgName, scopedPkg)
const indent = ' '.repeat(depth + 1)
console.log(`${indent}└─ @${pkgName}/${scopedPkg}`)
scanDeps(fullPath, deps, depth + 1)
})
} else {
const fullPath = path.join(nodeModulesPath, pkgName)
const indent = ' '.repeat(depth + 1)
console.log(`${indent}└─ ${pkgName}`)
scanDeps(fullPath, deps, depth + 1)
}
})
return deps
}
console.log(`📂 依赖树 for: ${process.cwd()}\n`)
console.log('└─ (root)')
scanDeps(process.cwd())
运行:
node deps-tree.js
预期输出(简化示例):
📂 依赖树 for: /Users/xxx/my-project
└─ (root)
└─ node_modules
└─ lodash
└─ semver
└─ node_modules
└─ balanced-match
└─ concat-map
解释:这个脚本递归扫描 node_modules 目录,用树状结构展示依赖关系,帮助你理解「谁依赖谁」。
💪 进阶 20 分钟:常见坑 + 性能小贴士
❌ 坑 1:node_modules 太大,GitHub 传不上去
错误做法:把 node_modules 一起提交到 Git
正确做法:在项目根目录加 .gitignore 文件
# 创建 .gitignore
echo "node_modules/" > .gitignore
# .gitignore 内容
node_modules/
dist/
.env
*.log
解释:.gitignore 告诉 Git「这些文件夹不要管」,换电脑后用 npm install 重新下载即可。
❌ 坑 2:全局安装 vs 本地安装,傻傻分不清
错误做法:
npm install lodash # 本地安装(正确)
node index.js # 运行报错!
原因:本地安装的包只能用 require() 引用,命令行工具才需要全局安装。
| 安装方式 | 用途 | 引用方式 |
|---|---|---|
npm install lodash |
项目依赖 | const _ = require('lodash') |
npm install -g nodemon |
命令行工具 | nodemon server.js |
❌ 坑 3:依赖版本冲突
场景:项目 A 用 lodash@4,项目 B 用 lodash@5,把他俩装一起就冲突了。
解决思路:npm 会自动处理,找一个「最大公约数」版本。如果实在冲突,用 npm ls 排查:
npm ls lodash
输出会告诉你「谁用了这个包、用的是什么版本」。
❌ 坑 4:删了 node_modules 不知道怎么恢复
错误做法:手动一个个重新装
正确做法:
rm -rf node_modules # 删除
npm install # 重新安装(根据 package.json)
解释:package.json 记录了所有依赖,一个命令就能恢复。
❌ 坑 5:yarn.lock 和 package-lock.json 混用
错误场景:你用 npm,他用 yarn,提交时把两个 lock 文件都提交了。
正确做法:团队统一用同一个包管理器,选一个坚持用。
⚡ 性能小技巧:使用 npx 直接运行命令
有些工具「用一次就不用了」,装全局太浪费,装本地又懒得写路径。
npx 完美解决这个问题:
# 不用安装,直接运行
npx cowsay "Hello npm!"
# 效果等价于
./node_modules/.bin/cowsay "Hello npm!"
解释:npx 会自动找本地 node_modules 里的命令,找不到就临时下载、用完删除。
🐛 调试技巧:npm scripts 怎么调试?
如果你写的 npm script 报错了,加个 -- 后面跟调试参数:
npm run dev -- --inspect
或者直接看详细日志:
npm run dev --verbose
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):改项目名
- 输入:修改 package.json 的 name 为 "my-awesome-tool"
- 预期输出:运行 node check-deps.js 显示新的项目名
- 提示:直接改 package.json 的 name 字段即可
练习 2(2 分钟):加个判断
- 输入:在 check-deps.js 基础上,如果没有依赖就显示「这个项目很干净,暂时没有依赖」
- 预期输出:对于空项目显示提示语
- 提示:用 Object.keys() 判断对象是否为空
练习 3(2 分钟):用 semver 比较版本
- 输入:比较 "2.0.0" 和 "1.9.0"
- 预期输出:打印 "2.0.0 更新"
- 提示:semver.gt(a, b) 判断 a 是否大于 b
练习 4(2 分钟):串起来用
- 输入:用 check-updates.js 的方法,但检测 lodash 版本
- 预期输出:显示 lodash 是否有更新
- 提示:把 lodash 加入检测列表
练习 5(2 分钟):分析报错
- 输入:运行 npm run dev 报错 "Missing script: dev"
- 预期输出:解释为什么报错,怎么修复
- 提示:检查 package.json 的 scripts 字段有没有 dev
📝 作业题(30 分钟-2 小时)
做一个「npm 依赖健康检查工具」
- 需求描述:输入一个项目路径,分析它的依赖状态,输出健康报告
- 功能点:
1. 读取指定路径的 package.json
2. 列出所有生产依赖和开发依赖
3. 检测依赖数量,输出健康建议(依赖过多可能有风险)
4. 生成简单的健康评分(如:依赖数 < 10 优秀,10-20 良好,>20 需优化) - 加分项:
1. 支持命令行参数指定路径
2. 输出彩色 terminal 报告 - 验收标准:
- 能运行
node health-check.js ./某个项目 - 正确读取并分析 package.json
- 输出包含评分和建议
📚 总结 + 资源
一句话总结
本章学了 npm/yarn 的安装、版本管理、scripts 脚本和常见坑——现在你可以像个真正的开发者一样,「站在巨人的肩膀上」用别人的代码了。
延伸资源
互动钩子
🎉 恭喜你完成了包管理学习!
下一步我们要学的 Vite,就是一个「帮你省去手动配置、一键启动开发服务器」的工具——它依赖的就是今天学的 npm 生态。
你在项目中踩过哪些依赖的坑? 依赖版本冲突、node_modules 膨胀、还是装了不该装的包?评论区聊聊,老粉优先回复!
上期回顾:第8章 8.3 Node.js 基础(与浏览器 JS 对比)
下期预告:[第8章 8.5 Vite 构建工具入门]——有了 npm 这个武器,下一章我们来玩转 Vite,一键启动「快如闪电」的开发服务器!
(全文约 5200 字,学习时间约 90 分钟)

评论(0)