1、先弄清楚npm到底解决什么问题
Node.js生态里,npm是随运行时一起提供的默认包管理器。它承担两件事:
第一,包管理。把项目需要的第三方库下载到本地,记录版本,后续可以更新、删除、还原。
第二,包注册表。npmregistry上存放了大量开源JavaScript包,开发者既能下载,也能发布自己的包。
很多初学者会误以为npm是一个独立工具,需要单独安装。实际上安装Node.js时npm已经捆绑在内,终端执行npm-v能输出版本号就说明环境可用。这一步看似简单,却是后面所有操作的前提。
我在项目里遇到过一种情况:同事本地Node.js版本过旧,npm版本停留在6.x,执行npminstall时锁文件解析行为和新版本不一致,导致CI上装出来的依赖树不同。后来统一用npm-v和node-v做环境检查,才把这类问题压下去。所以版本确认不是形式,而是排查依赖问题的起点。
2、安装一个包:从命令到node_modules
安装包的基础命令:
npm install <package-name>
以Express为例:
npm install express
执行后npm会做几件事:解析Express的版本范围,下载包本身及其依赖,写入node_modules目录,同时在package.json的dependencies中登记,并更新package-lock.json。
node_modules是本地依赖的落地目录。它的结构可能嵌套较深,因为不同包可能依赖不同版本的同一个库。npm从v3开始尽量扁平化,但冲突时仍会嵌套。不建议手动修改node_modules里的文件,因为下一次npminstall可能覆盖。
搜索包可以用:
npm search express
查看npm版本:
npm -v
安装指定版本:
npm install express@4.18.2
卸载本地包:
npm uninstall express
卸载全局包:
npm uninstall -g <package-name>
这里有个容易踩的坑:在项目目录里执行npmuninstall-gexpress,删掉的是系统全局的Express,而不是当前项目的。反过来,在全局环境下执行不带-g的卸载,又删不到项目依赖。判断依据很简单,看命令有没有-g,以及当前终端所在目录是不是项目根目录。
3、本地包与全局包:边界要分清
本地安装:
npm install <package-name>
本地包只属于当前项目,会写入package.json的dependencies。例如:
"dependencies": {
"ejs": "^3.1.9",
"is-odd": "^3.0.1",
"react": "^18.2.0",
"tailwindcss": "^3.3.5"
}
全局安装:
npm install -g <package-name>
全局包安装在系统级目录,所有项目都能调用,但不会出现在项目的package.json或package-lock.json里。查看全局包列表:
npm list -g --depth=0
我的建议是:项目运行时真正依赖的库,一律本地安装;只有命令行工具类、且多个项目都会用到的包,才考虑全局。原因是全局包不受项目锁文件约束,换一台机器或换一个Node.js版本,全局环境未必一致。曾经有项目依赖全局的nodemon,结果新同事机器上没装,启动命令直接报错。后来把nodemon放进devDependencies,用npxnodemon或npmscripts调用,问题才消失。
4、package.json:依赖清单与项目
package.json可以手动创建,也可以用npm生成:
npm init
交互式回答问题后生成。想跳过提问:
npm init -y
拿到一个已有项目,通常先执行:
npm install
npm会读取package.json中列出的依赖,把它们安装到node_modules。这是克隆仓库后恢复开发环境的标准动作。
package.json里有两个关键字段:dependencies和devDependencies。
dependencies是生产环境运行必需的包,比如Express、Mongoose。安装方式:
npm install express
对应:
{
"dependencies": {
"express": "^5.1.0"
}
}
devDependencies是只在开发阶段需要的包,比如nodemon、测试框架、构建工具。安装方式:
npm install nodemon --save-dev
对应:
{
"devDependencies": {
"nodemon": "^3.1.0"
}
}
为什么要把它们分开?因为部署到生产环境时,可以用npminstall--production或npmci--omit=dev跳过开发依赖,减少安装体积和攻击面。把测试工具、热重载工具塞进dependencies,会让生产镜像无谓变大,也增加依赖审计的噪声。
一个实际反思:早期做项目时图省事,所有包都用npminstall装,结果dependencies里混进了eslint、jest、nodemon。后来做Docker镜像,发现生产层装了一堆根本用不到的东西。重新梳理依赖归属后,镜像体积下降明显,构建时间也缩短了。依赖分类不是洁癖,它直接影响部署效率。
5、npmscripts:把重复命令收进项目
package.json的scripts字段用来定义自定义命令。示例:
{
"name": "example",
"scripts": {
"test": "echo 'hello world'"
}
}
运行:
npm run test
输出:
> example@1.0.0 test
> echo "hello world"
hello world
这个机制的价值在于:把测试、构建、启动、代码检查等重复操作固化到项目里。团队成员不需要记住一长串参数,执行npmrundev或npmrunbuild即可。CI环境也能直接复用同一套命令,减少“本地能跑、线上跑不起来”的偏差。
常见脚本命名约定:
{
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"test": "jest",
"build": "tsc"
}
}
start和test是npm的默认脚本名,可以省略run,直接npmstart、npmtest。其他脚本名需要npmrun<script-name>。
6、npm与npx的区别:什么时候用哪个
npm是包管理器,负责安装、更新、删除依赖,管理package.json和package-lock.json。
npx是包执行器,负责直接运行某个包的命令,不要求全局安装。
对比项:
| npm | npx |
|---|---|
| 安装、更新、管理依赖 | 直接执行包,无需全局安装 |
| 包需要先安装才能使用 | 本地没有时自动下载并执行 |
| 全局包必须先安装 | 不依赖全局安装 |
| 用于增删改package.json依赖 | 用于一次性命令,不改动package.json |
| 通过package.json定义和运行脚本 | 适合临时执行create-react-app、eslint等 |
npx从npm5.2.0开始随npm一起提供。典型场景:想快速创建一个React项目,不想全局安装create-react-app,直接:
npx create-react-app my-app
执行完脚手架任务后,npx不会在全局留下这个包。对于只跑一次的命令,这种方式比“先全局安装、再执行、再卸载”干净得多。
但要注意:项目里长期使用的工具,仍然应该放进devDependencies,用npmscripts或npx调用本地版本。npx自动下载的版本可能不是项目锁定的版本,存在行为差异风险。我的做法是:一次性脚手架用npx,项目内工具走本地依赖加scripts。
7、本节课程知识要点
-
npm随Node.js一起安装,
npm-v可确认版本。 -
npminstall<package-name>安装本地包,写入dependencies。 -
npminstall<package-name>--save-dev安装开发依赖,写入devDependencies。 -
npminstall-g<package-name>安装全局包,不进入项目package.json。 -
npminstall不带包名,用于根据package.json还原全部依赖。 -
npmuninstall删除本地包,npmuninstall-g删除全局包。 -
npminit-y快速生成默认package.json。 -
npmrun<script-name>执行scripts中定义的命令。 -
npx用于一次性执行包命令,不替代npm的依赖管理职责。
-
生产依赖与开发依赖要分开,部署时可跳过
devDependencies。
8、项目实例:一个Express项目的依赖管理流程
假设用代码号学习编程的方式创建一个Node.js项目,目录为codehao-express-demo。
第一步,初始化:
mkdir codehao-express-demo
cd codehao-express-demo
npm init -y
第二步,安装生产依赖Express:
npm install express
第三步,安装开发依赖nodemon:
npm install nodemon --save-dev
第四步,查看package.json:
{
"name": "codehao-express-demo",
"version": "1.0.0",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
第五步,创建app.js:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('codehao express demo');
});
app.listen(3000, () => {
console.log('server running at http://localhost:3000');
});
第六步,开发时运行:
npm run dev
生产启动:
npm start
第七步,模拟新环境还原依赖。删除node_modules后执行:
npm install
npm会根据package.json和package-lock.json重新安装依赖。package-lock.json锁定了具体版本和依赖树,保证不同机器装出一致的结果。这也是为什么锁文件应该提交到版本库。
踩坑提醒:不要随意删除package-lock.json。有些开发者遇到依赖冲突时直接删锁文件再npminstall,短期能跑通,但版本漂移可能引入不可复现的问题。更稳妥的做法是先看npmls<package-name>定位冲突来源,再决定是升级、降级还是用overrides处理。
9、关于npm的使用时机
以下场景适合使用npm:
-
需要为项目安装本地依赖或全局命令行工具。
-
需要把依赖版本记录到
package.json,保证团队和CI一致。 -
长期项目,会反复使用同一批库和工具。
-
需要通过
scripts统一测试、构建、启动流程。
如果只是临时跑一个包的命令,不想污染全局环境,也不想改动项目依赖,优先考虑npx。
10、依赖版本符号的简单说明
package.json中常见版本前缀:
-
^5.1.0:允许更新到5.x.x范围内的较新版本,不跨主版本。 -
~5.1.0:允许更新到5.1.x范围内的较新版本。 -
5.1.0:锁定确切版本。
^在多数项目里是默认行为,兼顾了修复更新和主版本稳定性。但对稳定性要求高的项目,可以结合package-lock.json和npmci来保证每次安装一致。npmci直接按锁文件安装,不修改package.json,适合CI环境。
11、常见问题与处理思路
问题一:npminstall很慢或卡住。
先检查网络和registry配置。国内环境可以切换镜像源,但要注意镜像同步延迟。团队项目建议统一registry配置,避免有人用默认源、有人用镜像源导致锁文件差异。
问题二:权限报错。
全局安装时遇到EACCES,不要直接sudonpminstall-g。更合理的方式是配置npm的全局目录到用户目录下,或者用nvm管理Node.js,从根源上避开系统目录权限问题。
问题三:依赖冲突。
用npmls<package-name>查看依赖树,确认是哪个包引入了冲突版本。能升级就升级,不能升级再用overrides强制指定版本。不要一上来就删node_modules和锁文件。
问题四:全局包在脚本里调用不到。
npmscripts执行时会优先使用本地node_modules/.bin下的命令。如果工具只装在全局,脚本里可能找不到。解决办法是把工具加入devDependencies,让npmscripts使用本地版本。
npm的核心价值不只是“下载包”,而是把依赖版本、安装位置、脚本命令和项目配置统一到package.json与package-lock.json中。本地依赖与全局依赖的边界、生产依赖与开发依赖的划分、npm与npx的分工,这三组概念理清楚,日常开发中的依赖问题会少很多。把常用命令收进scripts,把版本锁定交给锁文件,把一次性命令交给npx,项目环境就会更可控。