
作者:ConardLi
现如今,前端开发的同学已经离不开npm这个包管理工具,其优秀的包版本管理机制承载了整个繁荣发展的NodeJS社区,理解其内部机制非常有利于加深我们对模块开发的理解、各项前端工程化的配置以加快我们排查问题(相信不少同学收到过各种依赖问题的困扰)的速度。
本文从三个角度:、版本管理、依赖安装结合具体实例对npm的包管理机制进行了详细分析。

在中,模块是一个库或框架,也是一个项目。项目遵循模块化的架构,当我们创建了一个项目,意味着创建了一个模块,这个模块必须有一个描述文件,即。它是我们最常见的配置文件,但是它里面的配置你真的有详细了解过吗?配置一个合理的文件直接决定着我们项目的质量,所以首先带大家分析下的各项详细配置。
中有非常多的属性,其中必须填写的只有两个:name和version,这两个属性组成一个npm模块的唯一标识。
name即模块名称,其命名时需要遵循官方的一些规范和建议:
包名会成为模块url、命令行中的一个参数或者一个文件夹名称,任何非url安全的字符在包名中都不能使用,可以使用validate-npm-package-name包来检测包名是否合法。
语义化包名,可以帮助开发者更快的找到需要的包,并且避免意外获取错误的包。
若包名称中存在一些符号,将符号去除后不得与现有包名重复
例如:由于react-native已经存在,、reactnative都不可以再创建。
如果你的包名与现有的包名太相近导致你不能发布这个包,那么推荐将这个包发布到你的作用域下。
例如:用户名conard,那么作用域为@conard,发布的包可以是@conard/react。
name是一个包的唯一标识,不得和其他包名重复,我们可以执行npmviewpackageName查看包是否被占用,并可以查看它的一些基本信息:
若包名称从未被使用过,则会抛出404错误:
另外,你还可以去。
{"description":"Anenterprise-classUIdesignlanguageandReactcomponentsimplementation","keywords":["ant","component","components","design","framework","front","react","react-component","ui"]}
description用于添加模块的的描述信息,方便别人了解你的模块。
keywords用于给你的模块添加关键字。
当然,他们的还有一个非常重要的作用,就是利于模块检索。当你使用npmsearch检索模块时,会到description和keywords中进行匹配。写好description和keywords有利于你的模块获得更多更精准的曝光:
描述开发人员的字段有两个:author和contributors,author指标的主要作者,一个author对应一个人。contributors指贡献者信息,一个contributors对应多个贡献者,值为数组,对人的描述可以是一个字符串,也可以是下面的结构:
{"name":"ConardLi","email":"lisqPersion@163.com","url":""}
地址{"homepage":"","bugs":{"url":""},"repository":{"type":"git","url":""},}
homepage用于指定该模块的主页。
repository用于指定模块的代码仓库。
bugs指定一个地址或者一个邮箱,对你的模块存在疑问的人可以到这里提出问题。
我们的项目可能依赖一个或多个外部依赖包,根据依赖包的不同用途,我们将他们配置在下面几个属性下:depencies、devDepencies、peerDepencies、bundledDepencies、optionalDepencies。
在介绍几种依赖配置之前,首先我们来看一下依赖的配置规则,你看到的依赖包配置可能是下面这样的:
"depencies":{"antd":"ant-design/ant-designcommit-ish|commit-ishgit+ssh://user@hostname:
git+http://user@hostname/project/
depenciesdepencies指定了项目运行所依赖的模块,开发环境和生产环境的依赖模块都可以配置到这里,例如
"depencies":{"lodash":"^4.17.13","moment":"^2.24.0",}devDepencies有一些包有可能你只是在开发环境中用到,例如你用于检测代码规范的eslint,用于进行测试的jest,用户使用你的包时即使不安装这些依赖也可以正常运行,反而安装他们会耗费更多的时间和资源,所以你可以把这些依赖添加到devDepencies中,这些依赖照样会在你本地进行npminstall时被安装和管理,但是不会被安装到生产环境:
"devDepencies":{"jest":"^24.3.1","eslint":"^6.1.0",}peerDepenciespeerDepencies用于指定你正在开发的模块所依赖的版本以及用户安装的依赖包版本的兼容性。
上面的说法可能有点太抽象,我们直接拿ant-design来举个例子,ant-design的中有如下配置:
"peerDepencies":{"react":"=16.0.0","react-dom":"=16.0.0"}当你正在开发一个系统,使用了ant-design,所以也肯定需要依赖React。同时,ant-design也是需要依赖React的,它要保持稳定运行所需要的React版本是16.0.0,而你开发时依赖的React版本是15.x:
这时,ant-design要使用React,并将其引入:
import*asReactfrom'react';import*asReactDOMfrom'react-dom';
这时取到的是宿主环境也就是你的环境中的React版本,这就可能造成一些问题。在npm2的时候,指定上面的peerDepencies将意味着强制宿主环境安装react@=16.0.0和react-dom@=16.0.0的版本。
npm3以后不会再要求peerDepencies所指定的依赖包被强制安装,相反npm3会在安装结束后检查本次安装是否正确,如果不正确会给用户打印警告提示。
"depencies":{"react":"15.6.0","antd":"^3.22.0"}例如,我在项目中依赖了antd的最新版本,然后依赖了react的15.6.0版本,在进行依赖安装时将给出以下警告:
optionalDepencies某些场景下,依赖包可能不是强依赖的,这个依赖包的功能可有可无,当这个依赖包无法被获取到时,你希望npminstall继续运行,而不会导致失败,你可以将这个依赖放到optionalDepencies中,注意optionalDepencies中的配置将会覆盖掉depencies所以只需在一个地方进行配置。
当然,引用optionalDepencies中安装的依赖时,一定要做好异常处理,否则在模块获取不到时会导致报错。
bundledDepencies和以上几个不同,bundledDepencies的值是一个数组,数组里可以指定一些模块,这些模块将在这个包发布时被一起打包。
"bundledDepencies":["package1","package2"]
1.4协议{"license":"MIT"}license字段用于指定软件的开源协议,开源协议里面详尽表述了其他人获得你代码后拥有的权利,可以对你的的代码进行何种操作,何种操作又是被禁止的。同一款协议有很多变种,协议太宽松会导致作者丧失对作品的很多权利,太严格又不便于使用者使用及作品的传播,所以开源作者要考虑自己对作品想保留哪些权利,放开哪些限制。
以下就是几种主流的开源协议:
MIT:只要用户在项目副本中包含了版权声明和许可声明,他们就可以拿你的代码做任何想做的事情,你也无需承担任何责任。
Apache:类似于MIT,同时还包含了贡献者向用户提供专利授权相关的条款。
GPL:修改项目代码的用户再次分发源码或二进制代码时,必须公布他的相关修改。
如果你对开源协议有更详细的要求,可以到。
1.5目录、文件相关程序入口{"main":"lib/",}main属性可以指定程序的主入口文件,例如,上面antd指定的模块入口lib/,当我们在代码用引入antd时:import{notification}from'antd';实际上引入的就是lib/中暴露出去的模块。
命令行工具入口当你的模块是一个命令行工具时,你需要为命令行工具指定一个入口,即指定你的命令名称和本地可指定文件的对应关系。如果是全局安装,npm将会使用符号链接把可执行文件链接到/usr/local/bin,如果是本地安装,会链接到./node_modules/.bin/。
{"bin":{"conard":"./bin/"}}例如上面的配置:当你的包安装到全局时:npm会在/usr/local/bin下创建一个以conard为名字的软链接,指向全局安装下来的conard包下面的"./bin/"。这时你在命令行执行conard则会调用链接到的这个js文件。
发布文件配置{"files":["dist","lib","es"]}files属性用于描述你npmpublish后推送到npm服务器的文件列表,如果指定文件夹,则文件夹内的所有内容都会包含进来。我们可以看到下载后的包是下面的目录结构:
manman命令是Linux下的帮助指令,通过man指令可以查看Linux中的指令帮助、配置文件帮助和编程帮助等信息。
如果你的模块是一个全局的命令行工具,在通过man属性可以指定man命令查找的文档地址。
man文件必须以数字结尾,或者如果被压缩了,以.gz结尾。数字表示文件将被安装到man的哪个部分。如果man文件名称不是以模块名称开头的,安装的时候会给加上模块名称前缀。
例如下面这段配置:
{"man":["/Users/isaacs/dev/npm/cli/man/man1/","/Users/isaacs/dev/npm/cli/man/man1/"]}在命令行输入mannpm-audit:
规范项目目录一个模块是基于CommonJS模块化规范实现的,严格按照CommonJS规范,模块目录下除了必须包含包描述文件以外,还需要包含以下目录:
bin:存放可执行二进制文件的目录
lib:存放js代码的目录
doc:存放文档的目录
test:存放单元测试用例代码的目录
在模块目录中你可能没有严格按照以上结构组织或命名,你可以通过在指定directories属性来指定你的目录结构和上述的规范结构的对应情况。除此之外directories属性暂时没有其他应用。
{"directories":{"lib":"src/lib/","bin":"src/bin/","man":"src/man/","doc":"src/doc/","example":"src/example/"}}1.6脚本配置script{"scripts":{"test":"","dist":"antd-toolsrundist","compile":"antd-toolsruncompile","build":"npmruncompilenpmrundist"}}scripts用于配置一些脚本命令的缩写,各个脚本可以互相组合使用,这些脚本可以覆盖整个项目的生命周期,配置后可使用npmruncommand进行调用。如果是npm关键字,则可以直接调用。例如,上面的配置制定了以下几个命令:npmruntest、npmrundist、npmruncompile、npmrunbuild。
configconfig字段用于配置脚本中使用的环境变量,例如下面的配置,可以在脚本中使用_package_config_port进行获取。
{"config":{"port":"8080"}}1.7发布配置preferGlobal如果你的模块主要用于安装到全局的命令行工具,那么该值设置为true,当用户将该模块安装到本地时,将得到一个警告。这个配置并不会阻止用户安装,而是会提示用户防止错误使用而引发一些问题。
private如果将private属性设置为true,npm将拒绝发布它,这是为了防止一个私有模块被无意间发布出去。
publishConfig"publishConfig":{"registry":""},发布模块时更详细的配置,例如你可以配置只发布某个tag、配置发布到的私有npm源。更详细的配置可以参考npm-config
os假如你开发了一个模块,只能跑在darwin系统下,你需要保证windows用户不会安装到你的模块,从而避免发生不必要的错误。
使用os属性可以帮助你完成以上的需求,你可以指定你的模块只能被安装在某些系统下,或者指定一个不能安装的系统黑名单:
"os":["darwin","linux"]"os":["!win32"]
例如,我把一个测试模块指定一个系统黑名单:"os":["!darwin"],当我在此系统下安装它时会爆出如下错误:
cpu和上面的os类似,我们可以用cpu属性更精准的限制用户安装环境:
"cpu":["x64","ia32"]"cpu":["!arm","!mips"]
二、剖析包版本管理机制Nodejs成功离不开npm优秀的依赖管理系统。在介绍整个依赖系统之前,必须要了解npm如何管理依赖包的版本,本章将介绍npm包的版本发布规范、如何管理各种依赖包的版本以及一些关于包版本的最佳实践。
2.1查看npm包版本你可以执行npmviewpackageversion查看某个package的最新版本。
执行npmviewconardversions查看某个package在npm服务器上所有发布过的版本。
执行npmls可查看当前仓库依赖树上所有包的版本信息。
2.2SemVer规范npm包中的模块版本都需要遵循SemVer规范——由Github起草的一个具有指导意义的,统一的版本号表示规则。实际上就是SemanticVersion(语义化版本)的缩写。
标准版本SemVer规范的标准版本号采用的格式,其中X、Y和Z为非负的整数,且禁止在数字前方补零。X是主版本号、Y是次版本号、而Z为修订号。每个元素必须以数值来递增。
主版本号(major):当你做了不兼容的API修改
次版本号(minor):当你做了向下兼容的功能性新增
修订号(patch):当你做了向下兼容的问题修正。
例如:1.9.1-1.10.0-1.11.0
先行版本当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,你可能要先发布一个先行版本。
先行版本号可以加到“主版本号.次版本号.修订号”的后面,先加上一个连接号再加上一连串以句点分隔的标识符和版本编译信息。
内部版本(alpha):
公测版本(beta):
正式版本的候选版本rc:即Releasecandiate
React的版本下面我们来看看React的历史版本:
可见是严格按照SemVer规范来发版的:
版本号严格按照主版本号.次版本号.修订号格式命名
版本是严格递增的,:16.8.0-16.8.1-16.8.2
发布重大版本或版本改动较大时,先发布alpha、beta、rc等先行版本
发布版本在修改npm包某些功能后通常需要发布一个新的版本,我们通常的做法是直接去修改到指定版本。如果操作失误,很容易造成版本号混乱,我们可以借助符合Semver规范的命令来完成这一操作:
npmversionpatch:升级修订版本号
npmversionminor:升级次版本号
npmversionmajor:升级主版本号
2.3版本工具使用在开发中肯定少不了对一些版本号的操作,如果这些版本号符合SemVer规范,我们可以借助用于操作版本的npm包semver来帮助我们进行比较版本大小、提取版本信息等操作。
npminstallsemver
比较版本号大小
('1.2.3','9.8.7')//('1.2.3','9.8.7')//true判断版本号是否符合规范,返回解析后符合规范的版本号。
('1.2.3')//'1.2.3'('')//null将其他版本号强制转换成semver版本号
(('v2'))//'2.0.0'(('42.6.7.9.3-alpha'))//'42.6.7'一些其他用法
('=')//'1.2.3'('1.2.3','1.x||=2.5.0||5.0.0-7.2.3')//('=1.0.0')//'1.0.0'以上都是semver最常见的用法,更多详细内容可以查看semver文档:
2.4依赖版本管理我们经常看到,在中各种依赖的不同写法:
"depencies":{"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.017.0.0)
"react":"16.3.x":匹配主要版本和次要版本(=16.3.016.4.0)
再来看看后面两个,版本号中引用了~和^符号:
~:当安装依赖时获取到有新版本时,安装到中z的最新的版本。即保持主版本号、次版本号不变的情况下,保持修订号的最新版本。
^:当安装依赖时获取到有新版本时,安装到中y和z都为最新版本。即保持主版本号不变的情况下,保持次版本号、修订版本号为最新版本。
在文件中最常见的应该是"yargs":"^14.0.0"这种格式的依赖,因为我们在使用npminstallpackage安装包时,npm默认安装当前最新版本,然后在所安装的版本号前加^号。
注意,当主版本号为0的情况,会被认为是一个不稳定版本,情况与上面不同:
主版本号和次版本号都为0:^0.0.z、~0.0.z都被当作固定版本,安装依赖时均不会发生变化。
主版本号为0:^0.表现和~0.相同,只保持修订号为最新版本。
2.5锁定依赖版本lock文件实际开发中,经常会因为各种依赖不一致而产生奇怪的问题,或者在某些场景下,我们不希望依赖被更新,建议在开发中使用。
锁定依赖版本意味着在我们不手动执行更新的情况下,每次安装依赖都会安装固定版本。保证整个团队使用版本号一致的依赖。
每次安装固定版本,无需计算依赖版本范围,大部分场景下能大大加速依赖安装时间。
关于详细的结构,我们会在后面的章节进行解析。
定期更新依赖我们的目的是保证团队中使用的依赖一致或者稳定,而不是永远不去更新这些依赖。实际开发场景下,我们虽然不需要每次都去安装新的版本,仍然需要定时去升级依赖版本,来让我们享受依赖包升级带来的问题修复、性能提升、新特性更新。
使用npmoutdated可以帮助我们列出有哪些还没有升级到最新版本的依赖:
黄色表示不符合我们指定的语意化版本范围-不需要升级
红色表示符合指定的语意化版本范围-需要升级
执行npmupdate会升级所有的红色依赖。
2.6依赖版本选择的最佳实践版本发布对外部发布一个正式版本的npm包时,把它的版本标为1.0.0。
某个包版本发行后,任何修改都必须以新版本发行。
版本号严格按照主版本号.次版本号.修订号格式命名
版本号发布必须是严格递增的
发布重大版本或版本改动较大时,先发布alpha、beta、rc等先行版本
依赖范围选择主工程依赖了很多子模块,都是团队成员开发的npm包,此时建议把版本前缀改为~,如果锁定的话每次子依赖更新都要对主工程的依赖进行升级,非常繁琐,如果对子依赖完全信任,直接开启^每次升级到最新版本。
主工程跑在docker线上,本地还在进行子依赖开发和升级,在docker版本发布前要锁定所有依赖版本,确保本地子依赖发布后线上不会出问题。
保持依赖一致确保npm的版本在5.6以上,确保默认开启文件。
由初始化成员执行npminatall后,将提交到远程仓库。不要直接提交node_modules到远程仓库。
定期执行npmupdate升级依赖,并提交lock文件确保其他成员同步更新依赖,不要手动更改lock文件。
依赖变更升级依赖:修改文件的依赖版本,执行npminstall
降级依赖:直接执行npminstallpackage@version(改动不会对依赖进行降级)
注意改动依赖后提交lock文件
三、剖析npminstall原理
npminstall大概会经过上面的几个流程,这一章就来讲一讲各个流程的实现细节、发展以及为何要这样实现。
3.1嵌套结构我们都知道,执行npminstall后,依赖包被安装到了node_modules,下面我们来具体了解下,npm将依赖包安装到node_modules的具体机制是什么。
在npm的早期版本,npm处理依赖的方式简单粗暴,以递归的形式,严格按照结构以及子依赖包的结构将依赖安装到他们各自的node_modules中。直到有子依赖包不在依赖其他模块。
举个例子,我们的模块my-app现在依赖了两个模块:buffer、ignore:
{"name":"my-app","depencies":{"buffer":"^5.4.3","ignore":"^5.1.4",}}ignore是一个纯JS模块,不依赖任何其他模块,而buffer又依赖了下面两个模块:base64-js、ieee754。
{"name":"buffer","depencies":{"base64-js":"^1.0.2","ieee754":"^1.1.4"}}那么,执行npminstall后,得到的node_modules中模块目录结构就是下面这样的:
这样的方式优点很明显,node_modules的结构和结构一一对应,层级结构明显,并且保证了每次安装目录结构都是相同的。
但是,试想一下,如果你依赖的模块非常之多,你的node_modules将非常庞大,嵌套层级非常之深:
在不同层级的依赖中,可能引用了同一个模块,导致大量冗余。
在Windows系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。
3.2扁平结构为了解决以上问题,NPM在3.x版本做了一次较大更新。其将早期的嵌套结构改为扁平结构:
安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在node_modules根目录。
还是上面的依赖结构,我们在执行npminstall后将得到下面的目录结构:
此时我们若在模块中又依赖了base64-js@1.0.1版本:
{"name":"my-app","depencies":{"buffer":"^5.4.3","ignore":"^5.1.4","base64-js":"1.0.1",}}当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的node_modules下安装该模块。
此时,我们在执行npminstall后将得到下面的目录结构:
对应的,如果我们在项目代码中引用了一个模块,模块查找流程如下:
在当前模块路径下搜索
在当前模块node_modules路径下搜素
在上级模块的node_modules路径下搜索
直到搜索到全局路径中的node_modules
假设我们又依赖了一个包buffer2@^5.4.3,而它依赖了包base64-js@1.0.3,则此时的安装结构是下面这样的:
所以版本并未完全解决老版本的模块冗余问题,甚至还会带来新的问题。
试想一下,你的APP假设没有依赖base64-js@1.0.1版本,而你同时依赖了依赖不同base64-js版本的buffer和buffer2。由于在执行npminstall的时候,按照里依赖的顺序依次解析,则buffer和buffer2在的放置顺序则决定了node_modules的依赖结构:
先依赖buffer2:
先依赖buffer:
另外,为了让开发者在安全的前提下使用最新的依赖包,我们在通常只会锁定大版本,这意味着在某些依赖包小版本更新后,同样可能造成依赖结构的改动,依赖结构的不确定性可能会给程序带来不可预知的问题。
3.3Lock文件为了解决npminstall的不确定性问题,在版本新增了文件,而安装方式还沿用了的扁平化的方式。
的作用是锁定依赖结构,即只要你目录下有文件,那么你每次执行npminstall后生成的node_modules目录结构一定是完全相同的。
例如,我们有如下的依赖结构:
{"name":"my-app","depencies":{"buffer":"^5.4.3","ignore":"^5.1.4","base64-js":"1.0.1",}}在执行npminstall后生成的如下:
{"name":"my-app","version":"1.0.0","depencies":{"base64-js":{"version":"1.0.1","resolved":"","integrity":"sha1-aSbRsZT7xze47tUTdW3i/Np+pAg="},"buffer":{"version":"5.4.3","resolved":"","integrity":"sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==","requires":{"base64-js":"^1.0.2","ieee754":"^1.1.4"},"depencies":{"base64-js":{"version":"1.3.1","resolved":"","integrity":"sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="}}},"ieee754":{"version":"1.1.13","resolved":"","integrity":"sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="},"ignore":{"version":"5.1.4","resolved":"","integrity":"sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A=="}}}我们来具体看看上面的结构:
最外面的两个属性name、version同中的name和version,用于描述当前包名称和版本。
depencies是一个对象,对象和node_modules中的包结构一一对应,对象的key为包名称,值为包的一些描述信息:
version:包版本——这个包当前安装在node_modules中的版本
resolved:包具体的安装来源
integrity:包hash值,基于SubresourceIntegrity来验证已安装的软件包是否被改动过、是否已失效
requires:对应子依赖的依赖,与子依赖的中depencies的依赖项相同。
depencies:结构和外层的depencies结构相同,存储安装在子依赖node_modules中的依赖包。
这里注意,并不是所有的子依赖都有depencies属性,只有子依赖的依赖和当前已安装在根目录的node_modules中的依赖冲突之后,才会有这个属性。
例如,回顾下上面的依赖关系:
我们在my-app中依赖的base64-js@1.0.1版本与buffer中依赖的base64-js@^1.0.2发生冲突,所以base64-js@1.0.1需要安装在buffer包的node_modules中,对应了中buffer的depencies属性。这也对应了npm对依赖的扁平化处理方式。
所以,根据上面的分析,文件和node_modules目录结构是一一对应的,即项目目录下存在可以让每次安装生成的依赖目录结构保持相同。
另外,项目中使用了可以显著加速依赖安装时间。
我们使用npmi--timing=true--loglevel=verbose命令可以看到npminstall的完整过程,下面我们来对比下使用lock文件和不使用lock文件的差别。在对比前先清理下npm缓存。
不使用lock文件:
使用lock文件:
可见,中已经缓存了每个包的具体版本和下载链接,不需要再去远程仓库进行查询,然后直接进入文件完整性校验环节,减少了大量网络请求。
使用建议开发系统应用时,建议把文件提交到代码版本仓库,从而保证所有团队开发者以及CI环节可以在执行npminstall时安装的依赖版本都是一致的。
在开发一个npm包时,你的npm包是需要被其他仓库依赖的,由于上面我们讲到的扁平安装机制,如果你锁定了依赖包版本,你的依赖包就不能和其他依赖包共享同一semver范围内的依赖包,这样会造成不必要的冗余。所以我们不应该把文件发布出去(npm默认也不会把文件发布出去)。
3.4缓存在执行npminstall或npmupdate命令下载依赖后,除了将依赖包安装在node_modules目录下外,还会在本地的缓存目录缓存一份。
通过npmconfiggetcache命令可以查询到:在Linux或Mac默认是用户主目录下的.npm/_cacache目录。
在这个目录下又存在两个目录:content-v2、index-v5,content-v2目录用于存储tar包的缓存,而index-v5目录用于存储tar包的hash。
npm在执行安装时,可以根据中存储的integrity、version、name生成一个唯一的key对应到index-v5目录下的缓存记录,从而找到tar包的hash,然后根据hash再去找缓存的tar包直接使用。
我们可以找一个包在缓存目录下搜索测试一下,在index-v5搜索一下包路径:
grep""-rindex-v5
然后我们将json格式化:
{"key":"pacote:version-manifest:","integrity":"sha512-C2EkHXwXvLsbrucJTRS3xFHv7Mf/y9klmKDxPTE8yevCoH5h8Ae69Y+/lP+ahpW91crnzgO78elOk2E6APJfIQ==","time":57,"size":1,"metadata":{"id":"base64-js@1.0.1","manifest":{"name":"base64-js","version":"1.0.1","engines":{"node":"=0.4"},"depencies":{},"optionalDepencies":{},"devDepencies":{"standard":"^5.2.2","tape":"4.x"},"bundleDepencies":false,"peerDepencies":{},"deprecated":false,"_resolved":"","_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提供了几个命令来管理缓存数据:
npmcacheadd:官方解释说这个命令主要是npm内部使用,但是也可以用来手动给一个指定的package添加缓存。
npmcacheclean:删除缓存目录下的所有数据,为了保证缓存数据的完整性,需要加上--force参数。
npmcacheverify:验证缓存数据的有效性和完整性,清理垃圾数据。
基于缓存数据,npm提供了离线安装模式,分别有以下几种:
--prefer-offline:优先使用缓存数据,如果没有匹配的缓存数据,则从远程仓库下载。
--prefer-online:优先使用网络数据,如果网络数据请求失败,再去请求缓存数据,这种模式可以及时获取最新的模块。
--offline:不请求网络,直接使用缓存数据,一旦缓存数据不存在,则安装失败。
3.5文件完整性上面我们多次提到了文件完整性,那么什么是文件完整性校验呢?
在下载依赖包之前,我们一般就能拿到npm对该依赖包计算的hash值,例如我们执行npminfo命令,紧跟tarball(下载链接)的就是shasum(hash):
用户下载依赖包到本地后,需要确定在下载过程中没有出现错误,所以在下载完成之后需要在本地在计算一次文件的hash值,如果两个hash值是相同的,则确保下载的依赖是完整的,如果不同,则进行重新下载。
3.6整体流程好了,我们再来整体总结下上面的流程:
检查.npmrc文件:优先级为:项目级的.npmrc文件用户级的.npmrc文件全局级的.npmrc文件npm内置的.npmrc文件
检查项目中有无lock文件。
无lock文件:
不存在缓存:
存在缓存:将缓存按照依赖结构解压到node_modules
将下载的包复制到npm缓存目录
将下载的包按照依赖结构解压到node_modules
重新下载
从npm远程仓库下载包
校验包的完整性
校验不通过:
校验通过:
构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在node_modules根目录。
当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的node_modules下放置该模块。
注意这一步只是确定逻辑上的依赖树,并非真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包
从npm远程仓库获取包信息
根据构建依赖树,构建过程:
在缓存中依次查找依赖树中的每个包
将包解压到node_modules
生成lock文件
有lock文件:
检查中的依赖版本是否和中的依赖有冲突。
如果没有冲突,直接跳过获取包信息、构建依赖树过程,开始在缓存中查找包信息,后续过程相同
上面的过程简要描述了npminstall的大概过程,这个过程还包含了一些其他的操作,例如执行你定义的一些生命周期函数,你可以执行npminstallpackage--timing=true--loglevel=verbose来查看某个包具体的安装流程和细节。
3.7yarn
yarn是在2016年发布的,那时npm还处于V3时期,那时候还没有文件,就像上面我们提到的:不稳定性、安装速度慢等缺点经常会受到广大开发者吐槽。此时,yarn诞生:
上面是官网提到的yarn的优点,在那个时候还是非常吸引人的。当然,后来npm也意识到了自己的问题,进行了很多次优化,在后面的优化(lock文件、缓存、默认-s)中,我们多多少少能看到yarn的影子,可见yarn的设计还是非常优秀的。
yarn也是采用的是npmv3的扁平结构来管理依赖,安装依赖后默认会生成一个文件,还是上面的依赖关系,我们看看的结构:
yarnlockfilev1base64-js@1.0.1:version"1.0.1"resolved""integritysha1-aSbRsZT7xze47tUTdW3i/Np+pAg=base64-js@^1.0.2:version"1.3.1"resolved""integritysha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==buffer@^5.4.3:version"5.4.3"resolved""integritysha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==depencies:base64-js"^1.0.2"ieee754"^1.1.4"ieee754@^1.1.4:version"1.1.13"resolved""integritysha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==ignore@^5.1.4:version"5.1.4"resolved""integritysha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
可见其和文件还是比较类似的,还有一些区别就是:
使用的是json格式,使用的是一种自定义格式
中子依赖的版本号不是固定的,意味着单独又一个确定不了node_modules目录结构,还需要和文件进行配合。而只需要一个文件即可确定。
yarn的缓策略看起来和npmv5之前的很像,每个缓存的模块被存放在独立的文件夹,文件夹名称包含了模块名称、版本号等信息。使用命令yarncachedir可以查看缓存数据的目录:
参考小结希望阅读完本篇文章能对你有如下帮助:
了解中的各项详细配置从而对项目工程化配置有更进一步的见解
掌握npm的版本管理机制,能合理配置依赖版本
理解npminstall安装原理,能合理运用npm缓存、
作者:ConardLi
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。