第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\nSimple tech illustration expla\n\nAI comic creation scene, creat\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

注意starttest 可以省略 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 脚本和常见坑——现在你可以像个真正的开发者一样,「站在巨人的肩膀上」用别人的代码了。

延伸资源

  1. npm 官方文档 - 最权威的参考资料
  2. yarn 官方文档 - yarn 详细教程
  3. SemVer 规范 - 版本号国际标准

互动钩子

🎉 恭喜你完成了包管理学习!

下一步我们要学的 Vite,就是一个「帮你省去手动配置、一键启动开发服务器」的工具——它依赖的就是今天学的 npm 生态。

你在项目中踩过哪些依赖的坑? 依赖版本冲突、node_modules 膨胀、还是装了不该装的包?评论区聊聊,老粉优先回复!


上期回顾第8章 8.3 Node.js 基础(与浏览器 JS 对比)
下期预告:[第8章 8.5 Vite 构建工具入门]——有了 npm 这个武器,下一章我们来玩转 Vite,一键启动「快如闪电」的开发服务器!


(全文约 5200 字,学习时间约 90 分钟)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。