前言
据了解,大多数前端开发人员对 npm 的使用就只是为了让项目跑起来,很少人有正经的学一下 npm 到底是什么。其实我也一样,所以我就趁春节期间,好好学一学 npm。本文是我通过学习官方文档,结合之前所学,再加上源码的阅读,综合下来所写的我对 npm 的理解。
npm 由三个独立的部分组成:网站、注册表、命令行工具
- 网站:提供可视化视图界面,用于资料查阅
- 注册表:为整个社区生态提供数据支撑
- 命令行工具:为项目开发提供了一系列资源管理的工具
命令行工具又包含很多内容:
- 命令行工具提供了大量的配置参数,要学会如何修改配置
- 命令行工具要在项目中使用,需要 package.json 文件,需要知道 package.json 有哪些内容
- 在项目中安装模块,会引发很多,需要知道安装过程中经历了什么,模块间存放位置如何组织的,为什么需要 package-lock.json 文件
- 开发模块和安装模块都需要关注版本号,需要知道版本号该如何书写
以上提到的内容都是本文需要讲解的知识点,下面开启正文。
npm 网站

用来查找对应的模块的信息,可以进 网站 搜索,如果知道模块名称,可以直接拼上链接,规则是:
https://www.npmjs.com/package/ + 模块名
例如 react 模块就是:https://www.npmjs.com/package/react
默认显示的是最新版本,如果想要指定其他版本,可以在后面加上
/v/+ 版本号,例如:https://www.npmjs.com/package/react/v/16.8.6/v/+ 标签名,例如:https://www.npmjs.com/package/react/v/latest
如果是淘宝镜像地址,格式是如下:
https://developer.aliyun.com/mirror/npm/package/+ 模块名指定版本地址同理
注意:淘宝里的信息不一定是最新的,每 10 分钟自动同步一次。如果发现不是最新的,可以手动点下
SYNC
另外除了 npm 有淘宝镜像地址,github 也同样有,只要在域名后面加上
.cnpmjs.org即可。例如https://github.com/facebook/react改成https://github.com.cnpmjs.org/facebook/react
npm 注册表
注册表是一个巨大的数据库,保存了每个模块的信息。
注册表也可以看作一系列接口,用来获取每一个模块的信息,接口访问格式是:
https://registry.npmjs.com/ + 模块名
比如 react 模块信息,可以访问 https://registry.npmjs.com/react,就可能拿到该模块所有信息数据。

从这些数据里看找到模块的下载地址,在指定版本下的 dist -> tarball

模块下载路径有一个通用的格式,我们是可以直接得出下载地址的,格式如下:
https://registry.npmjs.com/[module]/-/[module]-[version].tgz
其中 [module] 是模块名称,[version] 是模块版本,例如:
https://registry.npmjs.org/react/-/react-16.8.6.tgz
如果是淘宝镜像,格式是如下:
https://registry.npm.taobao.org/+ 模块名
https://registry.npm.taobao.org/[module]/download/[module]-[version].tgz
对于一些二进制模块,例如 node、node-sass 等,针对于这类模块,淘宝镜像提供了另一个下载地址,格式如下:
https://npm.taobao.org/mirrors/ + 模块名
进入后就能根据版本号找到对应的下载包。

