npm、bower、jamjs 等包管理器,哪个比较好用?

其他 js 的包管理:jamjs、volojs、component
关注者
935
被浏览
102,965

18 个回答

在当前的js包管理生态里,npm一统天下是早晚的事。然而我依旧不倾向于用npm管理前端包,并不是因为有更好的选择,而是因为在我的实际使用中,npm还是存在一些边界情况并不好使

大家都说思考的过程才是重要的,所以这里我的主要目的是分享我在这方面的思考,并不给你一个推荐的工具( 因为就没有我满意的)

1. 前端、后端、脚本工具共享包可能会带来一些不便

一个最常见的例子在于,线上部署的环境会追求稳定,而脚本工具的环境可以激进地使用新版,这包括了包的版本和Node本身的版本

那么当 脚本工具追求新版本的库的时候,同样用了这个库的前后端线上代码要如何处理呢?npm并不允许顶层存在一个包的多个版本

假设线上的前端依赖着underscore 1.5.x,而现在我们的工具脚本发现1.7.x有不少很好用的函数,我们就要去做取舍

  1. 升级到1.7.x,那么要进行回归测试,同时线上的js缓存 可能因为仅仅一个库的升级,在功能没变化的情况下失效
  2. 不升级,本地的工具脚本继续用1.5.x,那么工具脚本的生产效率会下降

这个问题有另一个解决办法,就是把repo本身进行拆分,工具脚本变成一个npm package,然而这并不好玩

2. npm并不能决定版本冲突时哪一个才是主版本

我们假设有这样的依赖关系:

foo - underscore 1.5.x
alice - underscore 1.7.x
bob - underscore 1.7.x

我们需要安装这三个库,那么underscore必然存在版本冲突,但是在npm3中哪个版本作为主版本(放在扁平化顶层)并不受控制

如果你的安装顺序是foo - alice - bob,那么1.5.x将变成主版本,而alice和bob将各自拥有自己的underscore,这就导致一共出现了3次:

node_modules
├── alice
│   └── node_modules
│       └── underscore
├── bob
│   └── node_modules
│       └── underscore
├── foo
└── underscore

这显然是一种浪费,因此我们需要自己去看各种依赖关系,然后决定安装顺序(根据上面的场景,其实应该是alice - bob - foo的顺序安装),npm在这点上并不透明,依赖复杂时并没有为我们带来特别明显的福利

当然你可以说你不是强迫症,并不在乎这种重复,也不care体积上的问题,那么如果这并不是underscore这种工具库,而是一个有side-effect的库又会怎么样呢?

3. 关于 源码和构建产物的关系

前面有很多同学都说了,bower早期一大问题是直接从Git上拉取,很容易只拉到源码而没有构建过程

不过在这里我其实一直有一个思考,就是如何对src和dist,甚至是bundle进行取舍。这在我最近做一个小lib的时候达到了巅峰:

  1. 如果只发布源码,意味着使用者要对我的源码进行构建,要知道构建需要什么工具,无疑增加了使用成本,大家并不高兴
  2. 如果只发布构建产物,那么意味着我在构建过程中会产生冗余信息,比如N个ES6编写的库如果要让构建产物直接可用,就不能使用external-helpers参数进行babel编译(所以我不明白babrel-preset-es2015-rollup为啥内置了这个plugin),那么会产生复数的helper,这种冗余带来的代价对于大型系统来说并不好受
  3. 同样的问题出在bundle和dist上,如果我发布一个bundle(把多个模块搞成一个),那么使用者如果有用不到的部分就很麻烦,基于UMD做tree shaking的技术并不成熟

所以最后我的选择是,对于小的lib发布src + bundle,对于大点的lib发布src + dist,所以你看,src还是得发上去

而在大型的系统中,我更倾向于直接使用依赖库的src(当然这很大程度上是因为我们大部分依赖库是内部自己写的,使用的工具体系相同,所以统一构建没什么压力),来解决代码冗余等问题

4. npm提供的依赖类型不够

这是个很现实的问题,事实上我们依赖一些第三方库是为了干很多事,比如:

  1. 实际运行需要
  2. 进行构建需要
  3. 跑单元测试需要

但是npm只提供了production模式,只提供了dependencies和devDependencies的区分,这让人很尴尬,一个实际我遇到的案例是:某个包依赖mocha + phantom进行单元测试,然而我们的npm包自动发布的web hook机安装不上phantom,所以这个包再也没有成功自动发布过……

我觉得npm应该适当地提供更多的依赖模式,比如给依赖进行定制化的分类管理,并且npm i --category=xxx控制安装哪一部分会更好

5. “反式依赖”功能的缺失

