前言

npm(全称 Node Package Manager,即“node包管理器”)是Node.js预设的、用JavaScript编写的包管理工具。尽管是Node.js中的工具,但当初更多的被用来配合前端构建工具给前端进行包治理。

作为一个包管理器,最重要的就是治理依赖了。对于简单的依赖树,npm 的解决机制和其余的包管理器会有所不同,本文将会具体介绍这些细节

npm2和npm3+版本对依赖的解决有所不同,但当初很少有应用npm3以下版本的我的项目了,本文中所有的介绍都是基于npm3+以上版本

npm 依赖管理机制

npm 大体上来看,和其余的包管理器差不多,都是包依赖包,并且用版本号来申明这些依赖的包。

语义化版本号

npm 中应用语义化版本来管制版本依赖包的版本,比方^~>=<之类的范畴符号,不过本文中版本号的解析形式不是重点,只须要晓得如果应用范畴版本号,npm会装置范畴内可用的最新版本
**
这里要吐槽一下npm的文档,光是找这个范畴版本号具体应用的版本策略,就找了很久,文档中并没有清晰的阐明……最初在npm update页面中找到了一丝介绍

If app’s package.json contains:

"dependencies": {"dep1": "^1.1.1"}

Then npm update will install dep1@1.2.2, because 1.2.2 is latest and 1.2.2 satisfies ^1.1.1.

npm的这个范畴版本设计的理念还是挺先进的,通过范畴版本号让应用方能够及时的自动更新小版本,降级后可能修复一些bug,然而随之而来的也会有很多因更新导致的危险。毕竟版本号是人类管制的,人类管制就有可能呈现失误,比方一个订正版本号的更新中删除了某些api,导致无奈兼容

集体看来,这种范畴版本号的包管理机制,是弊大于利的,危险过高。如果在服务端场景下,什么都没改的状况下就偷摸换了个(小)版本,很可能会呈现一些重大的事变。一般来说,任何改变都须要通过测试,尤其是这种依赖包降级,是个挺有危险的事件。如果是那种通用根底包的危险就更大了,援用的中央过多,很可能呈现一些不兼容的状况。

依赖树和传递依赖

npm 会默认会将传递依赖的包用flat的模式,也装置至node_modules的根目录,比方有一个模块A,他依赖了模块B:
**

版本抵触

当初减少一个模块C,C也依赖B,然而C依赖了B的高版本V2.0,此时npm的解决就有点不一样了;因为C依赖的B模块版本和A依赖的B版本不兼容,npm 会先将A模块依赖的B1.0装置至根目录,而后将C依赖的B2.0装置至C本人的node_modules中,如下图所示

目录构造

|————mod-A@1.0|————mod-B@1.0|————mod-C@1.0    |————mod-B@2.0

对于版本不兼容的依赖树,npm的解决是先查看是否版本兼容,如果版本兼容就不反复装置,如果和之前的的传递依赖包版本不兼容,那么就将该依赖包装置至以后援用的包的node_modules下
**
npm 的包版本抵触解决方案尽管带来了包文件的 冗余,但能够很好的解决抵触问题

这种版本抵触解决机制真的很完满吗?

素来面的介绍能够看出,当呈现版本不兼容时,npm会将依赖的包装置至以后包的node_modules下,有点submodule的意思,但也不是真的十拿九稳,还是有可能呈现因为多版本共存导致的抵触。

还是拿下面的A/B/C三个依赖模块来举例,比方B v1.0中向window对象注册了一个属性,B v2.0也向window中注册了一个属性,因为B v1.0和v2.0差距很大,尽管注册的是同一个对象,但属性和其函数差距很大,当一个页面同时引入A和C模块时,B v1.0和B v2.0都会加载,可能会呈现一些意外的谬误。对于使用者来说是不能承受的

下面这个例子可能还不是很失当,因为注册window这件事原本就有肯定危险。当初构想另一种常见的场景,比方有在Angular(2)中,两个基于Angular的组件依赖了不同的Angular(Core)大版本,那么当一个页面同时应用两个组件,并且两个组件须要在以后页面进行交互时,比方赋值或者函数调用之类,就很容易呈现上图中的问题。

这种问题在Java生态中的包治理尽管也有,但模式会有所不同:

在Maven中(Java生态的包管理工具),尽管依赖是树状构造的,但构建后的后果其实是立体(flat)的的。如果呈现多个版本的jar包,运行时个别会将所有jar包都加载;不过因为JAVA中ClassLoader的parent delegate机制,同样的Class只会被加载一次,下N个Jar包内的的同名类(包名+类名)会被疏忽,这样的益处是简略,如果呈现版本抵触也清晰可见,抵触问题须要使用者自行处理。

Maven Build对包(传递)依赖多版本的解决,如下图所示:

npm 对于这种可能呈现的版本抵触问题,也提供了一个解决办法:peerDependencies

peerDependencies

peerDependencies和maven中的provide scope很像,当一个依赖模块X定义在peerDependencies中而不是devDependencies或dependencies中时,依赖该模块的我的项目就不会主动下载该依赖。

我的项目中须要间接或间接的申明合乎该版本的依赖,间接依赖是指间接在devDependencies或dependencies中申明,间接依赖是指以后我的项目依赖的其余模块依赖了X合乎版本范畴的模块,如果二者都不满足,在npm install时会呈现一个告警,比方:

npm WARN hidash@0.2.0 requires a peer of lodash@~1.3.1 but none is installed. You must install peer dependencies yourself.

npm & webpack

当初很多我的项目都会应用webpack来作为我的项目的构建工具,然而和java中的maven 不同,webpack和npm是两套独立的工具,构建和包治理是离开的

也就是说,哪怕npm将抵触包作为“submodule”的模式装置在以后包内,然而webpack可不肯定认

比方下面ABC三个模块的例子,如果A模块的代码中import BObj from B mod,那么webpack构建之后,会让A援用哪一个B版本呢?v1.0 还是 v2.0?

这个场景相当简单,本文就不介绍了,有一篇文章具体介绍了webpack下的解决形式和测试场景:《Finding and fixing duplicates in webpack with Inspectpack》

总结

npm 包治理的设计理念尽管很好,但不适宜所有的场景,比方这种submodule的模式拿到java里就不可行,而且submodule的模式还是有肯定的危险,只是危险升高了。一旦有多个依赖的代码在一个页面同时工作或交互,就很容易出问题。

无论是什么包管理工具,最平安的做法还是防止反复。在减少新依赖或是新建我的项目后,应用一些依赖剖析查看工具检测一遍,修复反复/抵触的依赖。

参考

  • Finding and fixing duplicates in webpack with Inspectpack
  • https://github.com/formidablelabs/inspectpack
  • Understanding the npm dependency model
  • https://www.reddit.com/r/haskell/comments/4zc6y3/why_doesnt_cabal_use_a_model_like_that_of_npm/?ref=share&ref_source=link
  • https://stackoverflow.com/questions/25268545/why-does-npms-policy-of-duplicated-dependencies-work
  • How npm3 Works
  • https://nodejs.org/es/blog/npm/peer-dependencies/
  • https://docs.npmjs.com/packages-and-modules/
  • npm 依赖治理中被疏忽的那些细节
  • How does npm handle conflicting package versions?