不过这边并不是所有模块都有,如果没内容就是没有。
命令行工具
命令行工具又称为 CI,通过命令行或终端运行,从而触发某些文件的执行。
命令行工具分为两部分:内置命令行、npm scripts
内置命令行
这块建议查官方文档,这边就只是做了分类整理,放在另外一篇文档里,请移至 《npm 内置命令行整理》
npm scripts
什么是 npm scripts?
npm scripts 指的是 package.json 文件里配置的 scripts,例如:
{
"scripts": {
"start": "webpack-dev-server --mode=development --open",
"build": "webpack --mode=production",
"lint": "eslint --ext js,vue src"
}
}这边 scripts 下定义了 start、build、lint 三个属性,对应三条命令行语句,这就相当于定义了三条命令行,这三条命令行就属于 npm scripts。
npm scripts 的出现为开发者提供了自定义命令行的途径,将一些常用的命令行封装起来,并取一个更具业务或功能含义的名字。
定义好命令行后通过以下命令行来执行:
$ npm run <command>
# 或者
$ npm run-script <command>这边的 <command> 传入命令行名称,例如执行 lint :
$ npm run lint其他相关命令行
对于一些常用的命令,npm 提供了一些简写写法,可以省去 run :
npm run start->npm startnpm run build->npm buildnpm run stop->npm stopnpm run test->npm test
npm run 是针对于当前项目的 package.json 文件,如果想运行 node_modules 里某个模块的 npm scripts,就需要借助另外一个命令:
$ npm explore <module> -- npm run <command>生命周期
npm scripts 每一条命令行都有对应的生命周期,例如 scripts 这样定义:
{
"scripts": {
"prestart": "",
"start": "",
"poststart": ""
}
}这时候执行 npm run start 会经历这样的过程:
npm run prestartnpm run startnpm run poststart
执行 start 之前会先执行 prestart,执行 start 之后还会执行 poststart 。每条命令行都遵守这样的执行规则,都有 pre 和 post。prestart 自身也有生命周期,也有自己的 preprestart 和 postprestart。不过只会执行当前命令行的 prev 和 post ,不会触发 prev 和 post 自身的生命周期。只有当你手动执行 prestart 时,才会触发 preprestart 的执行。
如果 npm script 没有定义 prestart 和 poststart ,就不做任何操作。
除了 pre 和 post ,npm scripts 还内置了几个特殊的生命周期钩子。
prepare- 执行
npm pack之前 - 执行
npm publish之前 - 执行
npm install之前 - 触发
prepublishOnly之前,prepublish之后
- 执行
prepublish已废弃,请使用prepareprepublishOnly- 执行
npm publish之前
- 执行
postpublish- 执行
npm publish之后
- 执行
prepack- 执行
npm pack之前 - 执行
npm publish之前
- 执行
postpack- 执行
npm pack之后
- 执行
执行
npm run pack与执行npm pack不同,前者是执行 npm scripts 定义的命令,后者是执行 npm 内置命令
以上的生命周期是执行内置命令行时触发的 npm scripts。通常内置命令行不会触发 npm scripts,只有个别特殊的命令才会,下列是这些特殊命令行触发的生命周期执行顺序:
npm publishprepublishprepareprepublishOnlyprepackpostpackpublishpostpublish
npm packprepublishprepareprepackpostpack
npm installpreinstallinstallpostinstallprepublishpreparepreshrinkwrapshrinkwrappostshrinkwrap
npm restartprerestartprestopstoppoststoprestartprestartstartpoststartpostrestart
以上是从项目的角度看,如果是作为模块,被项目安装,例如执行 npm install react。身为模块的 react 被安装,会先触发模块自身的生命周期,先卸载后安装:
preuninstalluninstallpostuninstallpreinstallinstallpostinstall
运行环境
npm scripts 与终端所处的运行环境有所不同,例如:
{
"scripts": {
"start": "webpack-dev-server --mode=development --open"
}
}$ npm run start在 npm scripts 上定义后,再用 npm run 去执行,跟直接在终端执行:
$ webpack-dev-server --mode=development --open这两者是有区别的。在 npm scripts 能执行,在终端并不一定能执行,因为这两者所处的环境不同,接下来来分析下为什么不同。
执行这样一条命令行,其实是去查找名为 webpack-dev-server 的可执行文件并执行它。查找的方式就是去查找环境变量 PATH,环境变量 PATH 上记录着一个个目录,从这一个个目录里找到对应的可执行文件。
不同系统的可执行文件类型不一样。通常在 Window 系统里是查找 cmd 文件,在 Linux 或 Mac 系统里是查找软链接或者直接使用 JS 文件。
不同系统查找环境变量 PATH 的方式也不一样:
- Window 系统的 cmd 程序或者 npm scripts 里是执行
set PATH - Window 系统的 PowerShell 里是执行
$env:PATH - Linux 和 Mac 系统是执行
echo $PATH
Window 返回的是这样一串字符串,存储着一个个目录地址,目录之间用分号隔开
C:\Program Files\PowerShell\7;C:\Program Files\Microsoft VS Code\bin;C:\Program Files\Common Files\Autodesk Shared\;D:\apache-maven-3.2.5\bin;C:\Program Files\Java\jdk1.8.0_181\bin;C:\Program Files\TortoiseSVN\bin;C:\Python27;C:\Program Files\nodejs\;C:\Program Files (x86)\Yarn\bin\;C:\Program Files\Microsoft VS Code\bin;C:\Users\jencia\AppData\Roaming\npm;C:\Users\jencia\AppData\Local\Yarn\binLinux 和 Mac 系统返回的也是一串字符串存储着一个个目录地址,不一样的是目录之间是用冒号隔开的
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin以上是在终端上输出的结果。如果是在 npm scripts 执行,结果就不一样了,比如 npm scripts 这样定义:
{
"scripts": {
"env": "echo $PATH"
}
}假设这是在 Mac 系统环境,执行 npm run env 的结果会变成这样:
/usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/Users/jencia/Workspace/test/node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin多了两个目录(Window 系统也是):
/usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin/Users/jencia/Workspace/test/node_modules/.bin
npm scripts 内部构建了一个局部环境变量,局部环境变量的 PATH 的前面加入这两个目录。 执行 npm scripts 的命令行时都处在这个局部环境变量下。这两个目录里存的东西就是 npm scripts 和终端执行时运行环境的差异。
第一个目录地址是全局 npm 安装目录下 npm-lifecycle 模块里的其中一个目录。这目录下只有两个文件:node-gyp 和 node-gyp.cmd。cmd 后缀的是 window 里使用,没有后缀名的是 Linux 和 Mac 里使用的,其实就是一个文件。也就是说加入这个目录,就是为了使用 node-gyp 命令。这里不深究 node-gyp 的作用,这边需要用到这个命令是因为当你执行 npm install 的时候内部会去执行 node-gyp rebuild。
第二个目录是当前项目下的 ./node_modules/.bin 目录,每个项目的 node_modules 下都会有一个 .bin 目录,里面存放的一些可执行文件,这些可执行文件是可以在 npm scripts 里直接以命令行的方式使用的。至于这些可执行文件如何产生的,在安装模块时,有些模块是命令行模块,安装完后就会在 ./node_modules/.bin 目录里生成对应的可执行文件。以这种方式管理命令行工具,而不是直接使用全局命令行,是为了保证项目团队每个人都是用同一个版本的命令,避免出现不一样的执行结果。
环境变量
上一节讲了 npm scripts 会有一个局部环境变量,除了 PATH 不一样,还存放着一些其他的 npm 相关的环境变量,用于在 NodeJS 代码里读取使用。读取的方式如下:
{
"scripts": {
"start": "node index.js"
}
}$ npm run start// index.js
console.log(process.env);这边通过 npm scripts 使用 node 去运行 index.js 文件,这时候将打印出了所有环境变量。
在终端直接运行 node index.js 也可以打印出来,只是缺少 npm 相关环境变量。
这个局部环境变量主要加入了三块数据:包数据、配置数据、生命周期数据。
1. 包数据 package.json 文件里的所有内容,都会转化为数据,例如:
{
"name": "my-project",
"keywords": ["111", "222", "333"],
"config": {
"test": "aaaaa"
},
"scripts": {
"start": "node index.js",
},
"devDependencies": {
"webpack-cli": "^4.5.0"
}
}转化后的数据:
{
npm_package_name: 'my-project',
npm_package_keywords_0: '111',
npm_package_keywords_1: '222',
npm_package_keywords_2: '333',
npm_package_config_test: 'aaaaa'
npm_package_scripts_start: 'node index.js',
npm_package_devDependencies_webpack_cli: '^4.5.0',
}全部以 npm_package_ 开头,转成扁平化数据,单词之间用下划线连接,中划线都被转为下划线。
2. 配置数据
npm 的配置有很多种方式,下一章节的 配置参数 将详细讲解,这边就以 .npmrc 为例讲解。
假设 .npmrc 是这样:
aaa=111
bbb=222转化后的数据:
{
npm_config_aaa: '111',
npm_config_bbb: '222'
}全部以 npm_config_ 开头,拼上配置项名称,值都为字符串。这边只举例两条配置项,实际会有很多,包括全局配置、命令行参数配置。
3. 生命周期数据
这块只有两条数据:
npm_lifecycle_event当前触发事件的名称npm_lifecycle_script当前触发事件对应的命令行
配置参数
配置参数的作用主要有这几方面:
- npm 内部使用了配置参数,添加配置可修改默认参数,比如切换镜像源
- 部分第三方模块也配置参数,可用于定制化配置,比如
node-sass更换二进制文件下载地址 - 团队开发使用同一套配置参数可以统一开发环境
添加配置参数有好几种方式,下列按照优先级由高到低讲解:
命令行添加参数
{
"scripts": {
"start": "node index.js"
}
}$ npm run start --test1='hello world' --test-2// index.js
console.log(process.env.npm_config_test1) // 'hello world'
console.log(process.env.npm_config_test_2) // true以上就是在命令行加参数的例子,其中有几个需要注意的地方:
- 加参数是在执行
npm run的时候加,而不是在 npm scripts 上面加 - 以
--代表一个参数,后面紧跟上参数名 - 参数值不写代表
'true',如果有参数值,必须加上等号(官网文档写错了),等号前后不能有空格 - 获取数据时都要在参数名前面加上
npm_config_,如果参数名有横杆,会被转为下划线
设置环境变量
环境变量里以 npm_config_ 或 NPM_CONFIG_ 开头的都会认为是配置参数,不管大小写都会统一转成小写,横杆转为下划线。如何设置环境变量不讲了,就像系统的那个环境变量,网上自己查,只要名字满足要求就好。
项目根目录创建 .npmrc 文件
aaa='111'
bbb=222
cccconsole.log(process.env.npm_config_aaa); // '111'
console.log(process.env.npm_config_bbb); // '222'
console.log(process.env.npm_config_ccc); // 'true'修改用户目录 .npmrc 文件
Window 系统在 c:\Users\用户名\.npmrc,Linux 和 Mac 系统在 ~/.npmrc
也可以通过命令行管理:
# 设置配置
$ npm config set aaa '111'
# 查看配置
$ npm config get aaa
# 删除配置
$ npm config delete aaa这命令修改的就是用户目录的 .npmrc 文件。
修改全局配置文件
文件位置在 $PREFIX/etc/npmrc,其中 $PREFIX 可以通过命令行查看:
$ npm config get prefixWindow 系统一般找不到这个路径,需要自己修改 globalconfig 参数设置全局配置文件的位置。globalconfig 本身就是一个配置参数,按照上述那几种方式选一种设置就好了。
npm 本身提供了很多默认配置,各个配置的作用和默认值,建议查阅官方文档 Config Settings 章节。
版本管理
npm 内部使用的版本管理是引用 semver 模块
npm 的版本遵守统一的规则:
<major>.<minor>.<patch>
<major>.<minor>.<patch>-<prerelease>
<major>.<minor>.<patch>-<alpha|beta|rc>.<prerelease>
例如:
1.5.2
1.5.3-2
1.5.3-alpha.5major主版本号,破坏性改动,与旧版本不兼容minor次版本号,新功能改动,能兼容旧版本patch修订版本号,问题修复和优化,能兼容旧版本alpha内测版本,给内部人员测试使用的版本,问题可能很多,可能会频繁变动beta公测版本,开放给公众人员使用,但仍然可能出现很多问题rc预览版本,比beta更稳定,功能不会再增加prerelease预发布版本号
版本必须包含 major、minor、patch 三部分,第一个版本号从 1.0.0 开始。
alpha、beta、rc 可选,属于预发布标识。prerelease 通常是有预发布标识才需要加上。
以上是版本的书写规则,至于版本管理,分为两方面:项目版本管理和模块依赖版本
项目版本管理
这边的项目版本指的是 package.json 文件的 version 属性:
{
"name": "module-name",
"version": "1.0.0",
"author": "jencia",
// ...
}一般只有在开发模块的时候才需要关心版本号,发布模块的时候就是以这个版本号进行发布。这边版本号可以手动修改,但不建议这样做,建议用命令行 npm version 升级版本。
语法:
npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]常用的就这几种:
- 升级主版本号
$ npm version major- 升级次版本号
$ npm version minor- 升级修订版本号
$ npm version patch- 标记为预发布版本并升级
$ npm version prerelease --preid=alpha如果项目结构组织是采用 monorepos 就不适合这种方式,建议使用 Lerna 管理
模块依赖版本
项目开发中通常会使用第三方模块,也就会依赖其他模块,依赖的模块都会记录在 package.json 的 dependencies 或 devDependencies ,例如:
{
"dependencies": {
"axios": "^0.21.0",
"core-js": "^3.6.5",
"dayjs": "^1.9.7",
"element-ui": "^2.14.1",
"qs": "^6.9.4",
"store": "^2.0.12",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^8.4.2",
"vue-router": "^3.2.0",
"vuex": "^3.4.0",
"wangeditor": "^4.6.3"
}
}通常会以 ^x.x.x 的方式写,版本号前面会加上 ^ ,这表示主版本号不升级,次版本号和修订版本号安装最新版本。当然写法不只这一种,下面将列出所有情况:
固定版本
1.5.2->1.5.2=1.5.2->1.5.2v1.5.2->1.5.2
使用
>、>=、<、<=范围版本>1.5.1-> 大于1.5.1>=1.5.1-> 大于等于1.5.1<1.5.1-> 小于1.5.1<=1.5.1-> 小于等于1.5.1>1.5.1 <1.7.0-> 大于1.5.1小于1.7.01.5.1 || >1.7.0-> 等于1.5.1或大于1.7.0
使用
-、*、x1.2.3 - 2.3.4->>=1.2.3 <=2.3.41.2 - 2.3.4->>=1.2.0 <=2.3.41.2.3 - 2.3->>=1.2.3 <2.4.01.2.3 - 2->>=1.2.3 <3.0.0*->>=0.0.01.x->>=1.0.0 <2.0.01.2.x->>=1.2.0 <1.3.0""(空字符串) ->*->>=0.0.01->1.x.x->>=1.0.0 <2.0.01.2->1.2.x->>=1.2.0 <1.3.0
使用
^、~~1.2.3->>=1.2.3 <1.3.0-> 只升级修订版本号^1.2.3->>=1.2.3 <2.0.0-> 只升级次版本号和修订版本号
对于预发布版本有几个值得注意的点:
~1.2.3-beta.2->>=1.2.3-beta.2 <1.3.0^1.2.3-beta.2->>=1.2.3-beta.2 <2.0.0
拿第一个来说,大于 1.2.3-beta.2 <1.3.0 的条件,1.2.9 能满足条件,但如果是 1.2.9-beta.6 就不能满足条件。对于带有预发布版本标识的只能是 1.2.3 下的,比如 1.2.3-beta.8 能满足。这个语句更加准确的描述应该是 >=1.2.3-beta.3 <1.2.4 || >=1.2.4 <1.3.0
未完待续...

