npm
嵌套的 node_modules 结构
npm 在早期采用的是嵌套的 node_modules 结构,"node_modules" 文件夹通常包含项目依赖的模块。在项目中使用多个依赖并且这些依赖本身也有它们自己的依赖时,就会出现嵌套的 "node_modules" 结构。
嵌套的 "node_modules" 结构的主要特点是依赖模块被嵌套在它们父模块的文件夹内,而不是所有依赖都放在项目根目录的 "node_modules" 文件夹中。这种结构的优点是可以确保每个依赖使用的是其所需的特定版本,从而提高了版本管理的可靠性。
下面是一个示例,展示了一个包含嵌套 "node_modules" 结构的项目目录:
my-project/ |-- node_modules/ | |-- dependency-1/ | | |-- node_modules/ | | | |-- nested-dependency-1/ | | | | |-- ... | | | | | |-- ... | | | |-- dependency-2/ | | |-- ... | | | |-- ... | |-- package.json |-- package-lock.json (或 yarn.lock) |-- ...
嵌套的 "node_modules" 结构使每个依赖的版本得以隔离,避免了不同依赖之间的版本冲突。这有助于确保项目的稳定性和可维护性。项目的 "package.json" 文件和锁定文件(如 "package-lock.json" 或 "yarn.lock")负责管理依赖关系和确保正确的版本被安装。尽管嵌套的 "node_modules" 结构可以解决版本冲突问题,但也会增加项目的文件大小。
依赖地狱
"依赖地狱"是指在JavaScript项目中,特别是使用npm进行依赖管理时,可能出现的问题。这个术语描述了项目中的依赖关系变得非常复杂,难以管理和解决的情况。
依赖地狱可能会出现以下问题:
- 深层次依赖:项目的依赖关系变得非常深层次,依赖依赖于其他依赖,形成了复杂的依赖链。这使得项目的 "node_modules" 目录变得庞大,占用大量磁盘空间。
- 版本冲突:不同依赖可能依赖于同一模块的不同版本,导致版本冲突。这可能会导致代码错误、不稳定性和不一致的行为。
- 维护困难:随着项目的增长,维护复杂的依赖关系和确保所有依赖保持最新状态变得非常困难。手动解决依赖问题可能非常耗时。
- 安全性问题:复杂的依赖关系可能导致项目潜在的安全漏洞,因为某些依赖可能包含已知的漏洞,而难以及时更新。
为了减轻"NPM的依赖地狱"问题,可以采取以下一些措施:
- 使用锁定文件:使用 "package-lock.json"(对于npm)或 "yarn.lock"(对于Yarn)来确保依赖版本的一致性。这将减少版本冲突的风险。
- 定期更新依赖:定期更新依赖以获取最新版本,以确保项目保持最新和安全。可以使用工具来帮助自动化这个过程。
- 精简依赖:审查项目的依赖关系,移除不需要的依赖项,以减少项目的复杂性。
- 使用工具:使用工具如npm-check、npm audit等,帮助识别和解决依赖问题。
- 定期检查漏洞:使用漏洞扫描工具,检查项目依赖中是否存在已知的漏洞,并及时更新受影响的依赖项。
扁平node_modules结构
为了将嵌套的依赖尽量打平,避免过深的依赖树和包冗余,npm v3 将子依赖提升(hoist),采用扁平的 node_modules 结构。依赖关系被尽量保持扁平,不出现深层次的嵌套结构。在这种结构下,所有的依赖包都直接安装在项目根目录的node_modules
文件夹下,而不会出现多层嵌套的情况。
扁平的node_modules
结构的主要特点包括:
- 所有依赖直接安装在根目录:每个依赖都会被安装在项目的
node_modules
文件夹中,而不会在其他依赖的node_modules
文件夹中创建嵌套结构。 - 减小
node_modules
文件夹的大小:由于依赖没有多层嵌套,因此node_modules
文件夹的大小相对较小,占用的磁盘空间较少。 - 减少版本冲突:扁平的结构有助于减少不同依赖之间的版本冲突,因为每个依赖都有自己的副本,不会受到其他依赖的影响。
使用扁平的node_modules
结构可以解决"NPM的依赖地狱"问题,降低了依赖管理的复杂性和性能问题。这种结构在一些情况下非常有用,特别是对于大型项目或需要精确版本控制的项目。
要在npm中启用扁平的node_modules
结构,你可以使用npm
的--legacy-peer-deps
选项,例如:
npm install --legacy-peer-deps
这将强制npm使用扁平结构来安装依赖项。但这可能会导致某些依赖关系无法正常工作。
幽灵依赖
"幽灵依赖"(Phantom dependencies)是指在Node.js项目中,通过npm安装依赖时,可能会安装一些看似没有明确列出的依赖项。这些依赖项不在项目的package.json
文件中显式列出,但由于npm解析依赖树时的某些行为,它们仍然被自动安装。
Phantom dependencies可能会导致一些问题,包括:
- 不明确的依赖关系:项目的
package.json
文件不明确列出这些依赖,可能会导致其他开发者不清楚项目的实际依赖关系。 - 版本控制问题:由于这些依赖没有明确列出,可能会导致项目的依赖版本控制不精确,增加了版本冲突的风险。
- 维护困难:Phantom dependencies可能会使项目的依赖关系更加复杂,从而增加了项目的维护难度。
Phantom dependencies通常出现在以下情况下:
- peerDependencies:如果一个依赖的
peerDependencies
中包含了一些未在项目中明确列出的依赖,npm可能会自动安装这些未列出的依赖作为幽灵依赖。 - 间接依赖:如果项目的依赖具有复杂的依赖关系,其中某个依赖需要另一个依赖,但没有在项目的
package.json
中显式列出,npm可能会将这个依赖安装为幽灵依赖。
为了解决Phantom dependencies问题,可以采取以下措施:
- 审查依赖:定期审查项目的
node_modules
目录,查看是否存在未明确列出的依赖。可以使用npm ls
或yarn list
命令来列出项目的依赖树。 - 显式列出依赖:将项目中所有需要的依赖都显式列出在
package.json
文件的dependencies
或devDependencies
中,以确保依赖关系是明确的。 - 更新依赖:定期更新项目的依赖,以确保它们的版本是最新的,并修复任何Phantom dependencies。
- 使用npm audit:使用
npm audit
命令来检查项目中的依赖是否存在已知的安全漏洞,包括Phantom dependencies。例如:
{ "dependencies": { "A": "^1.0.0", "C": "^1.0.0" } }
由于 B 在安装时被提升到了和 A 同样的层级,所以在项目中引用 B 还是能正常工作的。
依赖分身
"依赖分身"(Doppelgangers)是指在Node.js项目中,通过npm安装依赖时,可能会出现多个版本的同一依赖包同时存在于项目的node_modules
目录中。这些不同版本的依赖包通常来自于项目的直接依赖和间接依赖,而且它们的版本可能不一致。
依赖分身问题可能会导致以下问题:
- 版本冲突:不同版本的同一依赖包可能不兼容,导致项目出现错误或不稳定。
- 磁盘占用:多个版本的依赖包会占用磁盘空间,增加项目的体积。
- 不稳定性:依赖分身可能导致项目行为不一致,因为不同的代码路径使用了不同的依赖版本。
解决依赖分身问题的方法包括:
- 使用npm ls命令:使用
npm ls
命令查看项目的依赖树,识别哪些依赖存在多个版本。 - 更新依赖:尽量更新项目的依赖,使它们使用最新版本,从而减少版本冲突的可能性。
- 删除重复依赖:手动删除项目中不必要的重复依赖,可以使用
npm dedupe
命令来自动解决一些问题。 - 锁定依赖版本:使用
package-lock.json
或yarn.lock
等锁定文件,以确保项目中使用的依赖版本一致。 - 使用npm audit:使用
npm audit
命令来检查项目的依赖是否存在已知的安全漏洞,包括依赖分身。 - 使用npm ci:对于生产环境构建,可以使用
npm ci
命令来快速、可靠地安装依赖,减少依赖分身的问题。
不确定性
同样的 package.json 文件,install 依赖后可能不会得到同样的 node_modules 目录结构。如果有 package.json 变更,本地需要删除 node_modules 重新 install,否则可能会导致生产环境与开发环境 node_modules 结构不同,代码无法正常运行。
yarn
Yarn是一个用于管理JavaScript项目依赖的包管理工具,它在npm之后推出,旨在提供更快、可靠和安全的依赖管理。yarn也是扁平化的 node_modules 结构,没有解决幽灵依赖和依赖分身问题。
特点
- 快速安装:Yarn的并行安装和缓存机制使依赖包的安装速度更快。它能够并行下载多个依赖,从而提高了安装效率。这对于大型项目或拥有大量依赖的项目特别有用。
- 版本锁定:Yarn使用一个
yarn.lock
文件来确保依赖包的版本在不同环境中一致。这有助于避免不同开发者之间或不同部署环境之间的依赖版本冲突问题。 - 可靠性:Yarn的依赖解析算法更可靠,可以避免一些与npm相关的问题,如依赖分身(Doppelgangers)和幽灵依赖(Phantom dependencies)。
- 离线支持:Yarn允许你在没有网络连接的情况下安装依赖,前提是你已经在先前的安装中缓存了这些依赖。这对于在没有互联网连接的环境中工作的开发者来说非常有用。
- 安全性:Yarn提供了一个
yarn audit
命令,用于检查项目中的依赖是否存在已知的安全漏洞,并提供修复建议。这有助于提高项目的安全性。 - 易于使用:Yarn的命令行界面(CLI)与npm类似,因此对于熟悉npm的开发者来说,学习曲线较低。同时,Yarn还提供了一些额外的功能和命令,如
yarn workspaces
用于管理多包存储库。 - 工作区支持:Yarn支持工作区(workspaces),允许你更轻松地管理多个相关项目或包存储库之间的依赖关系。这对于使用monorepo风格的项目非常有用。
- 插件支持:Yarn支持插件,可以通过安装插件来扩展其功能。这允许开发者根据需要自定义和扩展Yarn。
pnpm
与yran和npm的改进
- 硬链接和符号链接:pnpm使用硬链接(hard links)和符号链接(symbolic links)来重复使用相同依赖的实例,而不是为每个项目复制依赖。这降低了磁盘空间的占用,减少了依赖包的复制。
- 共享存储:pnpm引入了一个全局的依赖存储位置,称为"store",它可以跨多个项目重复使用依赖。这减少了网络下载和本地磁盘占用,特别是对于拥有多个项目的开发者。
- 并行安装和更新:pnpm支持并行安装和更新依赖,这意味着它可以更快地同时处理多个依赖的安装和更新操作。
- 自动垃圾回收:pnpm具有内置的垃圾回收机制,定期清理不再需要的依赖,以释放磁盘空间。
- 可选版本锁定:pnpm提供了一种可选的版本锁定模式,开发者可以根据需要灵活选择是否锁定依赖版本。这使得pnpm适用于更广泛的项目需求。
- 快速启动:pnpm引入了"快速启动"(fast unpacking)功能,可以更快地启动项目,减少等待时间。
- 兼容性:pnpm声称与npm和Yarn的生态系统兼容,因此可以在现有项目中无缝切换到pnpm。
- CLI命令:pnpm提供了与npm和Yarn类似的CLI命令,使其易于学习和使用。
内容寻址存储
内容寻址存储(Content-Addressable Storage,CAS)是一种存储依赖包的方法,它将每个包的内容哈希(hash)后,将其存储在一个具有唯一哈希地址的存储库中。这种方法与传统的文件复制方式不同,其中每个项目都会在node_modules
目录下存储一份完整的依赖包。
CAS的主要特点和优势包括:
- 节省磁盘空间:CAS只存储每个包的内容一次,不管有多少个项目依赖于它。这降低了磁盘空间的占用,特别是对于拥有多个项目的开发者。
- 高效的网络下载:CAS允许重复使用相同依赖的实例,因此不需要多次下载相同的依赖包,这提高了网络下载的效率。
- 依赖隔离:每个项目的
node_modules
目录都包含对CAS中特定哈希地址的引用,这意味着每个项目的依赖是隔离的,不会相互干扰。 - 版本锁定:CAS结合哈希地址可以确保依赖版本的一致性。只要包的内容不变,哈希地址就不变,因此可以避免版本冲突。
- 垃圾回收:CAS有内置的垃圾回收机制,定期清理不再需要的依赖,以释放磁盘空间。
总结
npm、Yarn 和 pnpm 都是 JavaScript 包管理工具,用于下载、安装和管理 JavaScript 包和依赖。它们有各自的优势和劣势,选择使用哪个取决于项目需求和个人偏好。
以下是它们之间的主要区别以及优劣势:
npm (Node Package Manager):
- 优势:
- npm 是 Node.js 官方提供的包管理工具,它是默认的包管理器。
- 具有广泛的社区支持和生态系统,包括大量的开源包和模块。
- 支持自定义脚本,可以用于构建和测试。
- 兼容 CommonJS 模块规范。
- 劣势:
- npm 在性能方面相对较慢,尤其是在安装大量依赖时。
- 在安装和删除依赖时,会产生大量的中间文件和依赖。
Yarn:
- 优势:
- Yarn 由 Facebook、Google 和 Exponent 合作开发,旨在提高性能和可靠性。
- 支持并行下载,安装速度更快。
- 锁定依赖版本的机制更可靠,可以确保不同开发环境中的一致性。
- 有一个简化的 CLI。
- 劣势:
- 相对于 npm,Yarn 的生态系统稍小一些,但已经在快速增长。
pnpm (Plug'n'Play):
- 优势:
- pnpm 的最大优势是极低的磁盘占用和更快的安装速度,因为它不会创建大量的中间文件,而是将包存储在全局缓存中。
- 它具有优秀的支持,包括与 npm 生态系统的兼容性。
- 劣势:
- 尽管 pnpm 可以显著减小磁盘占用,但它的生态系统相对较小,一些依赖可能不完全兼容。
- 部分工具和 CI/CD 环境可能需要一些额外配置以支持 pnpm。
何时使用:
- 使用 npm:当你需要兼容性好的默认包管理器,并且你不太关心性能差异时,npm 是一个不错的选择。它在大多数情况下都能正常工作。
- 使用 Yarn:当你需要更快的安装速度、更可靠的版本控制和并行下载时,Yarn 是个不错的选择。它适用于大型项目和需要更严格依赖管理的情况。
- 使用 pnpm:当你关心磁盘占用和更快的安装速度,而且你的项目兼容性较好时,可以考虑使用 pnpm。尤其在容器化环境下,它可以显著减小镜像大小。