一张图概括前端包管理器历史:
在没有包管理器之前
正确来说Node.js是不存在没有包管理器的时期的。从A brief history of Node.js可以看到,2009年Node.js发布的时候npm的雏形也发布了。当然因为Node.js跟前端绑的很死,这里主要谈谈前端没有包管理器的时候是怎么做的。
那时候的情况基本是:
- 找到各个库的官网,比起就jQuery
- 下载zip包
- 解压,放到项目下libs的目录中,然后就可以使用了
- 想要方便的话,直接在html中使用CDN链接
模块化管理?版本号管理?依赖升级?都不存在的。
早期npm(v1&v2)
2009年Node.js诞生,npm的雏形也在酝酿。2011年5月npm发布1.0版本,2014年9月npm发布2.0版本。
早期npm主要问题:
- node_modules是嵌套结构,导致node_modules堪比黑洞
- 语义化版本带来的依赖不确定的问题
- npm cache不足,无离线模式。
早期npm的node_modules文件结构是嵌套的。如项目里依赖A、B,B依赖C。则结构如下:
这样会导致两个最头疼的问题:
- node_modules嵌套层级过深:只有找到一片不依赖任何第三方包的叶子时,这棵树才能走到尽头。因此node_modules的嵌套层级十分可怕。
- node_modules体积过大:如果一个第三方包A被两个甚至多个包依赖时,那么它就会被安装多次。这种结构导致磁盘使用很快。
这就导致node_modules堪比黑洞。。。
yarn诞生
时间来到2015年6月,npm v3发布。npm v3做了一项重大改进就是依赖提升,把node_modules改成了扁平化的结构。但是此时npm还是没有依赖锁,没有package-lock.json文件。2016年10月,yarn正式公布发行。yarn解决了当时npm最迫在眉睫的几个问题:
- 安装太慢:yarn解决方案是加缓存、多线程(后面npm也解决了这个问题)
- 无依赖锁(无确定性):添加yarn.lock文件。在2017年5月发布的npm v5中npm才添加package-lock.json文件,解决依赖锁的问题。
关于扁平化:
如果你的项目依赖A,A依赖B,B依赖C,node_modules不再是嵌套结构,将会进行依赖提升。结构如下:
扁平化后,依赖地狱的问题得到很大改善,实际需要安装的包的数量大大减少,安装速度提升很大。
关于依赖锁:
yarn.lock是yarn最大的贡献之一,直到一年之后的2017年npm才跟上脚步发明了package-lock.json文件。在没有依赖锁的年代,每一次install都可能带来巨大的依赖变动。因为npm采用语义化版本约定,简单来说a.b.c代表着:
- a主版本号:当做了重大变更或不兼容的api修改
- b次版本号:当做了向下兼容的功能性新增
- c修订号:当做了向下兼容的问题修正
问题在于这只是一个理想化的约定,具体到每个包有没有遵守,遵守的好不好,不是我们可以控制的。而默认情况下安装依赖时,得到的版本号是"^1.2.0"这样的,这代表着安装主版本号为1的最新版本。
虽然可以通过去掉"^"指定精确版本,但是无法指定二级依赖的精确版本号。因此安装仍然存在巨大的不确定性。
因此,为了解决这个问题,yarn提出了“锁”的解决方案:精确的将版本号锁定在一个值,并在安装时通过计算哈希值校验文件一致性,从而保证每次构建使用的依赖都是一致的。我们不应该手动修改yarn.lock文件,如果要升级一个包,应该使用yarn upgrade
命令。
扁平化带来的问题
node_modules扁平化解决了依赖嵌套地狱的问题,但也带来了新的问题:
- 幽灵依赖
- 多重依赖
幽灵依赖
当你的项目my-project依赖了A,而A又依赖了B,这时由于node_modules是扁平化的结构,包管理器会进行依赖提升,把B提升到my-project项目的node_modules中,结构如下:
这时就可以直接在你的项目my-project中使用B了:import B from 'B'
,但是你并没有在my-project的package.json中声明依赖B。这就是幽灵依赖问题。
幽灵依赖可能会带来以下几个问题:
- 依赖缺失:假设A升级到某个版本之后不再依赖B了,那你项目中所有依赖B的地方都会报错。
- 依赖不兼容:你的项目my-project中并没有定义B及其版本,假设A升级了依赖B,而B存在不兼容的API变更,此时你的项目中用到B的地方可能会报错。
- devdependency依赖缺失:你也可以在你的项目my-project中直接引用devdependency中的子依赖,而其他用户并不会下载devdependency,会立即报错。
多重依赖
当你的项目my-project中依赖了A、B、C、D,而A、B依赖E@1.0版本,而C、D依赖E@2.0版本,如下所示:
这个时候node_modules会怎么扁平化呢?是把E@1.0提升到my-project的node_modules下,E@2.0放到嵌套的node_modules下:
还是提升E@2.0呢?
答案是不确定的,取决于哪一个E出现的更早,更早的将被扁平化。在安装新包时,会不停的往上级node_modules中查找,如果找到相同版本的包就不会重新安装,在遇到版本冲突时才会在该包的node_modules下安装子依赖。
多重依赖可能会带来以下几个问题:
- 破坏单例模式:C、D中引入了E导出的一个单例对象,但实际上是两个不同的对象。
- 可能会导致ts的声明文件d.ts文件冲突。
因文章篇幅原因,yarn和npm更详细的时间线概述及梳理请看这篇文章:
相关文章: