foo - underscore 1.5.x
alice - underscore 1.7.x
bob - underscore 1.7.x
在当前的前端包管理生态中,npm已经成为主流选择,但我个人并不倾向于使用npm来管理前端包。这并不是因为没有更好的选择,而是因为在我实际的使用中,我发现npm存在一些边界情况,不太适用。在这篇文章中,我想分享一下我对这个问题的思考,而不是给出一个推荐的工具(因为我还没有找到满意的解决方案)。
首先,一个常见的问题是前端、后端和脚本工具共享包可能会带来一些不便。举个例子,线上部署的环境通常追求稳定性,而脚本工具的环境可以更激进地使用新版本的包和Node。那么当脚本工具需要使用一个新版本的库时,线上代码依赖的库版本该如何处理呢?npm并不允许在顶层存在一个包的多个版本。假设线上的前端代码依赖underscore 1.5.x,而我们的工具脚本发现1.7.x有一些很好用的函数,这时我们就需要做出取舍。如果升级到1.7.x,就需要进行回归测试,同时线上的js缓存可能因为仅仅一个库的升级,在功能没有变化的情况下失效。如果不升级,本地的工具脚本继续使用1.5.x,那么工具脚本的生产效率会下降。另一种解决办法是将repo本身进行拆分,将工具脚本变成一个独立的npm包,但这并不是一个很好的选择。
其次,npm并不能决定版本冲突时哪一个版本才是主版本。假设我们需要安装三个库,其中underscore必然存在版本冲突。然而,在npm3中,哪个版本作为主版本(放在扁平化顶层)并不受控制。如果安装顺序是foo – alice – bob,那么1.5.x将成为主版本,而alice和bob将各自拥有自己的underscore,这样就会出现3个underscore的实例。这显然是一种浪费,因此我们需要自己去查看各种依赖关系,然后决定安装顺序。npm在这方面并不透明,当依赖关系复杂时,并没有给我们带来明显的好处。如果不在意这种重复,也不关心体积问题,那么如果是一个有side-effect的库又会怎样呢?
另一个问题是源码和构建产物之间的关系。早期的bower存在一个问题,就是直接从Git上拉取代码,很容易只拉到源码而没有构建过程。我一直在思考如何在源码和构建产物之间做取舍。最近我做一个小型库的时候,这个问题达到了巅峰。如果只发布源码,意味着使用者需要对我的源码进行构建,需要知道构建所需的工具,这无疑增加了使用成本。如果只发布构建产物,那么在构建过程中可能会产生冗余信息,比如使用了N个ES6编写的库,如果要让构建产物直接可用,就不能使用babel编译的external-helpers参数(所以我不明白为什么babrel-preset-es2015-rollup内置了这个plugin),这样就会产生多个helper,这种冗余会给大型系统带来问题。同样的问题也存在于bundle和dist上,如果我发布一个bundle(将多个模块合并成一个),那么使用者如果只需要其中的一部分就会很麻烦,基于UMD的tree shaking技术还不够成熟。所以我的选择是,对于小型库,发布源码和bundle;对于大型库,发布源码和构建产物。因此,源码还是需要发布的。在大型系统中,我更倾向于直接使用依赖库的源码来解决代码冗余等问题。
另一个问题是npm提供的依赖类型不够。npm只提供了production模式,并且只区分了dependencies和devDependencies,这让人很尴尬。我遇到的一个实际案例是,某个包依赖mocha + phantom进行单元测试,但我们的自动发布机器无法安装phantom,所以这个包再也没有成功自动发布过。我认为npm应该提供更多的依赖模式,比如对依赖进行定制化的分类管理,并且通过npm i –category=xxx来控制安装哪一部分。
最后一个问题是缺乏”反式依赖”功能。目前没有任何一个包管理工具能够实现这个功能。”反式依赖”是指在一个项目中使用了一个固定的主框架版本,其他依赖需要符合这个版本。举个例子,当前项目使用了react 0.14,现在需要使用react-tree这个组件。当我们使用npm i react-tree时,我们会得到一个最新版本的react-tree,因为react-tree的最新版本依赖于react 15.x,所以在react-tree内部会有它自己的react。这是我们想要的吗?这个react-tree还能正常使用吗?然而,将react从0.14升级到15.x几乎是不可能的,我们没有足够的精力和资源来完成这个任务。因此,我们更常做的是先查看react-tree的版本,找到一个能在react 0.14上使用的版本,然后指定安装这个版本。这就是”反式依赖”,它的逻辑是”先有一个不可变的主框架版本,其他依赖需要符合这个版本”。然而,当前的包管理工具都无法提供这个功能,而且基于现有的npm包信息,实现这个功能也很困难。
总结一下,我明白这些问题在实际使用中并不经常出现(甚至有些问题大家可能并没有注意到),所以我并不反对使用npm来管理前端包。但是上述问题确实存在,对于可能遇到这些问题的场景,我们需要考虑一些解决方案。我还向我们的TC提出了一些关于包管理的期望,其中很多功能是现有的包管理工具无法支持的。