什么是 npm?
npm 是 Node.js 生态里的包管理器,全称是 Node Package Manager。它通过公共软件仓库和命令行工具,帮 JavaScript / Node.js 项目安装、升级、锁定和发布可复用的代码包。

写 JavaScript 或 Node.js 项目时,不可能所有功能都从零实现。你需要用别人写好的代码,还要管理这些代码的版本,以及它们彼此之间的依赖。npm 就是为这件事准备的默认工具:安装 Node.js 时,通常也会一并得到 npm 命令行。
一句话总结:npm 不是单纯的下载器,而是 Node.js 生态里用来发现、安装、锁定和发布软件包的基础设施。
npm 解决了什么核心问题?
npm 解决的核心问题是:项目要复用别人的代码,同时把版本和依赖关系管起来,而不是每个功能都自己重写。

如果没有包管理器,常见结果是两头不讨好:
- 自己从零写:日期计算、HTTP 请求、加密、构建工具都要重做,开发速度慢,也更容易出错。
- 手动拷贝别人的代码:文件来源不清楚,版本对不上,升级时不知道该换哪些文件。
有了 npm,你可以从公共仓库安装已经发布的包,让版本号进入项目配置,再由工具自动处理后续依赖。它改变的是工作方式:从“到处找代码、手工粘贴”变成“声明需要什么,然后一条命令安装”。
因此,npm 的价值不在于让你少敲几行代码,而在于让复用、版本和依赖变成可自动执行的工程步骤。
npm 同时指哪三样东西?
npm 这三个字母经常同时指三样相关但不同的东西,很多人一开始会把它们混成一个概念。

可以这样分开记:
- 公共代码仓库(registry):全球开发者把自己的包发布上去,其他人可以搜索和下载。这是包真正存放和分发的地方。
- 命令行工具(CLI):安装在电脑上的
npm程序。你在终端里敲的npm install,用的就是它。 - 日常说的 npm:口语里说“用 npm 装一个包”,通常同时指前面两样:仓库提供包,命令行负责下载和管理。
官方文档还会把 npmjs.com 网站单独算进去。网站用来浏览包、管理账号和组织权限;注册表是包的数据库;CLI 是开发者最常用的操作入口。对日常开发来说,先记住“仓库 + 命令行”就够用。
同一个名字,对应的是一套互相配合的系统,而不是一个孤立的下载按钮。
什么是包?
一个包(package)就是一份带说明文件、可以被安装和复用的代码,通常包含函数、组件或命令行工具。

更精确一点:npm 里的包必须有一份 package.json,用来描述名称、版本、入口文件和依赖。当前这个项目只要有 package.json,它本身也是一个包,哪怕你并不打算发布它。
一个常见例子是处理日期。你不必自己实现时区、格式化和日历计算,可以安装日期库,例如 dayjs:
npm init -y
npm install dayjs
const dayjs = require('dayjs')
console.log(dayjs().format('YYYY-MM-DD'))
包的形态并不只有函数库:
- 函数或工具库:例如日期、校验、HTTP 客户端。
- UI 组件:例如前端组件库里的按钮、表格、表单。
- 命令行工具:例如创建项目的脚手架、代码检查和打包命令。
npm 官方把它称为世界上最大的软件注册表。仓库里现在有超过两百万个包。数量大意味着可复用代码多,也意味着筛选成本更高:同名功能可能有很多实现,维护状态、许可证和安全记录都需要判断,不能只看下载量。
为什么不能手动下载依赖,一定要有包管理器?
必须使用包管理器的核心原因,是依赖还有自己的依赖。你装的一个包,可能又依赖十个包,那十个包再各自依赖别的包。这棵树叫依赖树(dependency tree),靠手工维护几乎不可能。

以安装 Web 框架为例。你在项目里可能只写了一行直接依赖,真正装进 node_modules 的往往还有:
- 直接依赖:你明确安装的那个包。
- 二级依赖:它自己声明需要的包。
- 三级及更多依赖:那些包继续声明的包。
如果改用手动下载,你需要自己找到每个包的正确版本、解压到合适目录、处理不同包要求同一依赖的不同版本,还要在升级时重复整套工作。任何一环出错,项目都可能在另一台电脑上跑不起来。
npm 最核心的价值,就是把这棵依赖树的解析、下载和安装自动化。你声明“我需要这个包”,它负责把整棵树装完。
package.json 和 node_modules 分别做什么?
npm 项目的骨架主要由两个位置构成:package.json 负责声明,node_modules 负责存放实际代码。