当然这并不是npm的弱点,至今并没有任何一款包管理工具能完成这一功能(这也是我一直和我们的TC说包管理要自己做的一个原因)

这个我自创的“反式依赖”是什么意思呢?我们试想一个场景

当前项目使用了react 0.14,现在项目需要react-tree这么个组件

当我们使用npm i react-tree的时候,我们会得到什么?

  1. 一个最新的react-tree版本
  2. 因为react-tree的最新版本的依赖是react 15.x,所以在react-tree内部有了自己的react

这是你想要的么,这个react-tree还能用么?而往往react 0.14升级到15.x几乎是没什么可能的,你并没有这么多的精力和资源

所以我们更常做的是,先去翻一翻react-tree的版本,看看哪个版本是能用在react 0.14上的,然后指定这个版本安装

这就是“反式依赖”,这里的逻辑事实上是“先有了一个不能变化的主框架版本,其他依赖需要符合这个版本”,而这一功能是当前包管理工具都无法提供的,基于现在npm的包信息也很难实现这一功能

---

以上,我明白这些问题其实实际使用中并不会经常出现(甚至有些出现了大家也并没有关注到),所以我并不反对使用npm进行前端包的管理,但以上例举的问题是切实存在的,对于可能踩到这些坑的场景,还是要给予一些考虑的

补充一个我向我们的TC报告时对包管理的期待,这里很多功能就是因为现有的包管理工具无法支持才会提出的

@倪云建

的回答不错,在此作一些补充。

Bower

bower 的缺点比较明显,最大的问题就是缺乏统一的构建机制。但有意思的是 Google 的 Polymer 选择了 bower 作为包管理器,因为 Polymer 是建立在两个还没在浏览器里普遍实现的东西上的:HTML Import 和 SPDY。HTML Import 让你可以把 HTML, CSS, JS 写在同一个 HTML 文件里作为一个组件或是模块,然后通过一行代码引入:

<link rel="import" href="my-component.html">

同时,在一个组件里也可以引入其他的组件,也可以直接引用远程服务器上的组件。某种程度上 HTML import 可以取代现在的组件模块机制。而 SPDY 是下一代的 http 协议,可以让浏览器只用一个服务器连接传输多个文件。换句话说即使你页面里有很多个 HTML import 也不会因为多次请求导致页面加载缓慢。在这两个东西存在的理想情况下,前端项目是完全可以不需要构建过程的。这是 Bower 长远来看的一个意义,但现阶段对大多数开发者而言,构建依然是一个必不可少的步骤。

Component

曾经作为 TJ 脑残粉的我是坚定的 component 使用者,但用了一段时间以后有几个比较重要的不爽之处:

- 每一个 component 都必须要在 component.json 中手动列出所有文件,每次更改项目结构或是重命名文件都很麻烦,我还为此写了个 grunt 插件专门自动做这个事情。

- component 只有一个 wiki page 列表,没有一个可搜索的中央数据库,模块的可发现度比较低。同时,github 仓库的星数是唯一的模块质量指标,而 npm 则有下载统计和被依赖数量这些更实在的数据。

- 模块发现度低带来的另一个问题就是不同作者的模块之间很少出现公用的依赖。虽然 Component 的依赖是扁平的,在实际使用别人的模块的时候依然会出现重复(同样的问题不同的实现),这就导致很多人宁可自己造轮子,自己依赖自己,只是把 Component 当个工具而不是平台用。

npm + Browserify

我这里想要指出,npm 其实是一个非常好的前端(对,没错,是前端)包管理方案,最主要的就是依靠

Browserify

这个神器。Browserify 最大的意义不是让你能在 npm 上发布前端的静态资源,而是实现前后端的代码共享。npm上有很多包是前后端通用的,比如我要找个现成的算法实现,什么 levenshtein distance 啊,perlin noise 啊,gaussian distribution 啊,A* 寻路啊,npm 上一搜一大把。常用的库如 jquery backbone 之类的,只要你想得到的基本上都有 npm 版本。需要什么直接 npm install 就可以用在浏览器端的项目里了,Component 和 Bower 在这方面跟 npm 完全没有可比性,spm 就更不提了。开发流程上来说也极其省心,项目用 CommonJS 写,不需要任何配置,给一个入口文件就行!还有一个官方工具 watchify,一行命令跑起,保存文件自动构建,连 grunt gulp 都不需要。

这个方案唯一的缺点,就是 npm 的树状依赖结构可能导致重复的模块和代码量的臃肿,需要跑一次 `npm dedupe` 来尽量压平依赖树。当然,实际情况中前端模块出现依赖同一模块的不兼容版本还是很少见的。