**npm管理机制**
Nodejs
成功离不开 npm
优秀的依赖管理系统。在介绍整个依赖系统以前,必需要了解 npm
如何管理依赖包的版本,本章将介绍 npm包
的版本发布规范、如何管理各类依赖包的版本以及一些关于包版本的最佳实践。html
你能够执行 npm view package version
查看某个 package
的最新版本。node
执行 npm view conard versions
查看某个 package
在npm服务器上全部发布过的版本。react
执行 npm ls
可查看当前仓库依赖树上全部包的版本信息。git
npm包
中的模块版本都须要遵循 SemVer
规范——由 Github
起草的一个具备指导意义的,统一的版本号表示规则。实际上就是 Semantic Version
(语义化版本)的缩写。github
SemVer规范官网: semver.org/docker
SemVer
规范的标准版本号采用 X.Y.Z
的格式,其中 X、Y 和 Z 为非负的整数,且禁止在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每一个元素必须以数值来递增。npm
major
):当你作了不兼容的API 修改minor
):当你作了向下兼容的功能性新增patch
):当你作了向下兼容的问题修正。例如:1.9.1 -> 1.10.0 -> 1.11.0
json
当某个版本改动比较大、并不是稳定并且可能没法知足预期的兼容性需求时,你可能要先发布一个先行版本。缓存
先行版本号能够加到“主版本号.次版本号.修订号”的后面,先加上一个链接号再加上一连串以句点分隔的标识符和版本编译信息。安全
alpha
):beta
):rc
: 即 Release candiate
下面咱们来看看 React
的历史版本:
可见是严格按照 SemVer
规范来发版的:
主版本号.次版本号.修订号
格式命名16.8.0 -> 16.8.1 -> 16.8.2
alpha
、beta
、rc
等先行版本在修改 npm
包某些功能后一般须要发布一个新的版本,咱们一般的作法是直接去修改 package.json
到指定版本。若是操做失误,很容易形成版本号混乱,咱们能够借助符合 Semver
规范的命令来完成这一操做:
npm version patch
: 升级修订版本号npm version minor
: 升级次版本号npm version major
: 升级主版本号在开发中确定少不了对一些版本号的操做,若是这些版本号符合 SemVer
规范 ,咱们能够借助用于操做版本的npm包semver
来帮助咱们进行比较版本大小、提取版本信息等操做。
Npm 也使用了该工具来处理版本相关的工做。
npm install semver
复制代码
semver.gt('1.2.3', '9.8.7') // false
semver.lt('1.2.3', '9.8.7') // true
复制代码
semver.valid('1.2.3') // '1.2.3'
semver.valid('a.b.c') // null
复制代码
semver.valid(semver.coerce('v2')) // '2.0.0'
semver.valid(semver.coerce('42.6.7.9.3-alpha')) // '42.6.7'
复制代码
semver.clean(' =v1.2.3 ') // '1.2.3'
semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3') // true
semver.minVersion('>=1.0.0') // '1.0.0'
复制代码
以上都是semver最多见的用法,更多详细内容能够查看 semver文档:github.com/npm/node-se…
咱们常常看到,在 package.json
中各类依赖的不一样写法:
"dependencies": {
"signale": "1.4.0",
"figlet": "*",
"react": "16.x",
"table": "~5.4.6",
"yargs": "^14.0.0"
}
复制代码
前面三个很容易理解:
"signale": "1.4.0"
: 固定版本号"figlet": "*"
: 任意版本(>=0.0.0
)"react": "16.x"
: 匹配主要版本(>=16.0.0 <17.0.0
)"react": "16.3.x"
: 匹配主要版本和次要版本(>=16.3.0 <16.4.0
)再来看看后面两个,版本号中引用了 ~
和 ^
符号:
~
: 当安装依赖时获取到有新版本时,安装到 x.y.z
中 z
的最新的版本。即保持主版本号、次版本号不变的状况下,保持修订号的最新版本。^
: 当安装依赖时获取到有新版本时,安装到 x.y.z
中 y
和 z
都为最新版本。 即保持主版本号不变的状况下,保持次版本号、修订版本号为最新版本。在 package.json
文件中最多见的应该是 "yargs": "^14.0.0"
这种格式的 依赖, 由于咱们在使用 npm install package
安装包时,npm
默认安装当前最新版本,而后在所安装的版本号前加 ^
号。
注意,当主版本号为 0
的状况,会被认为是一个不稳定版本,状况与上面不一样:
0
: ^0.0.z
、~0.0.z
都被看成固定版本,安装依赖时均不会发生变化。0
: ^0.y.z
表现和 ~0.y.z
相同,只保持修订号为最新版本。1.0.0 的版本号用于界定公共 API。当你的软件发布到了正式环境,或者有稳定的API时,就能够发布1.0.0版本了。因此,当你决定对外部发布一个正式版本的npm包时,把它的版本标为1.0.0。
实际开发中,常常会由于各类依赖不一致而产生奇怪的问题,或者在某些场景下,咱们不但愿依赖被更新,建议在开发中使用 package-lock.json
。
锁定依赖版本意味着在咱们不手动执行更新的状况下,每次安装依赖都会安装固定版本。保证整个团队使用版本号一致的依赖。
每次安装固定版本,无需计算依赖版本范围,大部分场景下能大大加速依赖安装时间。
使用 package-lock.json 要确保npm的版本在5.6以上,由于在5.0 - 5.6中间,对 package-lock.json的处理逻辑进行过几回更新,5.6版本后处理逻辑逐渐稳定。
关于 package-lock.json
详细的结构,咱们会在后面的章节进行解析。
咱们的目的是保证团队中使用的依赖一致或者稳定,而不是永远不去更新这些依赖。实际开发场景下,咱们虽然不须要每次都去安装新的版本,仍然须要定时去升级依赖版本,来让咱们享受依赖包升级带来的问题修复、性能提高、新特性更新。
使用 npm outdated
能够帮助咱们列出有哪些尚未升级到最新版本的依赖:
执行 npm update
会升级全部的红色依赖。
1.0.0
。主版本号.次版本号.修订号
格式命名alpha、beta、rc
等先行版本npm
包,此时建议把版本前缀改成~
,若是锁定的话每次子依赖更新都要对主工程的依赖进行升级,很是繁琐,若是对子依赖彻底信任,直接开启^
每次升级到最新版本。docker
线上,本地还在进行子依赖开发和升级,在docker
版本发布前要锁定全部依赖版本,确保本地子依赖发布后线上不会出问题。npm
的版本在5.6
以上,确保默认开启 package-lock.json
文件。npm inatall
后,将 package-lock.json
提交到远程仓库。不要直接提交 node_modules
到远程仓库。npm update
升级依赖,并提交 lock
文件确保其余成员同步更新依赖,不要手动更改 lock
文件。package.json
文件的依赖版本,执行 npm install
npm install package@version
(改动package.json
不会对依赖进行降级)lock
文件npm install
大概会通过上面的几个流程,这一章就来说一讲各个流程的实现细节、发展以及为什么要这样实现。
咱们都知道,执行 npm install
后,依赖包被安装到了 node_modules
,下面咱们来具体了解下,npm
将依赖包安装到 node_modules
的具体机制是什么。
在 npm
的早期版本, npm
处理依赖的方式简单粗暴,以递归的形式,严格按照 package.json
结构以及子依赖包的 package.json
结构将依赖安装到他们各自的 node_modules
中。直到有子依赖包不在依赖其余模块。
举个例子,咱们的模块 my-app
如今依赖了两个模块:buffer
、ignore
:
{
"name": "my-app",
"dependencies": {
"buffer": "^5.4.3",
"ignore": "^5.1.4",
}
}
复制代码
ignore
是一个纯 JS
模块,不依赖任何其余模块,而 buffer
又依赖了下面两个模块:base64-js
、 ieee754
。
{
"name": "buffer",
"dependencies": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
}
复制代码
那么,执行 npm install
后,获得的 node_modules
中模块目录结构就是下面这样的:
这样的方式优势很明显, node_modules
的结构和 package.json
结构一一对应,层级结构明显,而且保证了每次安装目录结构都是相同的。
可是,试想一下,若是你依赖的模块很是之多,你的 node_modules
将很是庞大,嵌套层级很是之深:
Windows
系统中,文件路径最大长度为260个字符,嵌套层级过深可能致使不可预知的问题。为了解决以上问题,NPM
在 3.x
版本作了一次较大更新。其将早期的嵌套结构改成扁平结构:
node_modules
根目录。仍是上面的依赖结构,咱们在执行 npm install
后将获得下面的目录结构:
此时咱们若在模块中又依赖了 base64-js@1.0.1
版本:
{
"name": "my-app",
"dependencies": {
"buffer": "^5.4.3",
"ignore": "^5.1.4",
"base64-js": "1.0.1",
}
}
复制代码
node_modules
下安装该模块。此时,咱们在执行 npm install
后将获得下面的目录结构:
对应的,若是咱们在项目代码中引用了一个模块,模块查找流程以下:
node_modules
路径下搜素node_modules
路径下搜索node_modules
假设咱们又依赖了一个包 buffer2@^5.4.3
,而它依赖了包 base64-js@1.0.3
,则此时的安装结构是下面这样的:
因此 npm 3.x
版本并未彻底解决老版本的模块冗余问题,甚至还会带来新的问题。
试想一下,你的APP假设没有依赖 base64-js@1.0.1
版本,而你同时依赖了依赖不一样 base64-js
版本的 buffer
和 buffer2
。因为在执行 npm install
的时候,按照 package.json
里依赖的顺序依次解析,则 buffer
和 buffer2
在 package.json
的放置顺序则决定了 node_modules
的依赖结构:
先依赖buffer2
:
先依赖buffer
:
另外,为了让开发者在安全的前提下使用最新的依赖包,咱们在 package.json
一般只会锁定大版本,这意味着在某些依赖包小版本更新后,一样可能形成依赖结构的改动,依赖结构的不肯定性可能会给程序带来不可预知的问题。
为了解决 npm install
的不肯定性问题,在 npm 5.x
版本新增了 package-lock.json
文件,而安装方式还沿用了 npm 3.x
的扁平化的方式。
package-lock.json
的做用是锁定依赖结构,即只要你目录下有 package-lock.json
文件,那么你每次执行 npm install
后生成的 node_modules
目录结构必定是彻底相同的。
例如,咱们有以下的依赖结构:
{
"name": "my-app",
"dependencies": {
"buffer": "^5.4.3",
"ignore": "^5.1.4",
"base64-js": "1.0.1",
}
}
复制代码
在执行 npm install
后生成的 package-lock.json
以下:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"base64-js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz",
"integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg="
},
"buffer": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
"integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
},
"dependencies": {
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
}
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"ignore": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
"integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A=="
}
}
}
复制代码
咱们来具体看看上面的结构:
最外面的两个属性 name
、version
同 package.json
中的 name
和 version
,用于描述当前包名称和版本。
dependencies
是一个对象,对象和 node_modules
中的包结构一一对应,对象的 key
为包名称,值为包的一些描述信息:
version
:包版本 —— 这个包当前安装在 node_modules
中的版本resolved
:包具体的安装来源integrity
:包 hash
值,基于 Subresource Integrity
来验证已安装的软件包是否被改动过、是否已失效requires
:对应子依赖的依赖,与子依赖的 package.json
中 dependencies
的依赖项相同。dependencies
:结构和外层的 dependencies
结构相同,存储安装在子依赖 node_modules
中的依赖包。这里注意,并非全部的子依赖都有 dependencies
属性,只有子依赖的依赖和当前已安装在根目录的 node_modules
中的依赖冲突以后,才会有这个属性。
例如,回顾下上面的依赖关系:
咱们在 my-app
中依赖的 base64-js@1.0.1
版本与 buffer
中依赖的 base64-js@^1.0.2
发生冲突,因此 base64-js@1.0.1
须要安装在 buffer
包的 node_modules
中,对应了 package-lock.json
中 buffer
的 dependencies
属性。这也对应了 npm
对依赖的扁平化处理方式。
因此,根据上面的分析, package-lock.json
文件 和 node_modules
目录结构是一一对应的,即项目目录下存在 package-lock.json
可让每次安装生成的依赖目录结构保持相同。
另外,项目中使用了 package-lock.json
能够显著加速依赖安装时间。
咱们使用 npm i --timing=true --loglevel=verbose
命令能够看到 npm install
的完整过程,下面咱们来对比下使用 lock
文件和不使用 lock
文件的差异。在对比前先清理下npm
缓存。
不使用 lock
文件:
使用 lock
文件:
可见, package-lock.json
中已经缓存了每一个包的具体版本和下载连接,不须要再去远程仓库进行查询,而后直接进入文件完整性校验环节,减小了大量网络请求。
开发系统应用时,建议把 package-lock.json
文件提交到代码版本仓库,从而保证全部团队开发者以及 CI
环节能够在执行 npm install
时安装的依赖版本都是一致的。
在开发一个 npm
包 时,你的 npm
包 是须要被其余仓库依赖的,因为上面咱们讲到的扁平安装机制,若是你锁定了依赖包版本,你的依赖包就不能和其余依赖包共享同一 semver
范围内的依赖包,这样会形成没必要要的冗余。因此咱们不该该把package-lock.json
文件发布出去( npm
默认也不会把 package-lock.json
文件发布出去)。
在执行 npm install
或 npm update
命令下载依赖后,除了将依赖包安装在node_modules
目录下外,还会在本地的缓存目录缓存一份。
经过 npm config get cache
命令能够查询到:在 Linux
或 Mac
默认是用户主目录下的 .npm/_cacache
目录。
在这个目录下又存在两个目录:content-v2
、index-v5
,content-v2
目录用于存储 tar
包的缓存,而index-v5
目录用于存储tar
包的 hash
。
npm 在执行安装时,能够根据 package-lock.json
中存储的 integrity、version、name
生成一个惟一的 key
对应到 index-v5
目录下的缓存记录,从而找到 tar
包的 hash
,而后根据 hash
再去找缓存的 tar
包直接使用。
咱们能够找一个包在缓存目录下搜索测试一下,在 index-v5
搜索一下包路径:
grep "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz" -r index-v5
复制代码
而后咱们将json格式化:
{
"key": "pacote:version-manifest:https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz:sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=",
"integrity": "sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==",
"time": 1575554308857,
"size": 1,
"metadata": {
"id": "base64-js@1.0.1",
"manifest": {
"name": "base64-js",
"version": "1.0.1",
"engines": {
"node": ">= 0.4"
},
"dependencies": {},
"optionalDependencies": {},
"devDependencies": {
"standard": "^5.2.2",
"tape": "4.x"
},
"bundleDependencies": false,
"peerDependencies": {},
"deprecated": false,
"_resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.1.tgz",
"_integrity": "sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=",
"_shasum": "6926d1b194fbc737b8eed513756de2fcda7ea408",
"_shrinkwrap": null,
"bin": null,
"_id": "base64-js@1.0.1"
},
"type": "finalized-manifest"
}
}
复制代码
上面的 _shasum
属性 6926d1b194fbc737b8eed513756de2fcda7ea408
即为 tar
包的 hash
, hash
的前几位 6926
即为缓存的前两层目录,咱们进去这个目录果真找到的压缩后的依赖包:
以上的缓存策略是从 npm v5 版本开始的,在 npm v5 版本以前,每一个缓存的模块在 ~/.npm 文件夹中以模块名的形式直接存储,储存结构是{cache}/{name}/{version}。
npm
提供了几个命令来管理缓存数据:
npm cache add
:官方解释说这个命令主要是 npm
内部使用,可是也能够用来手动给一个指定的 package 添加缓存。npm cache clean
:删除缓存目录下的全部数据,为了保证缓存数据的完整性,须要加上 --force
参数。npm cache verify
:验证缓存数据的有效性和完整性,清理垃圾数据。基于缓存数据,npm 提供了离线安装模式,分别有如下几种:
--prefer-offline
: 优先使用缓存数据,若是没有匹配的缓存数据,则从远程仓库下载。--prefer-online
: 优先使用网络数据,若是网络数据请求失败,再去请求缓存数据,这种模式能够及时获取最新的模块。--offline
: 不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败。上面咱们屡次提到了文件完整性,那么什么是文件完整性校验呢?
在下载依赖包以前,咱们通常就能拿到 npm
对该依赖包计算的 hash
值,例如咱们执行 npm info
命令,紧跟 tarball
(下载连接) 的就是 shasum
(hash
) :
用户下载依赖包到本地后,须要肯定在下载过程当中没有出现错误,因此在下载完成以后须要在本地在计算一次文件的 hash
值,若是两个 hash
值是相同的,则确保下载的依赖是完整的,若是不一样,则进行从新下载。
好了,咱们再来总体总结下上面的流程:
检查 .npmrc
文件:优先级为:项目级的 .npmrc
文件 > 用户级的 .npmrc
文件> 全局级的 .npmrc
文件 > npm 内置的 .npmrc
文件
检查项目中有无 lock
文件。
无 lock
文件:
从 npm
远程仓库获取包信息
根据 package.json
构建依赖树,构建过程:
构建依赖树时,无论其是直接依赖仍是子依赖的依赖,优先将其放置在 node_modules
根目录。
当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,若是符合则跳过,不符合则在当前模块的 node_modules
下放置该模块。
注意这一步只是肯定逻辑上的依赖树,并不是真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包
在缓存中依次查找依赖树中的每一个包
不存在缓存:
从 npm
远程仓库下载包
校验包的完整性
校验不经过:
从新下载
校验经过:
将下载的包复制到 npm
缓存目录
将下载的包按照依赖结构解压到 node_modules
存在缓存:将缓存按照依赖结构解压到 node_modules
将包解压到 node_modules
生成 lock
文件
有 lock
文件:
检查 package.json
中的依赖版本是否和 package-lock.json
中的依赖有冲突。
若是没有冲突,直接跳过获取包信息、构建依赖树过程,开始在缓存中查找包信息,后续过程相同
上面的过程简要描述了 npm install
的大概过程,这个过程还包含了一些其余的操做,例如执行你定义的一些生命周期函数,你能够执行 npm install package --timing=true --loglevel=verbose
来查看某个包具体的安装流程和细节。
yarn
是在 2016
年发布的,那时 npm
还处于 V3
时期,那时候尚未 package-lock.json
文件,就像上面咱们提到的:不稳定性、安装速度慢等缺点常常会受到广大开发者吐槽。此时,yarn
诞生:
上面是官网提到的 yarn
的优势,在那个时候仍是很是吸引人的。固然,后来 npm
也意识到了本身的问题,进行了不少次优化,在后面的优化(lock
文件、缓存、默认-s...)中,咱们多多少少能看到 yarn
的影子,可见 yarn
的设计仍是很是优秀的。
yarn
也是采用的是 npm v3
的扁平结构来管理依赖,安装依赖后默认会生成一个 yarn.lock
文件,仍是上面的依赖关系,咱们看看 yarn.lock
的结构:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
base64-js@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.0.1.tgz#6926d1b194fbc737b8eed513756de2fcda7ea408"
integrity sha1-aSbRsZT7xze47tUTdW3i/Np+pAg=
base64-js@^1.0.2:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
buffer@^5.4.3:
version "5.4.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115"
integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
ignore@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
复制代码
可见其和 package-lock.json
文件仍是比较相似的,还有一些区别就是:
package-lock.json
使用的是 json
格式,yarn.lock
使用的是一种自定义格式yarn.lock
中子依赖的版本号不是固定的,意味着单独又一个 yarn.lock
肯定不了 node_modules
目录结构,还须要和 package.json
文件进行配合。而 package-lock.json
只须要一个文件便可肯定。yarn
的缓策略看起来和 npm v5
以前的很像,每一个缓存的模块被存放在独立的文件夹,文件夹名称包含了模块名称、版本号等信息。使用命令 yarn cache dir
能够查看缓存数据的目录:
yarn
默认使用prefer-online
模式,即优先使用网络数据,若是网络数据请求失败,再去请求缓存数据。