一、npm/yarn install 原理
npm/yarn install 时发生了什么?主要分为两个部分,首先是:包如何到达 node_modules 当中,其次是:node_modules 内部是如何管理依赖的;
执行命令以后,首先会构建依赖树,然后针对每个节点下的包,会经历四个步骤:
- 将依赖包的版本区间解析为某个具体的版本号
- 下载对应依赖的 tar 包到本地离线镜像
- 将依赖从离线镜像解压到本地缓存
- 将依赖从缓存拷贝到当前目录的 node_modules 目录
然后,对应的包就会到达项目的 node_modules 中。
那么,这些依赖在 node_modules 当中是什么样的目录结构呢?也就是我们上面输的 依赖树是什么样子呢?这个要看下面详细讲解,不同的版本是不一样的!
二、npm
我们按照 npm 包管理工具的发展历史,从 npm2 开始讲起:
用 node 版本管理工具把 node 版本降到 4,那 npm 版本 就是 2.x了
然后我们新建一个Demo,执行下 npm init -y,快速创建一个 package.json。
然后执行 npm install express ,那么 express 包和它的依赖都会被下载下来:
展开 express,它也有 node_modules
再展开几层,我们会发现每个依赖都有自己的 node_modules
也就是 npm2 的 node_modules 是嵌套的。
ode_modules └─ express ├─ index.js ├─ package.json └─ node_modules └─ accepts ├─ index.js └─ package.json
这种结构就是我们上面所说的 依赖树!
现在的 accepts 当中又有依赖,然后就会继续嵌套下去。试想一下这样的设计存在什么问题呢?
依赖层级太深,导致致命的问题是 Windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 Windows 路径的长度限制的。
大量重复的包被安装,文件体积超大。比如跟 express 统计目录下有一个 foo,两者都依赖于同一个版本的 Lodash,那么 Lodash 会分别在两者的 node_modules 中被安装,也就是重复安装,会占据比较大的磁盘空间。
模块实例不能共享。比如 React 有一些内部变量,在两个不同包引入的 React 不是同一个模块实例,因此无法共享内部变量,导致一些不可预知的 bug。当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:↓
三、yarn
yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题呢?
铺平,也就是说所有的依赖不再一层一层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也不会存在路径过长的问题了;
我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:
这时候 node_modules 就是这样了:
全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:
所有的依赖都被拍平到 node_modules目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的 node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。
但是多展开几个依赖包,大家会发现,为什么还会有嵌套呢?
因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。
npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似。当然,yarn 还实现了 yarn.lock 来锁定版本,这个功能 npm 也实现了。
但是 yarn 和 npm 都采用铺平了的方案,这种方案就没有问题了吗?
并不是,它照样还是存在诸多问题的,梳理一下:
依赖结构的不确定性。
扁平化算法本身的复杂性很高,耗时较长。
幽灵依赖( 通俗一点的说:可以非法访问没有声明过依赖的包 )
后来两个都好理解,但是第一点的 不确定性 该如何理解呢?
假如现在项目依赖两个包 Barry 和 Lishen,这两个包的依赖又是这样的:
那么 npm / yarn install 的时候,通过扁平化处理之后究竟是怎么样子?
究竟是这样?
还是这样呢?
答案是:都有可能,取决于 Barry 和 Lishen 再 package.json 中的位置,如果 Barry 声明在前则是前面的结构,否则就是后面的结构。这就是为什么依赖会产生依赖结构的 不确定 问题,也就是前面说的 lock 文件诞生的原因,无论是 package-lock.json (npm 5.X 以后才有的,也就是npm3)还是 yarn.lock ,都是为了保证 install 之后都产生确定的 node_modules 结构。
那 pnpm 是怎么解决这俩问题的呢?
四、pnpm
回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?
那如果不复制呢,比如通过 link。
首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。
如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?
这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。
没错,pnpm 就是通过这种思路来实现的。
再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。
你会发现它打印了这样一句话:
包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。
我们打开 node_modules 看一下:
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。
展开 .pnpm 看一下:
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之前的依赖关系是通过软链接组织的。 .pnpm/node_modules 下的 expresss,这些都是软链接。
也就是说,所有的依赖都是从全局 store 硬连接到了 .pnpm/node_modules 下,然后之间通过软链接来相互依赖。
官方给了一张原理图,配合着看一下就明白了:pnpm官方地址
这就是 pnpm 的实现原理。
那么回过头来看一下,pnpm 为什么优秀呢?
首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。
其次就是快,因为通过链接的方式而不是复制,自然会快。
五、更好的实用性
说了这么多,估计你会觉得 pnpm 挺复杂的,是不是用起来成本很高呢?
恰好相反,pnpm 使用起来十分简单,如果你之前有 npm/yarn 的使用经验,甚至可以无缝迁移到 pnpm 上来。
仅仅是这些吗?下面还有更重要的就是项目管理方式的支持
个人感觉:pnpm 可以更好的支持 Multirepo 项目过渡到 Monorepo 项目,原因如下:
如果 A 依赖 X,B 依赖 X,还有一个 C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。
总结
当然,今天只是深入的学习,那种包管理方式更适合大家具体情况还是要看我们自己的项目,可能因人而异,因项目而异,因业务而异~~~
欢迎大家一起讨论学习~~~