在Node.js生态系统中,包管理器是至关重要的组件之一,它们负责维护各种应用程序和库之间的依赖关系。npm是Node.js的默认包管理器,它的初始版本是npm1,但是它很快就被npm2所取代。
关于npm2最初作为包管理管理,采用的是node_modules嵌套模式,即每个包都会有自己独立的node_modules,且会将各自依赖进行安装,依赖的依赖也会产生自己的node_modules,这样就产生了“嵌套依赖”。
就像回调嵌套一样,容易陷入回调地狱,嵌套依赖也不例外。
这种嵌套依赖的模式,虽然可以使依赖项的版本更加明确和稳定,但是在实际应用中也存在一些问题。其中最大的问题是包的嵌套层级很深,这可能会导致安装和更新依赖项的时间变长,并增加包的大小。此外,由于每个包都有自己的node_modules文件夹,这可能会导致文件系统中出现大量重复的依赖项,从而占用更多的磁盘空间。
在实际操作中,当需要在特定的Node.js版本中使用npm2时,可以使用Node Version Manager (nvm)来管理多个Node.js版本。例如,在切换到Node.js v4.0版本时,对应的npm版本是npm2.x。
为了更好地说明嵌套依赖的问题,我们可以通过安装koa来演示。koa是一个基于Node.js的Web应用程序框架,它有许多依赖项,我们可以使用以下命令来安装koa:
npm install koa
在安装koa时,npm会自动下载和安装所有必需的依赖项,并将它们安装到koa的node_modules文件夹中。如果我们检查koa的node_modules文件夹,我们会发现它包含了大量的依赖项,这些依赖项中又包含了更多的依赖项,导致整个文件夹的嵌套层级变得很深。
对于多包之间会存在公共依赖,如果对于每个依赖都生成自己独立的node_modules,那么就会对相同包重复安装多次,这就会占据很大的磁盘空间。且无限嵌套,也会超过windows的最大文件路径长度限制(265个字符)。
嵌套依赖项的模式是npm2中的一个特性,虽然可以保证依赖项的版本稳定性和精确性,但是它可能会导致嵌套层级变得很深,并占用大量的磁盘空间。
我们想到,既然树形结构存在弊端,为什么不将依赖包在根node_modules进行扁平化处理,这不就解决了依赖嵌套、依赖重复和路径限制问题了?
此时新方式yarn就横空诞生。
当使用yarn进行依赖管理时,我们可以看到所有依赖都会被安装在根目录下的node_modules文件夹中。与npm2不同的是,yarn采用了扁平依赖项的模式,这意味着相同的依赖包只会被安装一次,并且不会存在多个嵌套的node_modules文件夹。
使用yarn add koa进行安装,可以看到通过yarn进行管理的依赖全部平铺在根node_modules下,且没有重复依赖安装的问题。
但是,当某些依赖包存在多个版本时,yarn会将其中一个版本提升到根node_modules文件夹中,而其他依赖包则会继续维护自己的版本。这可能会导致某些依赖包无法正常工作,因为它们可能需要使用特定版本的依赖包。为了解决这个问题,yarn仍然需要使用嵌套的node_modules文件夹,以确保每个依赖包使用正确的版本。
值得注意的是,yarn采用的扁平依赖项模式具有许多优点,例如更快的安装速度,更少的磁盘空间占用和更少的依赖冲突问题。此外,yarn还提供了一个lock文件,该文件记录了所有依赖项的确切版本和位置,以确保依赖项的版本稳定性和一致性。
yarn的变与不变:
yarn采用了更加高效和可靠的依赖项管理方式,可以有效地避免依赖冲突和嵌套的问题。但是,对于某些多版本依赖包,yarn仍然需要使用node_modules嵌套的方式来确保每个依赖包都使用正确的版本。
npm3在2015年发布时引入了一种新的依赖项安装算法,称为“扁平依赖项”。其主要原理是通过将所有依赖项都放置在同一个目录下,并使用符号链接来实现依赖项的共享。
在npm3中,所有依赖项都被直接安装到根目录下的node_modules中,而不是像npm2一样在每个依赖包中嵌套一个node_modules目录。这种扁平化的结构可以减少依赖项的嵌套层级,从而降低了磁盘空间的占用和文件路径的长度。在这种模式下,所有依赖项都被安装到顶级node_modules文件夹中,这样就避免了嵌套依赖项的问题。这种模式虽然简单,但是它可能会导致依赖项的版本不稳定,从而可能会导致依赖冲突的问题。
当我们使用npm3安装koa包时,它会首先检查该包所需的所有依赖项是否已经安装,如果没有安装,则会将这些依赖项直接安装到根目录下的node_modules目录中。同时,npm3会使用符号链接将这些依赖项链接到需要使用它们的包的node_modules目录下。
通过使用符号链接,npm3可以实现依赖项的共享,从而避免了依赖项的重复安装和占用大量的磁盘空间。此外,npm3还支持npm shrinkwrap命令,可以生成一个lockfile文件,记录每个包所使用的依赖项的精确版本号,从而避免了版本冲突和不兼容的问题。
shrinkwrap 文件的作用是什么?
这个文件用于记录整个依赖树的结构和依赖包的版本信息,可以保证依赖包的版本稳定性和一致性。
那么使用扁平化方案就能完美解决以上问题吗?当然不是。
什么是幽灵依赖?
在安装和使用某个第三方包时,该包依赖的其他依赖没有在它的js文件中显式引入的情况。这些依赖可能在代码中被引用,但是没有被包含在软件包的package.json文件中。这种情况被称为“幽灵依赖”。
举个例子,假设有个项目需要依赖包 A 和 B,而这两个包都依赖于包 C,但是包 A 依赖于包 C 的版本 1.0.0,而包 B 依赖于包 C 的版本 2.0.0。在 npm2 中,这两个版本的包 C 会被分别安装在 A 和 B 的 node_modules 目录下,不会产生冲突。但在 npm3 中,这两个版本的包 C 可能会被安装在同一个 node_modules 目录下,这时候就会产生冲突,导致代码无法运行。
虽然在npm3提供了 npm dedupe 命令,可以是手动输入命令将重复的依赖项合并到顶层 node_modules 目录下,避免了幽灵依赖的问题。但是好像并没有很智能。
总的来说,npm3通过采用扁平化的依赖管理结构和符号链接机制,引入 shrinkwrap 文件实现了依赖项的共享和版本精确控制,并且减少了依赖项的嵌套层级和磁盘空间占用。可以手动使用 dedupe 命令等方式,解决了 npm2 中出现的幽灵依赖问题,提高了包管理的效率和可靠性。
针对上面遗留下的两个问题,pnpm横空出世,采用硬链接和符号链接来管理依赖项,以减少重复下载和占用空间,从而有效地解决幽灵依赖和磁盘浪费的问题。
link:也就是软硬连接,这是操作系统提供的机制。
具体来说,当使用 pnpm 安装koa包的依赖项时,它会首先检查系统上是否已经安装了所需的依赖项。如果已经安装,则 pnpm 将创建一个符号链接到该依赖项,而不是在当前项目中复制该依赖项。这样就避免了重复下载和占用磁盘空间的问题。
我们在命令行输入:
pnpm add koa
此外,pnpm 还支持不同的包引用方式,如路径引用和 git 仓库引用,这使得 pnpm 可以更快地安装依赖项并减少重复下载,从而提高开发效率和依赖项管理的可靠性。通过将包从全局 store 进行硬链接到项目的虚拟 store 中,pnpm 可以避免多次拷贝文件和深度嵌套路径过长的问题,从而进一步减少磁盘空间的占用和提高性能。
PNPM 的核心思想是在整个项目内共享依赖项,而不是每个项目都拥有自己的依赖项副本。
这是官方文档提供的原理图:
可以看到有个公共的依赖包安装池,然后通过软链接引入到各个项目所需要的依赖中,这样就减少了幽灵依赖、依赖嵌套和重复下载的问题。
PNPM的优点如下:
节省磁盘空间:pnpm采用链接的方式将依赖项共享到全局store中,避免了每个项目都需要拷贝一份依赖包的问题,从而显著减少了磁盘占用空间。
提升安装速度:pnpm不需要每次都下载相同的依赖项,而是从全局store中直接链接到各个项目中,因此可以极大地提高安装速度。
避免了幽灵依赖、重复依赖和依赖嵌套:pnpm采用链接的方式,避免了项目之间依赖相同包不一致的问题,同时避免了重复安装相同版本的依赖项和依赖嵌套的问题。
支持多种包引用方式:pnpm支持路径引用和git仓库引用,可以更加灵活地管理依赖项。
天生支持monorepo管理:得益于pnpm的软链接特性,可以在同一个workspace下共享依赖和模块等。
另外,对于存储大量依赖的情况,pnpm提供了「pnpm store prune」命令,可以定期清理不再使用的依赖项,释放磁盘空间。
最后对不同包管理器的优缺点、特点做了一些总结:
学而知不足,水平有限,还望诸君多多指教。觉得文章不错的读者,不妨点个关注,收藏起来上班摸鱼的时候品尝。