package.json 可以理解成项目的身份证和说明书。它通常记录:
- 名称和版本:这个项目/包叫什么,当前是哪个版本。
- 依赖:运行时需要哪些包,开发时还需要哪些包。
- 脚本:启动、测试、打包等可重复执行的命令。
- 入口和其他元数据:主文件、许可证、仓库地址等。
一份最小的例子如下:
{
"name": "date-demo",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"test": "node --test"
},
"dependencies": {
"dayjs": "^1.11.0"
},
"devDependencies": {
"eslint": "^9.0.0"
}
}
两类依赖不要混用:
| 字段 | 何时安装 | 典型内容 |
|---|---|---|
dependencies | 运行项目时需要 | 日期库、Web 框架、数据库客户端 |
devDependencies | 只在开发、测试或构建时需要 | 代码检查、测试框架、打包工具 |
node_modules 则是实际下载下来的包所在的目录。程序 require() 或 import 时,通常就是从这里解析文件。它体积往往很大,而且可以根据 package.json 和 lock 文件重新生成,所以应用项目一般会把它加入 .gitignore,不提交到 Git。
可以记成一句话:package.json 说明你要什么,node_modules 保存你已经装到的东西。
依赖版本里的 ^ 和 ~ 是什么意思?
package.json 里的依赖版本通常不是写死的单个数字,而是一段允许安装的范围。这些范围来自语义化版本(Semantic Versioning,semver):版本号写成 MAJOR.MINOR.PATCH,也就是大版本、小版本和补丁。

三段数字的含义是:
- MAJOR(大版本):包含不兼容的改动,升级后调用方式或行为可能变。
- MINOR(小版本):在保持兼容的前提下增加功能。
- PATCH(补丁):兼容的问题修复。
package.json 里最常见的两种符号是 ^ 和 ~:
| 写法 | 含义 | 可安装范围 |
|---|---|---|
^1.2.3 | 允许同一大版本内的新功能和补丁,不跨大版本 | >=1.2.3 <2.0.0 |
~1.2.3 | 只允许同一小版本里的补丁 | >=1.2.3 <1.3.0 |
1.2.3 | 精确版本,不自动浮动 | 仅 1.2.3 |
所以,^1.2.3 可以升到 1.9.0,但不会自动升到 2.0.0;~1.2.3 可以升到 1.2.9,但不会自动升到 1.3.0。
有一个容易忽略的边界:如果主版本是 0,npm 对 ^ 更保守。^0.2.3 的范围相当于 >=0.2.3 <0.3.0,因为 0.x 按 semver 约定仍可能包含不兼容改动。
版本要这么讲究,是因为包升级可能带来不兼容行为,轻则测试失败,重则线上功能异常。范围写得太宽,下次安装可能装到你没验证过的版本;写得太死,又会错过安全修复。这就是为什么只靠 package.json 还不够。
为什么还需要 package-lock.json?
package-lock.json 记录的是上一次安装时,每个包精确到具体版本的快照。别人拿到你的项目,照着它安装,得到的就是同一棵依赖树,而不是“范围内随便一个新版本”。

package.json 里的 ^1.2.3 只表达“可以接受哪些版本”。如果没有 lock 文件,今天安装可能得到 1.4.0,一周后别人安装可能得到 1.5.0。只要中间某个间接依赖发布了有问题的版本,两台电脑上的项目就会表现不同。
lock 文件补上的是精确结果:哪个包解析成了哪个版本、从哪里下载、内容校验值是什么。npm 在安装或更新依赖时会自动生成或更新它。
| 文件或目录 | 记录什么 | 是否提交到 Git |
|---|---|---|
package.json | 项目信息和允许的版本范围 | 必须提交 |
package-lock.json | 实际解析到的精确版本和依赖树 | 应用项目应提交 |
node_modules | 下载下来的实际代码 | 通常不提交 |
实践上可以按项目类型区分:
- 应用程序(网站、服务、桌面工具):应该把
package-lock.json提交进 Git。持续集成里优先用npm ci:它会删除已有的node_modules,再按 lock 文件做一次干净安装;如果 lock 和package.json不一致,命令会直接失败,而不是悄悄改出版本。 - 准备发布给别人用的库:使用者安装你的包时,npm 主要看你在
package.json里声明的版本范围,而不是你仓库里的 lock 文件。因此库作者仍要把直接依赖的范围写准确。
package.json 描述允许什么,package-lock.json 冻结实际装了什么。 两者一起,才构成可复现的安装。
npm 日常最常用的命令有哪些?
理解了声明、安装目录和 lock 文件之后,常用命令就可以按职责来记。日常最高频的是安装、添加依赖、跑脚本和发布。

