npm 是非常棒的包管理器。特别是对子依赖处理得非常好。如果我的包依赖于 request v2 及 some-other-library,但是 some-other-library 依赖于 request v1,安装后的依赖树是这样的:
├── request@2.12.0
└─┬ some-other-library@1.2.3
└── request@1.9.9
通常这样不错:some-other-library
用自己的 request v1
,不会影响到我的包的 request v2
。
问题:插件
不过有一种情况这样会失败:插件。一个插件(plugin package )跟另一个宿主(host plugin)一起用,即使它不直接使用宿主。在 Node.js 里已有有不少的例子:
- Grunt 插件
- Chai 插件
- Levelup 插件
- Express 中间件
- Winston transports
如果你是客户端开发者,即使你不熟悉上面这些包,你可以想一下 “jQuery plugins”
:向页面插入 <script>
,它们向 jQuery.prototype
添加属性,方便后面使用。
大体上,插件是要与宿主一起用。但是更重要的是,它们与特定版本的宿主一起用。例如,我的插件 chai-as-promised 1.x
与 2.x
与 Chai v0.5
一起用,3.x
与 Chai v1.x
一起用。又比如,对于不遵守 semver
的 Grunt
插件,grunt-contrib-stylus v0.3.1
与 Grunt 0.4.0rc4
一起用,但是不能与 Grunt 0.4.0rc5
一起用,因为移除了相关 API
。
作为包管理器,npm 在安装依赖包的时候一大块的工作是处理版本。但是它的常用模式—— package.json 的 dependencies
表,对于插件毫无疑问地失败。多数插件从不依赖于它们的宿主,例如 grunt
插件从不 require("grunt")
,所以即使写下依赖的宿主,也不会使用下载的宿主。于是我们回到了起点,所用的插件不兼容于它的宿主。
即使是直接依赖宿主的插件——可能用到宿主的 API,在插件的 package.json
指定依赖,这将下载多个版本的宿主,却不是你想要的。举个例子,假如 winston-mail
在它的 dependencies
表中指定了 "winston": "0.5.x"
,这是它测试的最新版本。作为一个应用开发者,你想要最新最棒的,所以你查找最新版本的 winston
和 winston-mail
,在你的 package.json
这样写道:
{
"dependencies": {
"winston": "0.6.2",
"winston-mail": "0.2.3"
}
}
但是运行 npm install
得到的依赖树不是预期的:
├── winston@0.6.2
└─┬ winston-mail@0.2.3
└── winston@0.5.11
解决办法:同伴依赖
我们需要一个办法表达插件与它们的宿主之间的依赖。好比说 “我是宿主 1.2.x 的扩展,如果你安装我,请确定安装兼容的宿主。” 我们称这种关系为同伴依赖(peer dependency)。
同伴依赖的思路提出来已经有"好些年"了(930 - 1400)。九个月前我说用“整个周末”实现它,终于我得到一个空闲的周末,现在 npm 有同伴依赖了!
特别是它作为基础部分引入到 npm 1.2.0,在后面几个版本中又继续完善,我很高兴看到这点。今天 Isaac 将 npm 1.2.10 打包进 Node.js 0.8.19,所以如果你安装最新的 Node, 你能马上使用同伴依赖。
为证明我说的,看我用 npm 1.2.10 安装 jistu 0.11.6:
npm ERR! peerinvalid The package flatiron does not satisfy its siblings' peerDependencies requirements!
npm ERR! peerinvalid Peer flatiron-cli-config@0.1.3 wants flatiron@~0.1.9
npm ERR! peerinvalid Peer flatiron-cli-users@0.1.4 wants flatiron@~0.3.0
正如所见,jistu
依赖于两个 Flatiron
插件,它们同伴依赖于版本相冲突的 Flatiron
。npm
在帮我们判断这个冲突,jistu 0.11.7
将修复这个问题。
使用同伴依赖
同伴依赖用法很简单。写插件时,确定插件使用哪个版本的宿主,将它添加到你的 package.json
:
{
"name": "chai-as-promised",
"peerDependencies": {
"chai": "1.x"
}
}
当安装 chai-as-promised
时,也会安装 chai
。如果后面你打算安装另一个适用于 Chai 0.x
的 插件,将会得到一个错误。好!
一个建议:不像一般的依赖,同伴依赖需要放宽版本。你不应该将你的同伴依赖锁定到特定的补丁版本号。这将会很烦人,一个 Chai 同伴依赖于 Chai 1.4.1,另一个同伴依赖于 Chai 1.5.0,只是因为作者比较懒,不花时间去确定最低可以使用的 Chai 版本。
确定同伴依赖的最好办法是确实地遵守 semver。假定只有宿主的主版本号的变动才会损坏你的插件。因此,如果是 1.x 宿主,使用 “~1.0” 或 “1.x";如果依赖于 1.5.2 引入的功能,使用 ”>= 1.5.2 < 2"。