原文地址:https://blog.domenic.me/peer-dependencies/
nodejs官网收录了这篇文章,地址是https://nodejs.org/en/blog/npm/peer-dependencies/
作者是 Domenic Denicola ,就职于谷歌Chrome团队。github 上npm开源项目的参与者。
在本文中我把 Peer Dependencies 翻译成了同版本依赖。peer 字典中,除了“仔细看”的意思外,还有“(官阶、等级、地位或功绩)同等的人”的意思。这里结合上下文,应该是版本号相同的意思,所以翻译成同版本依赖。
下面是正文内容:
npm 作为一款包管理器来说,是很优秀的。特别值得一提的是,它可以把子依赖处理得非常好。假如我的包依赖于两个模块:2号版本的 request 和 some-other-library (为了方便举例子,假设另一个模块的名字叫some-other-library),同时 some-other-library 又依赖于 1 号版本的 request ,产生的依赖图看起来像这样:
├── request@2.12.0
└─┬ some-other-library@1.2.3
└── request@1.9.9
一般来讲,这个结果是非常好的:现在 some-other-library 有了能供它调用的 request 1 号版本的副本,同时不会干扰我的包依赖的request 2号版本的副本。每个人的代码都可以正常工作。
难题:插件
然而,有一个用例会让上文提及的处理方案行不通:插件。即使插件包不一定直接使用宿主包,插件包也往往意味着会被其它的宿主包所用。在 Node.js 开发包生态中,已经有了许多这种模板的例子了:
- Grunt plugins
- Chai plugins
- Levelup plugins
- Express middleware
- Winston transports
即使你不精通哪些用例,你也一定能从过去当客户端开发者的时候回忆起 jQuery 插件:你放进页面中的 <script>
标签会把插件连接到 jQuery.prototype
,这方便了以后的维护。
本质上,插件的设计初衷就是给宿主包用的。但更重要的是,插件作者把插件设计成给特定版本的宿主包用。举个例子,我的 chai-as-promised
包的 1.x 和 2.x 版本运行在 chai
的 0.5 版本上,然而 chai-as-promised
3.x 版本却运行在 chai
1.x 版上。或者在更快节奏和更少语义化版本规范支持的 Grunt 插件世界,虽然 grunt-contrib-stylus
0.3.1 版能在 grunt
0.4.0rc4 版上运行,但是 grunt-contrib-stylus
同一个版本不能在 grunt
0.4.0rc5 版上运行,原因竟是新版本的 grunt 删除了一些 API 。
作为一款包管理器,当 npm 安装你的依赖的时候,npm很大一部分工作是管理依赖的版本。但是npm 的普通模式,根据package.json中的配置来使用依赖的哈希值,在碰到插件的时候很明显会运行出错。绝大多数插件从来没有实际依赖宿主包(译者注:这里指插件的代码中有引用宿主包的语句。),比如 grunt 插件从来没有写 require("grunt")
代码。就算插件下载了宿主包作为依赖,那个下载下来的副本也从来不会被使用。因为你的应用使用的插件可能和宿主包不兼容,我们又回到了原点。
甚至对于那些由于宿主包提供了工具API,而可以直接引用宿主包的插件,依然会有问题。在插件包的 package.json 文件中指定宿主包会造成依赖树有宿主包的多个拷贝——而不是你想的那样。举个例子,让我们假设 winston-mail 0.2.3 在其依赖项哈希值中指定”winston”: “0.5.x”,原因在于 winston 0.5.x 是 winston-mail 0.2.3 反复测试的最后一个版本。作为一名应用开发者,因为你想要最后的和最好的版本,所以你查阅了 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
因为插件使用了不同的Winston API,所以会产生这个不易察觉的安装失败。我愿意留下这个不易察觉的安装失败来给你思考,而不是留下整个主要的应用。
解决方案:同版本依赖(Peer Dependencies)
我们需要一种表现插件及其宿主包之间依赖关系的方法。某种程度上说,“因为我只能运行在1.2.x版本的宿主包中,所以如果你要安装我,请确保使用了兼容的宿主包。”我们把这种关系叫做同版本依赖。
同版本依赖的主意已经被讨论了好几年了。九个月前,在我花了整个周末时间志愿完成了这个功能之后,我最终经历了一个自由(译者注:free指自由软件)的周末,现在npm有了同版本依赖。
特别地,npm 1.2.0 以基础的方式实现了同版本依赖,并在接下来几个新的发布版本不断改善同版本依赖,加入了一些我事实上喜欢的东西。今天 Issac 把 npm 1.2.10 打包进了 Node.js 0.8.19,因此假如你安装了最新版本的 Node,你就已经做好了使用同版本依赖的准备了。
As proof,I present you the results of trying to install jitsu 0.11.6 with npm 1.2.10:
作为证据,我给你看看用 npm 1.2.10 尝试安装 jitsu 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
如你所见,jitsu依赖于两个和 Flatiron 相关的包,这些包同版本依赖于 Flatiron 的两个冲突的版本。好东西npm已经积极地帮助我们找出了冲突,因此这个错误能在0.11.7版本被修复!
使用同版本依赖
同版本依赖非常易于使用。当写一个插件的时候,弄明白宿主包的哪一个版本是你要做同版本依赖的,然后把宿主包的版本加入 package.json中。
{
"name": "chai-as-promised","peerDependencies": { "chai": "1.x" } }
现在当安装chai-as-promised的时候,chai 报会跟着一块安装。如果你以后尝试安装只兼容 Chai 0.x 版本的 Chai 插件,你会得到一个错误。这非常好!
一条建议:同版本依赖的必要条件,不同于正常依赖的必要条件,应该更宽容些。你不应该把你的同版本依赖锁定到某个特定的补丁版本号上。假如仅仅因为 Chai 插件的作者们懒惰并且没有花时间弄清楚 Chai 插件兼容的 Chai 宿主包的最小版本,就会让一个Chai 插件同版本依赖于 Chai 1.4.1,另一个Chai插件同版本依赖于 Chai 1.5.0 ,这样同版本依赖就会用起来很烦人。
确定同版本依赖的必要条件的最好方法是切实遵守语义化版本控制规范。我们先假定只有宿主包的主要版本中的更改会破坏你的插件。因此如果你的插件兼容宿主包 1.x 中的每个细分版本,那么就用 "~1.0"
或 "1.x"
来表达这个意思。如果你依赖于 1.5.2 版本实现的特性,使用 ">= 1.5.2 < 2"
。
从现在开始,使用同版本依赖吧!