| 命令 | 做什么 |
|---|---|
npm install | 读取 package.json 和 lock 文件,把依赖装进 node_modules |
npm install <包名> | 安装一个新包,并写入 package.json 与 lock 文件 |
npm run <脚本名> | 执行 package.json 里 scripts 定义的命令 |
npm publish | 把当前包发布到公共仓库,让别人也能安装 |
对应到具体操作:
# 根据已有配置安装全部依赖
npm install
# 添加运行时依赖
npm install dayjs
# 添加开发依赖
npm install eslint --save-dev
# 执行脚本。npm start / npm test 是常用快捷方式
npm run start
# 发布自己的包。需要先登录,且包名不能冲突
npm publish
还有几个同样常用、但图里没有单独画出的命令:
| 命令 | 适合场景 |
|---|---|
npm init -y | 生成一份初始 package.json |
npm ci | 在 CI 或部署环境按 lock 文件做干净安装 |
npm uninstall <包名> | 卸载依赖,并同步修改 package.json |
npm update | 在现有版本范围内更新依赖 |
npx <命令> | 运行某个包提供的命令,不一定先全局安装 |
npm run 之所以重要,是因为它把“这个项目怎么启动、怎么测试、怎么打包”写成了可分享的命令,而不是散落在每个人自己的备忘录里。克隆仓库的人只要安装依赖,再运行约定好的脚本即可。
什么是全局安装?
全局安装指的是 npm install -g(也可以写成 npm install --global)。它装的不是某个项目的依赖,而是装到系统或当前用户能访问的位置,让命令在任何目录都能调用。

两者的差别很直接:
| 方式 | 安装位置 | 谁能用 | 版本跟谁走 |
|---|---|---|---|
| 项目依赖 | 当前项目的 node_modules | 这个项目 | package.json / lock 文件 |
| 全局安装 | 系统或用户级目录 | 终端里的任意目录 | 你这台电脑当前装的那个版本 |
脚手架命令、本机常用的开发工具,历史上经常用 -g 安装。但这有明确边界:
- 项目里的构建和检查工具,例如 ESLint、TypeScript、打包器,应作为项目依赖安装。这样同事克隆仓库后能用同一版本,CI 也不依赖某台电脑的全局环境。
- 只想临时跑一次的命令,优先用
npx,不必先全局安装。 - 不要用
sudo npm install -g来绕过权限错误。 这会把包装到系统目录,增加权限和安全风险。更稳妥的做法是修复 npm 的安装前缀,或使用 nvm 这类 Node 版本管理器。
一句话:项目能跑起来的依赖放进项目;只有你确定要在所有项目外使用的命令行工具,才考虑全局安装。
npm 的完整工作链路是怎样的?
把前面的概念串起来,npm 的工作链路是固定的:公共仓库提供包,命令行负责下载和管理,package.json 声明需求,lock 文件锁定精确版本,node_modules 存放实际代码,最终自动搭好依赖树。

