npm2 npm3 yarn 的故事


虽然 Node 和 npm 那一套用了很久了,但是也都仅仅敲敲命令而已,今天抽空把 npm 官网文档看了一遍,简单记录下吧

模块和包

很多时候我们并不是很区分模块和包,因为我们一般使用场景就是 npm install xxx,然后在文件里直接 require('xxx')。但是考虑到很多时候我们也可以随意 require 一个本地自己写的 JS 文件,只要其按照 CommonJS 规范 export 即可,因此这里需要严格区分下:

  • 模块:符合 CommonJS 规范的文件
  • 包:一个包含 package.json 以及入口文件的文件夹

这里也不是绝对这样定义的,不过最终表现出来的就是这些,而 npm 的所有的管理的对象都必须包含 package.json 文件,用于模块确定依赖关系。

说了这么多,只是为了强调:npm 是包管理器。

npm2 问题

npm2 安装依赖的时候比较简单直接,直接按照包依赖的树形结构下载填充本地目录结构。

因为 npm 设计的初衷就是考虑到了包依赖的版本错综复杂的关系,同一个包因为被依赖的关系原因会出现多个版本,简单地填充结构保证了无论是安装还是删除都会有统一的行为和结构。

比如一个 App 里模块 A 和 C 都依赖 B,无论被依赖的 B 是否是同一个版本,都会生成对应结构:

于是缺陷就凸显出来了,太深的目录树结构会严重影响效率,甚至在 Windows 下可能会超出系统路径限制的长度。另外,在 Windows 有删 node_modules 目录经历的可能都经历过漫长的等待。

npm3 解决方式

针对 npm2 的问题,npm3 加了点算法,直白的解释就是:npm install 时会按照 package.json 里依赖的顺序依次解析,遇到新的包就把它放在第一级目录,后面如果遇到一级目录已经存在的包,会先判断版本,如果版本一样则忽略,否则会按照 npm2 的方式依次挂在依赖包目录下

还是刚刚的栗子,可以看下 npm2 和 npm3 生成的结构对比:

试想,在包版本差异化不太严重的情况下,这种构建方式会几乎把所有包放在一级目录下,很大程度上提升了效率以及节省了部分磁盘空间。

其实,npm3 这种方式在理论上其实会趋于一种平稳的状态,因为你可能会说,npm3 在极端情况下也可能退化为 npm2 的行为,不过这种情况在一般情况下是可以忽略的。npm3 还有个优点,就是在动态安装更新包的时候,是可以进一步调整目录结构的,比如某种依赖已经如下:

具体依赖细节我们不用追究,假设 E_v1.0 模块是依赖 B_v1.0 的,此时我们更新 E 到 v2.0,假设此时依赖 B_v2.0 了,那么最终生成的结构会是如下:

是不是觉得很多冗余?其实只需执行下 npm dedupe 就会变成如下结构:

这已经很接近我们理想的使用场景了!

npm3 新的问题

你以为就这么完了吗?注意到上面提到的 npm3 会按照 package.json 的顺序解析目录树,试着看下下面的场景:

对应的 dependencies 为:

"dependencies": {
  "mod-a": "^1.0.0",
  "mod-c": "^1.0.0",
  "mod-d": "^1.0.0",
  "mod-e": "^1.0.0"
}

如果恰好 A_v1.0 依赖 B_v1.0,然后我们本地升级了 A 到 v2.0,假设此时依赖 B_v2.0,那么此时目录结构会变成:

而此时部署到测试平台呢?因为 mod-a 在第一个,所以会优先解析,也就是 B_v2.0 会优先占据一级目录,最终可能目录结构为:

开发环境和测试环境的 node_modules 目录结构不一样了!!这个问题很可能会导致一些很微妙的问题,而且很难调试。如何解决呢?就是本地每次安装或者升级包后,完整删除 node_modules 目录然后再 install 一次……(感觉比 npm2 还粗暴)

新的工具 yarn

除了上面的问题,还有个严重的问题。npm 在使用的时候大多是用语义化版本号管理包依赖的,比如 ~1.0.0 表示只更新补丁,但是世界辣么大,什么人没有?说不定哪个开发者就在 patch version 上就搞了 major 的升级,即使你本地使用固定版本号也无济于事。

当然,后面 npm 也有 shrinkwrap 机制来保证这种一致性,不过说实话 npm-shrinkwrap.json 略难看,基本属于给 npm 打补丁,让我在一个项目引入这个几乎无法 review 的文件肯定会不开心的。

太多因素导致了 npm 已经步履维艰了,估计 Facebook 也累了吧,于是前不久搞了 yarn 用来替代 npm 了。

我觉得 yarn 革命性的更改在于其改变了构建的步骤,其它有点都是新构建方式的副产物,yarn 构建步骤如下:

  • Resolution: 向仓库请求依赖关系
  • Fetching: 看看本地缓存了没有,否则把包拉到缓存里
  • Linking: 直接全部从缓存里构建好目录树放到 node_modules 里

这里的缓存机制很像 mvn 之类的,而且其还引入了 lockfile 用于锁定版本号,这很类似 shrinkwrap,不过格式比 npm-shrinkwrap.json 更好 review。除了这些特别明显的改进,还有很多体验上的提升,具体可以看官方博客

以上,凭借前端近几年的发展,我觉得故事还没完……