每次克隆下他人的代码后,执行的第一步就是npm install装置依赖包,装置胜利后所有的包都会放在我的项目的node_modules文件夹下,也会主动生成package-lock.json文件。有没有好奇过node_modules下的文件都是啥?package-lock.json文件的作用是啥?

本文次要解决以下几个问题:

  1. package.json中的dependenciesdevDependencies的区别是啥,peerDependenciesbundledDependenciesoptionalDependencies又是啥?
  2. 为什么有的命令写在package.json中的script中就能够执行,然而通过命令行间接执行就不行?
  3. 为什么须要package-lock.json文件?
  4. 一个包在我的项目中有可能须要不同的版本,最初装置到根目录node_modules中的具体是哪个版本?

带着这几个问题,咱们先从package.json文件说起。

package.json

最靠谱的官网文档请点这里

官网文档中列出了好多属性,感兴趣的能够一个个看一遍。上面只列出其中几个比拟罕用且重要的属性。

name & version

如果想要公布一个npm包,nameversion属性是必须的。他们两个组合会造成一个惟一的标识来表名以后包。当前每更新一次包,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具体的阐明能够看这里

dependencies & devDependencies

dependenciesdevDependencies大家应该都不生疏,通过npm install xx --save装置的包会写入dependencies中,通过npm install xx --save-dev装置的包会写入devDependencies

dependencies中的包是生产环境的依赖,属于线上代码的一部分,比方vueaxiosveui等。devDependencies中的包是开发环境的依赖,只是在本地开发的时候须要依赖这里的包,比方 vue-loadereslint等。

咱们平时用的npm install命令既会装置dependencies中的包,也会装置devDependencies中的包。如果只想装置dependencies中包,能够应用npm install --production或者将NODE_ENV环境变量设置为production,通常在生成环境咱们会这么用。

须要留神的是,一个模块会不会被打包取决于咱们在我的项目中是否引入了该模块,跟该模块放在dependencies中还是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" }

这表明如果你想要应用veui1.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文件。该文件中蕴含renderizedsuper-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,则会列出当前目录下可执行的所有脚本。

teststartrestartstop这几个命令执行时能够不加run,间接npm testnpm startnpm restartnpm 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

对于npmpackage.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其中的一个依赖lodashlodash:^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文件,就能保障团队每个人装置的包版本都是雷同的,不会呈现有些包降级造成我这好使他人那不好使的兼容性问题。

上面是lesspackage-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,表明此包如果不是顶层模块的一个开发依赖(写在devDependencies中),就是一个传递依赖(如下面less中的copy-anything)。
  • requires: 对应子依赖的依赖,与依赖包的package.jsondependencies的依赖项雷同
  • 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 and move

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_modulesD@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.jsonpackage-lock.json,将以npm-shrinkwrap.json为主。

本文只是一些实践根底,之后会介绍一些npm源码相干的常识。

参考文章

  1. npm官网
  2. 前端工程化 - 分析npm的包管理机制
  3. 前端工程化(5):你所须要的npm常识储备都在这了
  4. semver