一次典型安装会经过这些步骤:
- 公共仓库汇集包:开发者把可复用代码发布到注册表。
- 命令行发起安装:你在项目目录执行
npm install或npm install <包名>。 - 读取声明:CLI 查看
package.json里要哪些包、版本范围是什么。 - 对照快照:如果存在
package-lock.json,优先按其中的精确版本解析,保证结果可复现。 - 写入本地目录:把包解压到
node_modules,必要时更新 lock 文件。 - 展开依赖树:递归安装间接依赖,直到整棵树完整。
所以你只需要一行命令,背后那棵可能有几十甚至几百个包的依赖树就会被自动装好。日常开发看到的是命令本身;工程上真正重要的,是声明、锁定和安装结果被分成了不同文件。
npm、Yarn 和 pnpm 有什么区别?
npm 是 Node.js 默认附带的包管理器,但不是唯一选择。Yarn 和 pnpm 也能读取 package.json、安装依赖并生成 lock 文件,只是默认策略不同。
| 维度 | npm | Yarn | pnpm |
|---|---|---|---|
| 是否随 Node.js 安装 | 是 | 否 | 否 |
| lock 文件 | package-lock.json | yarn.lock | pnpm-lock.yaml |
node_modules 形态 | 默认扁平安装 | 因 Yarn 版本而异 | 通过内容寻址仓库和链接安装,结构更严格 |
| 常见选择理由 | 官方默认,教程和示例最多 | 历史项目、部分团队的 workspace 工作流 | 节省磁盘、减少“幽灵依赖” |
| 适合场景 | 大多数个人项目和入门教程 | 已经使用 Yarn 的代码库 | 依赖多、对安装一致性和磁盘占用更敏感的项目 |
三者都在解决同一类问题:声明依赖、锁定版本、安装依赖树。对新手,先把 npm 用熟即可。团队一旦选定工具,就应只保留一种 lock 文件,不要在同一个项目里混用 npm install、yarn 和 pnpm install,否则 lock 文件会互相覆盖,安装结果变得难以预测。
使用 npm 时有哪些常见误区?
npm 能自动安装依赖,并不代表它可以替你判断包是否安全、版本是否兼容,或项目在每台机器上都会以相同方式启动。
| 误区 | 真实情况 | 更稳妥的做法 |
|---|---|---|
把 node_modules 提交进 Git | 目录很大,而且可以根据声明重新生成 | 加入 .gitignore,提交 package.json 和 lock 文件 |
不提交 package-lock.json | 下次安装可能解析到范围内的新版本 | 应用项目提交 lock 文件,CI 使用 npm ci |
| 把项目工具装到全局 | 同事和 CI 没有同一版本,脚本可能跑不起来 | 项目依赖 + npx 或 npm run |
用 sudo npm install -g 解决权限 | 可能改写系统目录,扩大安全风险 | 修复 npm 权限,或使用 Node 版本管理器 |
| 删除 lock 文件再安装来“修复问题” | 等于放弃已验证的依赖树 | 先看报错和 npm ls,必要时再有针对地更新 |
| 只按下载量选包 | 下载量不能代表维护状态或安全性 | 看最近提交、issue、许可证,并关注安全公告 |
| 同一个项目混用多个包管理器 | lock 文件冲突,依赖树不一致 | 团队只保留一种工具和一份 lock 文件 |
还要注意:语义化版本是约定,不是物理定律。包作者可能把不兼容改动发在小版本里,间接依赖也可能引入漏洞。npm audit 能列出已知漏洞,但不能替代你对直接依赖的选择和测试。
常见问题
npm 是什么?
npm 是 Node.js 生态的包管理器,也常用来指它背后的公共软件仓库。它负责安装 JavaScript 包、解析依赖树、锁定精确版本,并支持把包发布给其他人使用。
npm 和 Node.js 是什么关系?
Node.js 是 JavaScript 的运行时,负责执行代码;npm 是默认随 Node.js 一起提供的包管理器,负责获取和安排这些代码所依赖的包。没有 Node.js,npm 管理的包通常无法运行;没有包管理器,Node.js 项目很难稳定地复用现有生态。
package.json 和 package-lock.json 有什么区别?
package.json 记录项目信息和允许的依赖版本范围;package-lock.json 记录某次安装实际解析到的精确版本和依赖树。前者说明需求,后者保证别人安装时能复现同一棵树。应用项目通常两份文件都要提交。
node_modules 要不要提交到 Git?
通常不要。node_modules 体积大、文件多,而且可以用 package.json 加 lock 文件重新安装出来。应该提交的是声明和锁文件,而不是安装结果。例外很少,例如某些离线交付场景会单独打包依赖,那也不等于把它当作常规 Git 内容。
^ 和 ~ 有什么区别?
以 1.2.3 为例,^1.2.3 允许安装 1.2.3 到 2.0.0 之前的版本;~1.2.3 只允许安装 1.2.3 到 1.3.0 之前的版本。^ 更宽松,~ 更保守。主版本为 0 时,^ 的实际范围会更窄。
什么时候用全局安装,什么时候改用 Yarn 或 pnpm?
全局安装适合你想在任意目录调用、且不属于某个项目锁定版本的命令行工具。项目自己的依赖和构建工具应安装在项目内。至于 Yarn 或 pnpm,它们不是 npm 的必需替代品;如果团队已有约定、或者你明确需要 pnpm 那种更严格的依赖结构,再切换,并且全项目统一使用。
总结:npm 不只是一个下载工具
npm 是 Node.js 生态里的包管理器。它把可复用代码集中到公共仓库,用命令行帮你安装和管理,用 package.json 声明需求,用 lock 文件冻结精确版本,再用 node_modules 存放实际代码。

对个人项目,它让你用一行命令接上现成的函数库和工具;对团队项目,它让依赖声明可审查、安装结果可复现,减少“在我电脑上是好的”这类分歧。理解 npm,不只是记住 npm install,而是理解声明、锁定、安装和依赖树之间的分工。
所以,npm 并不只是一个下载工具。它是 Node 生态里的公共代码市场,是项目的依赖调度中心,也是保证团队环境一致的关键一环。理解了它,你才算真正踏进 JavaScript 工程化的门槛。
记住这一点:你声明的是需要哪些包,npm 负责把整棵依赖树装好,并用 lock 文件保证下次还能装成一样的结果。

