陈斌彬的技术博客

Stay foolish,stay hungry

NPM-Peer Dependencies 介绍

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.x2.xChai v0.5 一起用,3.xChai v1.x 一起用。又比如,对于不遵守 semverGrunt 插件,grunt-contrib-stylus v0.3.1Grunt 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",这是它测试的最新版本。作为一个应用开发者,你想要最新最棒的,所以你查找最新版本的 winstonwinston-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 插件,它们同伴依赖于版本相冲突的 Flatironnpm 在帮我们判断这个冲突,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"。

参考