共计 3434 个字符,预计需要花费 9 分钟才能阅读完成。
前言
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 installdep1@1.2.2
, because1.2.2
islatest
and1.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?