来源:阳呀呀
https://segmentfault.com/a/1190000039289332
每次克隆下别人的代码后,执行的第一步就是npm install
安装依赖包,安装成功后所有的包都会放在项目的node_modules
文件夹下,也会自动生成package-lock.json
文件。有没有好奇过node_modules
下的文件都是啥?package-lock.json
文件的作用是啥?
本文主要解决以下几个问题:
package.json
中的dependencies
和devDependencies
的区别是啥,peerDependencies
、bundledDependencies
、optionalDependencies
又是啥?- 为什么有的命令写在
package.json
中的script
中就可以执行,但是通过命令行直接执行就不行? - 为什么需要
package-lock.json
文件? - 一个包在项目中有可能需要不同的版本,最后安装到根目录
node_modules
中的具体是哪个版本?
带着这几个问题,我们先从package.json
文件说起。
package.json
最靠谱的官方文档请点这里:https://docs.npmjs.com/cli/v6/configuring-npm/package-json#people-fields-author-contributors
官方文档中列出了好多属性,感兴趣的可以一个个看一遍。下面只列出其中几个比较常用且重要的属性。
name & version
如果想要发布一个npm
包,name
和version
属性是必须的。他们两个组合会形成一个唯一的标识来表名当前包。以后每更新一次包,version
就需要进行相应的更改。如果你不打算发布包,只想在本地使用,这两个字段不是必须的。
name
字段命名的规则如下:
- 长度不能超过214个字符(对于有scoped的包,该限制包括scoped字段)(什么是Scoped packages?)
- 有作用域的包名字可以以.或者_开头,没有作用域限制的不可
- 不能含有大写字母
- 不能含有非URL安全的字符
version
字段
版本号需要符合semver
(语义化版本号)规则,具体版本格式为:主版本号.次版本号.修订号
, 如1.1.0。
- 主版本号(major):做了不兼容的 API 修改
- 次版本号(minor):做了向下兼容的功能性新增
- 修订号(patch):做了向下兼容的问题修正
当有一些先行版本需要发布时,可以在主版本号.次版本号.修订号
之后加上一个中划线和标识符如alpha(内部版本)、beta(公测版本)、rc(候选版本)等来表明。
以vue的版本为例:
- 最新的稳定版本:3.0.5
- 最新的rc版本:3.0.0-rc.13
- 最新的beta版本:3.0.0-beta.24
- 最新的alpha版本:3.0.0-alpha.13
可以通过npm install semver
来检查一个包的命名是否符合semver
规则。有关semver具体的说明可以看这里:https://docs.npmjs.com/cli/v6/using-npm/semver
dependencies & devDependencies
dependencies
和devDependencies
大家应该都不陌生,通过npm install xx --save
安装的包会写入dependencies
中,通过npm install xx --save-dev
安装的包会写入devDependencies
。
dependencies
中的包是生产环境的依赖,属于线上代码的一部分,比如vue
、axios
、veui
等。devDependencies
中的包是开发环境的依赖,只是在本地开发的时候需要依赖这里的包,比如 vue-loader
、eslint
等。
我们平时用的npm install
命令既会安装dependencies
中的包,也会安装devDependencies
中的包。如果只想安装dependencies
中包,可以使用npm install --production
或者将NODE_ENV
环境变量设置为production
,通常在生成环境我们会这么用。
需要注意的是,一个模块会不会被打包取决于我们在项目中是否引入了该模块,跟该模块放在dependencies
中还是devDependencies
并没有关系。
对于我们的项目来说,把用到的包写在dependencies
或者devDependencies
并没有什么区别。但要是做为一个包发到npm上时,写在devDependencies
中的依赖不会被下载。
peerDependencies & bundledDependencies & optionalDependencies
这三个属性在平时我们的项目开发中都用不到。不同于dependencies
& devDependencies
面向的是包的使用者,peerDependencies
& optionalDependencies
& bundledDependencies
这三个属性是面向包的发布者。
peerDependencies
我们在一些node_modules
包的package.json
中可以看到peerDependencies
,它用来表明如果你想要使用此插件,此插件要求宿主环境所安装的包。比如项目中用到的veui1.0.0-alpha.24
版本中:
"peerDependencies": {
"vue": "^2.5.16"
}
这表明如果你想要使用veui
的1.0.0-alpha.24
版本,所要求的vue
版本需要满足>=2.5.16
且<3.0.0
。
在npm3.x
以上版本中,如果安装结束后宿主环境没有满足peerDependencies
中的要求,会在控制台打印出警告信息。
bundledDependencies
当我们想在本地保留一个npm
完整的包或者想生成一个压缩文件来获取npm
包的时候,会用到bundledDependencies
。本地使用npm pack
打包时会将bundledDependencies
中依赖的包一同打包,当npm install
时相应的包会同时被安装。需要注意的是,bundledDependencies
中的包不应该包含具体的版本信息,具体的版本信息需要在dependencies
中指定。
例如一个package.json
文件如下:
{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized",
"super-streams"
]
}
当我们执行npm pack
后会生成awesome-web-framework-1.0.0.tgz
文件。该文件中包含renderized
和super-streams
这两个依赖,当执行npm install awesome-web-framework-1.0.0.tgz
下载包时,这两个依赖会被安装。
当我们使用npm publish
来发布包的话,这个属性不会起作用。
optionalDependencies
从名字上就可以看出,这是可选依赖。如果有包写在optionalDependencies
中,即使npm
找不到或者安装失败了也不会影响安装过程。需要注意的是,optionalDependencies
中的配置会覆盖dependencies
中的配置,所以不要将同一个包同时放在这两个里面。
如果使用了optionalDependencies
,一定记得要在项目中做好异常处理,获取不到的情况下应该怎么办。
scripts
定义在scripts
中的命令,我们通过npm run <command>
就可以执行。npm run <command>
是npm run-script <command>
的简写。如果不加command
,则会列出当前目录下可执行的所有脚本。
test
、start
、restart
、stop
这几个命令执行时可以不加run
,直接npm test
、npm start
、npm restart
、npm stop
调用即可。
env
是一个内置的命令,可以通过npm run env
可以获取到脚本运行时的所有环境变量。自定义的env
命令会覆盖内置的env
命令。
之前开发中遇到一种情况,比如我们想本地通过http-server
启动一个服务器,如果事先没有全局安装过http-server
包,只是安装在对应项目的node_modules
中。在命令行中输入http-server
会报command not found
,但是如果我们在scripts
中增加如下一条命令就可以执行成功。
scripts: {
"server": "http-server",
"eslint": "eslint --ext .js"
}
为什么同样的命令写在scripts
中就可以成功,但是在命令行中执行就不行呢?这是因为npm run
命令会将node_modules/.bin/
加入到shell
的环境变量PATH
中,这样即使局部安装的包也可以直接执行而不用加node_modules/.bin/
前缀。当执行结束后,再将其删除。
是不是还是没明白,下面我们来具体分析一下。
首先要明确什么是环境变量。环境变量就是系统在执行一个程序,但是没有明确表明该程序所在的完整路径时,需要去哪里寻找该程序。
对于局部安装的包,拿eslint
来说,npm
会在本地项目./node_modules/.bin
目录下创建一个指向./node_moudles/eslint/bin/eslint.js
名为eslint
的软链接,即执行./node_modules/.bin/eslint
实际上是执行./node_moudles/eslint/bin/eslint.js
。而当我们执行npm run eslint
的时候,node_modules/.bin/
会被加入到环境变量PATH
中,实际上执行的是./node_modules/.bin/eslint
,这样就串起来了。
理论说完之后,我们来实际验证一下。
首先看一下系统的环境变量。直接执行env
即可。
然后在当前项目目录下通过npm run env
查看脚本运行时的环境变量。
通过对比可以发现,运行时的PATH
多了两个环境变量。即npm
指令的路径和项目/node_modules/.bin
的路径。
以上就是package.json
中常用 & 重要的几个属性,接下来我们来看一看package-lock.json
。
package-lock.json
对于npm
,package.json
文件可以看成它的输入,node_modules
可以做为它的输出。在理想情况下,npm
应该是一个纯函数,无论何时执行相同的package.json
文件都应该产生完全相同的node_modules
树。在一些情况下,这确实可以做到。但是在大多情况下,都实现不了。主要有以下几个原因:
- 使用者的
npm
版本有可能不同,不同的npm
版本有着不同的安装算法 - 自上次安装之后,有些符合
semver-range
的包已经有新的版本发布。这样再有别人安装的时候,会安装符合要求的最新版本。比如引入vue
包:vue:^2.6.1
。A小伙伴下载的时候是2.6.1
,过一阵有另一个小伙伴B入职在安装包的时候,vue
已经升级到2.6.2
,这样npm
就会下载2.6.2
的包安装在他的本地 - 针对第二点,一个解决办法是固定自己引入的包的版本,但是通常我们不会这么做。即使这样做了,也只能保证自己引入的包版本固定,也无法保证包的依赖的升级。比如
vue
其中的一个依赖lodash
,lodash:^4.17.4
,A下载的是4.17.4
, B下载的时候有可能已经升级到了4.17.21
为了解决上述问题,npm5.x
开始增加了package-lock.json
文件。每当npm install
执行的时候,npm
都会产生或者更新package-lock.json
文件。package-lock.json
文件的作用就是锁定当前的依赖安装结构,与node_modules
中下所有包的树状结构一一对应。
有了这个package-lock.json
文件,就能保证团队每个人安装的包版本都是相同的,不会出现有些包升级造成我这好使别人那不好使的兼容性问题。
下面是less
的package-lock.json
文件结构:
"less": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/less/-/less-3.13.1.tgz",
"integrity": "sha512-SwA1aQXGUvp P5XdZslUOhhLnClSLIjWvJhmd Vgib5BFIr9lMNlQwmwUNOjXThF/A0x MCYYPeWEfeWiLRnTw==",
"dev": true,
"requires": {
"copy-anything": "^2.0.1",
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"native-request": "^1.0.5",
"source-map": "~0.6.0",
"tslib": "^1.10.0"
},
dependencies: {
"copy-anything": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz",
"integrity": "sha512-GK6QUtisv4fNS XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi D2o7HxJFPnDQ==",
"dev": true,
"requires": {
"is-what": "^3.12.0"
}
}
}
}
- version: 包的版本信息
- resoloved: 包的安装源
- integrity:一个
hash
值,用来校验包的完整性 - dev:布尔值,如果为
true
,表明此包如果不是顶层模块的一个开发依赖(写在devDependencie
s中),就是一个传递依赖(如上面less
中的copy-anything
)。 - requires: 对应子依赖的依赖,与依赖包的
package.json
中dependencies
的依赖项相同 - dependencies:结构与外层结构相同,存在于包自己的
node_modules
中的依赖(不是所有的包都有,当子依赖的依赖版本与根目录的node_modules
中的依赖冲突时,才会有)
通过分析上面的package-lock.json
文件,也许会有一个问题。为什么有的包可以被安装在根目录的node_modules
中,有的包却只能安装在自己包下面的node_modules
中?这就涉及到npm
的安装机制。
npm从3.x
开始,采用了扁平化的方式来安装node_modules
。在安装时,npm会遍历整个依赖树,不管是项目的直接依赖还是子依赖的依赖,都会优先安装在根目录的node_modules
中。遇到相同名称的包,如果发现根目录的node_modules
中存在但是不符合semver-range
,会在子依赖的node_modules
中安装符合条件的包。
具体的安装算法如下:
- 从磁盘加载
node_modules
树 - 克隆
node_modules
树 - 获取
package.json
文件和分类完毕的元数据信息并把元数据信息插入到克隆树中 - 遍历克隆树,检测是否有丢失的依赖。如果有,把他们添加到克隆树中,依赖会尽可能的添加到最高层
- 比较原始树和克隆树,列出将原始树转换为克隆树所要采取的具体步骤
- 执行,包括
install
,update
,remove
andmove
以npm
官网的例子举例,假设package{dep}
结构代表包和包的依赖,现有如下结构:A{B,C}
, B{C}
, C{D}
,按照上述算法执行完毕后,生成的node_modules
结构如下:
A
-- B
-- C
-- D
对于B
,C
被安装在顶层很好理解,因为是A
的直接依赖。但是B
又依赖C
,安装C
的时候发现顶层已经有C
了,所以不会在B
自己的node_modules
中再次安装。C
又依赖D
,安装D
的时候发现根目录并没有D
,所以会把D
提升到顶层。
换成A{B,C}
, B{C,D@1}
, C{D@2}
这样的依赖关系后,产生的结构如下:
A
-- B
-- C
-- D@2
-- D@1
B
又依赖了D@1
,安装时发现根目录的node_modules
没有,所以会把D@1
安装在顶层。C
依赖了D@2
,安装D@2
时,因为npm
不允许同层存在两个名字相同的包,这样就与跟目录node_modules
的D@1
冲突,所以会把D@2
安装在C
自己的node_modules
中。
模块的安装顺序决定了当有相同的依赖时,哪个版本的包会被安装在顶层。首先项目中主动引入的包肯定会被安装在顶层,然后会按照包名称排序(a-z)进行依次安装,跟包在package.json
中写入的顺序无关。因此,如果上述将B{C,D@1}
换成E{C,D@1}
,那么D@2
将会被安装在顶层。
有一种情况,当我们项目中所引用的包版本较低,比如A{B@1,C}
,而C
所需要的是C{B@2}
版本,现在的结构应该如下:
A
-- B@1
-- C
-- B@2
有一天我们将项目中的B
升级到B@2
,理想情况下的结构应该如下:
A
-- B@2
-- C
但是现在package-lock.json
文件的结构却是这样的:
A
-- B@2
-- C
-- B@2
B@2
不仅存在于根目录的node_modules
下,C
下也同样存在。这时需要我们手动执行npm dedupe
进行去重操作,执行完成后会发现C
下面的B@2
会消失。大家可以在自己的项目中试一试,优化一下package-lock.json
文件的结构。
以下是在我的项目中执行npm dedupe
的结果:
removed 41 packages, moved 15 packages and audited 1994 packages in 18.538s
在npm5.x
之前,可以手动通过npm shrinkwrap
生成npm-shrinkwrap.json
文件,与package-lock.json
文件的作用相同。当项目中同时存在npm-shrinkwrap.json
和package-lock.json
,将以npm-shrinkwrap.json
为主。
执行npm dedupe
去重之后的node_modules
会瘦身一些,但做为一个有追求的程序员怎么能局限于仅仅瘦身呢,我们要紧跟时代的潮流,对一些过时的东西say no。这时, npm-outdated
命令就派上用场了。
npm-outdated
命令是用来检查项目中用到的包版本在当前是否已经过时。如果有过时的包,会在控制台打印出信息。默认情况下,只会列出项目中顶层依赖的过时信息。如果想要更深层的查看,可以加上depth
参数,如npm-outdated --depth=1
以下是在我的项目中执行npm-outdated
的部分结果。从结果中可以看到包的当前版本,符合semver-range
的最高版本以及当前的最新版本等信息。
Package Current Wanted Latest Location
animate.css 3.7.0 3.7.2 4.1.1 xxx
autoprefixer 9.7.6 9.8.6 10.2.5 xxx
axios 0.19.2 0.19.2 0.21.1 xxx
babel-eslint 7.2.3 7.2.3 10.1.0 xxx
babel-loader 7.1.5 7.1.5 8.2.2 xxx
有需求的小伙伴可以尝试把自己项目中用到的已经过时的包升级一下。
本文只是一些理论基础,之后会介绍一些npm
源码相关的知识。
参考文章
- npm官网
- 前端工程化 - 剖析npm的包管理机制
- 前端工程化(5):你所需要的npm知识储备都在这了
- semver