npm依赖
# 前言
npm(全称 Node Package Manager,即“node 包管理器”)是 Node.js 预设的、用 JavaScript 编写的包管理工具。虽然是 Node.js 中的工具,但现在更多的被用来配合前端构建工具给前端进行包管理。
作为一个包管理器,最重要的就是管理依赖了。对于复杂的依赖树,npm 的处理机制和其他的包管理器会有所不同
# npm 依赖管理机制
npm 大体上来看,和其他的包管理器差不多,都是包依赖包,并且用版本号来声明这些依赖的包。
# 语义化版本号
npm 中使用语义化版本来控制版本依赖包的版本,比如^~>=<之类的范围符号,不过版本号的解析方式不是重点,只需要知道如果使用范围版本号,npm 会安装范围内可用的最新版本
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》 (opens new window)
# 总结
npm 包管理的设计理念虽然很好,但不适合所有的场景,比如这种 submodule 的模式拿到 java 里就不可行,而且 submodule 的模式还是有一定的风险,只是风险降低了。一旦有多个依赖的代码在一个页面同时工作或交互,就很容易出问题。
无论是什么包管理工具,最安全的做法还是避免重复。在增加新依赖或是新建项目后,使用一些依赖分析检查工具检测一遍,修复重复/冲突的依赖。