与你项目相关的npm知识总结

每次克隆下别人的代码后,执行的第一步就是npm install安装依赖包,安装成功后全部的包都会放在项目的node_modules文件夹下,也会自动生成package-lock.json文件。有没有好奇过node_modules下的文件都是啥?package-lock.json文件的做用是啥?前端

本文主要解决如下几个问题:vue

  1. package.json中的dependenciesdevDependencies的区别是啥,peerDependenciesbundledDependenciesoptionalDependencies又是啥?
  2. 为何有的命令写在package.json中的script中就能够执行,可是经过命令行直接执行就不行?
  3. 为何须要package-lock.json文件?
  4. 一个包在项目中有可能须要不一样的版本,最后安装到根目录node_modules中的具体是哪一个版本?

带着这几个问题,咱们先从package.json文件提及。node

package.json

最靠谱的官方文档请点这里ios

官方文档中列出了好多属性,感兴趣的能够一个个看一遍。下面只列出其中几个比较经常使用且重要的属性。web

name & version

若是想要发布一个npm包,nameversion属性是必须的。他们两个组合会造成一个惟一的标识来表名当前包。之后每更新一次包,version就须要进行相应的更改。若是你不打算发布包,只想在本地使用,这两个字段不是必须的。算法

name字段命名的规则以下:shell

  • 长度不能超过214个字符(对于有scoped的包,该限制包括scoped字段)(什么是Scoped packages?
  • 有做用域的包名字能够以.或者_开头,没有做用域限制的不可
  • 不能含有大写字母
  • 不能含有非URL安全的字符

version字段npm

版本号须要符合semver(语义化版本号)规则,具体版本格式为:主版本号.次版本号.修订号, 如1.1.0。json

  • 主版本号(major):作了不兼容的 API 修改
  • 次版本号(minor):作了向下兼容的功能性新增
  • 修订号(patch):作了向下兼容的问题修正

当有一些先行版本须要发布时,能够在主版本号.次版本号.修订号以后加上一个中划线和标识符如alpha(内部版本)、beta(公测版本)、rc(候选版本)等来代表。axios

以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
相关文章
相关标签/搜索