目录
- 一、介绍
- [二、npm 不同版本](#二、npm 不同版本)
-
- [2.1 从v1到v2](#2.1 从v1到v2)
- [2.2 从v2到v3](#2.2 从v2到v3)
- [2.3 从v3到v4](#2.3 从v3到v4)
- [2.4 从v4到v5](#2.4 从v4到v5)
- 二、扁平化依赖管理基本原理及其存在的问题
- [三、npm ls:依赖关系树](#三、npm ls:依赖关系树)
- [四、pnpm:performant node package manager](#四、pnpm:performant node package manager)
-
- [4.1 介绍](#4.1 介绍)
- [4.2 依赖管理机制](#4.2 依赖管理机制)
一、介绍
在 WHAT - npm和npx 中我们介绍过 npm,可以跳转阅读。
简而言之,npm(Node Package Manager)是用于管理Node.js包和依赖项的工具。
可以通过 npm --version
命令来查看当前系统版本:10.5.0
。今天我们主要介绍最开始的差异较大的几个版本,后续版本更多是关于性能和稳定性上的优化。
二、npm 不同版本
随着时间的推移,npm的不同版本发展出了许多变化和改进,以解决旧版本中存在的各种问题和限制。
以下是npm不同版本发展中的主要差异和一些问题的解决方案:
2.1 从v1到v2
在npm v2中,改进了依赖解析算法,解决了在v1中出现的潜在依赖冲突和版本冲突的问题。这意味着更准确地解析依赖项,减少了安装过程中的错误和冲突。
在 npm v1 中,依赖解析算法相对简单,主要基于深度优先搜索 (DFS)。当安装一个包时,npm v1 会按照包的依赖关系树逐级深入,然后解析和安装每个依赖项。在这个过程中,npm v1 会尽可能地满足每个依赖项的版本需求,但不会考虑到依赖项之间的冲突或者重复安装的情况。
具体来说,npm v1 的依赖解析算法会按照以下步骤进行:
-
递归解析依赖:从根级包开始,npm v1 会递归地解析依赖关系树。对于每个依赖项,它会查找并安装满足版本需求的包。
-
依赖版本决定:当解析依赖时,npm v1 会选择满足依赖项版本范围的最新版本。如果某个依赖项已经安装了一个版本,而另一个依赖项需要一个更高版本的包,npm v1 会安装这个新版本,而不考虑可能的冲突。
-
重复安装:npm v1 不会检查已经安装的依赖项,因此可能会导致同一个包的不同版本在项目中重复安装,这可能会引发冲突或者不一致的问题。
相比之下,在 npm v2 中,依赖解析算法得到了改进。npm v2 引入了一个更智能的算法,它会考虑到依赖项之间的关系和可能的冲突。具体来说,npm v2 会根据依赖项的版本需求和依赖关系,尽可能地保证依赖树的平坦化,避免不必要的依赖冲突和重复安装。这样可以更准确地解析依赖项,避免因为版本冲突而导致的问题。
npm v2还引入了scoped packages的概念,允许开发人员创建私有作用域的包,以便更好地管理和组织代码。
Scoped packages 允许开发者将包放置在特定的作用域下,以便更好地管理和组织代码。在npm中,作用域是以 @
符号开头的名称,例如,@myorg/mypackage
。
具体来说,scoped packages 具有以下特点:
-
独立的作用域:每个作用域都是独立的,因此不同作用域下的相同名称的包不会发生冲突。
-
私有访问:scoped packages 可以是公共的(公开到npm注册表)也可以是私有的(存储在私有注册表或者 npm Enterprise 中),因此可以用来管理项目内部的私有依赖项。
-
命名空间:作用域可以被用作命名空间,帮助开发者更好地组织和命名自己的包,以防止与其他包发生命名冲突。
-
权限控制:如果一个作用域中的包是私有的,开发者可以通过设置适当的访问权限来控制谁可以访问和使用这些包。
通过引入scoped packages,npm v2 使得开发者能够更灵活地组织和管理他们的包,尤其是在大型项目中或者需要管理私有依赖项时。
2.2 从v2到v3
npm v3引入了平面化的依赖结构,减少了依赖树的深度,提高了安装速度和效率。这解决了在以前版本中由于依赖树过深而导致的性能问题。
在使用早期的 npm v1 或则 npm v2 安装依赖,node_modules
文件夹会以递归的形式呈现,严格按照 package.json
结构以及次级依赖的 package.json
结构将依赖安装到它们各自的 node_modules
中,直到次级依赖不再依赖其它模块。
存在一个问题,假设项目的中的两个依赖同时依赖了相同的次级依赖,那么它们二者的次级依赖将会被重复安装。这只是简单的场景,在真实的开发场景中其问题还会更加恶劣:依赖层级太深,重复的包被安装,导致 node_modules
体积巨大。
从 npm3 或 yarn 开始,不再是以往的"嵌套式"而是采用了"扁平化"方式去管理项目依赖。
假设 foo1 和 foo2 都依赖了 bar,依赖安装后呈现的是下面的这种扁平化目录:
node_modules
├─ bar
│ ├─ index.js
│ └─ package.json
├─ foo1
│ ├─ index.js
│ └─ package.json
└─ foo2
├─ index.js
└─ package.json
此外,npm v3还增加了对Node.js v6的支持,以及一些性能优化和bug修复。
2.3 从v3到v4
npm v4主要是关于性能和稳定性的改进。它通过减少安装时的锁文件大小和更好地处理符号链接来提高了性能和稳定性。
此外,npm v4还修复了一些在早期版本中出现的安全漏洞和bug,并改进了文档和用户体验。
总的来说,npm的不同版本在功能、性能和稳定性方面都有所改进,旨在提供更好的开发体验和更可靠的包管理。升级到最新版本通常是一个好主意,因为它会带来更多的功能和改进,并解决旧版本中存在的问题。
2.4 从v4到v5
在 npm v5 中,npm 引入了 package-lock.json
文件。该文件旨在跟踪被安装的每个软件包的确切版本,以便产品可以以相同的方式被 100% 复制(即使软件包的维护者更新了软件包)。
这解决了 package.json 一直尚未解决的特殊问题。在 package.json 中,我们使用 semver 表示法设置要升级到的版本(补丁版本或次版本),这样,开发者无需将 node_modules 文件夹(该文件夹通常很大)提交到 Git,但是当我们尝试使用 npm install 命令在另一台机器上复制项目时安装依赖时,会发现并不是 100% 复制,如果指定了 〜
语法并且软件包发布了补丁版本,则该软件包最新补丁版本会被安装,^
语法同理,这就导致两个机器的项目实际上是不同的。即使补丁版本或次版本不应该引入重大的更改,但还是可能引入缺陷。
所以,在引入了 package-lock.json
文件之后,会固化当前安装的每个软件包的版本,当运行 npm install
时,npm 会使用这些确切的版本。
package-lock.json
文件需要被提交到 Git 仓库,以便被其他人获取(如果项目是公开的或有合作者,或者将 Git 作为部署源)。
二、扁平化依赖管理基本原理及其存在的问题
通过前面的学习,我们知道从 npm3 或 yarn 开始,不再是以往的"嵌套式"而是采用了"扁平化"方式去管理项目依赖。
具体来说,当安装依赖项时,会根据依赖项的版本范围以及已经安装的其他同名依赖,选择一个合适的版本并提升到依赖项的共同祖先处。
即如果存在多个依赖项需要相同的刺激依赖,但他们的版本范围不一致,会根据版本范围选择一个满足条件的版本,并将其提升到共同祖先处,这样可以避免版本冲突和重复安装,同时保证依赖树的扁平化。如果同名次级依赖的版本无法找到满足所有依赖项版本范围需求的版本,则可能出现安装失败和警告,提示存在版本冲突,需要手动解决。
因此,虽然 npm v3 引入了扁平化依赖方案以改善依赖树的深度和性能,但它也可能会引发一些问题,例如:
-
版本冲突增加:尽管扁平化依赖可以减少依赖树的深度,但它也可能导致更多的版本冲突。因为相同的依赖项的不同版本可能在不同的包里被依赖,这增加了解决版本冲突的复杂性。
-
隐式依赖:通常指项目中使用到了依赖项,但并没有在 package.json 中进行明确声明。比如,如果两个一级包都包含了同一个依赖,并且该依赖被提升到了共同祖先处(即一级),那么即使在 package.json 文件中没有明确声明这个依赖,它仍然可以在代码中被引入和使用,这很容易导致某个包在未在 package.json 中声明的情况下去使用它,当该依赖不再使用,使用它的包也将出现问题。需要注意的是,虽然可以在代码中引入和使用被提升的依赖项,但最好还是在 package.json 文件中明确声明所有的依赖项,以确保项目的依赖关系清晰可见,便于维护和管理,减少项目的不稳定性。
三、npm ls:依赖关系树
要查看当前项目的整个 npm 依赖关系树,你可以执行以下步骤:
-
使用命令行工具进入项目目录 :首先,在命令行中进入你的项目目录。你可以使用
cd
命令切换到你的项目目录中。 -
运行 npm ls 命令 :在项目目录中运行
npm ls
命令。这个命令会列出当前项目的整个 npm 依赖关系树。 -
可选:使用更详细的选项 :你可以根据需要添加一些选项来获得更详细的输出。例如,
npm ls --depth=0
将只显示一级依赖项,而npm ls --depth=1
将显示一级和二级依赖项,以此类推。 -
可选:将输出导出到文件 :如果依赖关系树很大,可能会有很多输出,你可以将输出导出到一个文件中以便稍后查看。例如,你可以使用
npm ls > dependencies.txt
将输出保存到一个名为dependencies.txt
的文件中。
通过运行 npm ls
命令,你可以查看当前项目的整个 npm 依赖关系树,从而更好地了解项目中使用的所有依赖项及其关系。
四、pnpm:performant node package manager
4.1 介绍
PNPM 是一种替代 npm 和 Yarn 的包管理器。它的名称中的 "PNPM" 代表 "Performant Node Package Manager",意味着它旨在提供更高性能的包管理功能。
另外,pnpm 兼容 npm 的大多数功能和命令,可以无缝切换和迁移现有的 npm 项目,而不需要修改现有的配置和代码。
PNPM 与传统的 npm 和 Yarn 有几个主要区别:
-
扁平化依赖:与 npm v3+ 类似,PNPM 使用扁平化依赖方案,尽可能减少依赖树的深度和重复安装。
-
硬链接或符号链接:PNPM 使用硬链接来在多个项目之间共享依赖项,而不是像 npm 和 Yarn 那样在每个项目中单独安装依赖项。这使得 PNPM 在磁盘使用方面更加高效,并且在安装和更新依赖时更快。
-
本地缓存:PNPM 会在本地缓存中保存所有安装过的包,以便在多个项目之间共享。这可以节省大量的磁盘空间,并且可以提高安装依赖的速度。
-
自动版本锁定:PNPM 会自动锁定安装的包的版本,以确保在不同的安装环境中得到相同的依赖关系。
-
并发安装:PNPM 支持并发安装依赖项,可以加快整个安装过程。
总的来说,PNPM 提供了一种快速、高效、可靠的包管理解决方案,特别适用于具有大量重复依赖项的项目。如果你对 npm 和 Yarn 的性能或者依赖项管理感到不满意,可以尝试使用 PNPM 来管理你的项目依赖。
4.2 依赖管理机制
当使用 pnpm 安装依赖时,它会创建一个全局的依赖项存储库,所有项目都会共享这个存储库。每个项目的 node_modules 文件夹都会包含一个硬链接
或符号链接
,指向全局存储库中实际的依赖项。这样,当一个依赖项在多个项目中被使用时,实际上只会在磁盘上存储一份。
硬链接和符号链接都是文件系统中用于创建文件链接的方法。
硬链接 。是文件系统中一个文件的多个名称 ,换句话说,它们是文件的不同名称,但实际上指向和原始文件指向的相同的数据块,硬链接创建后,文件的内容和权限等元数据都是共享的,如果删除其中一个硬链接(包括原始文件),并不会影响其他硬链接的正常访问。注意,硬链接不能指向目录。
硬链接常用场景:创建文件的备份(因为硬链接访问的是数据块)、创建多个访问入口(如果需要在不同位置访问相同的文件内容,可以使用硬链接来创建多个访问入口,这在一些特定的应用程序中很有用)。
符号连接 。也称软链接,是一个指向另一个文件或目录的特殊类型的文件 ,它存储了指向目标文件的路径,符号链接类似于 Windows 的快捷方式,当访问符号链接时,文件系统会解析链接并转到目标文件或目录,如果删除了符号链接的目标,则符号链接会失效。
符号链接常用场景:软件包管理(常用于创建指向可执行文件或共享库的链接,这样可以在不同的位置被引用,同时依然保持统一的访问方式)、创建快捷方式。
下面是一个简单的示例,演示了 pnpm 是如何使用符号链接或硬链接来共享依赖项的。
假设我们有两个项目:project1 和 project2,它们都依赖了同一个库 foo@1.0.0
。
在使用pnpm时,项目的文件结构可能如下所示:
global-store/
├─ foo@1.0.0/
project1/
├─ node_modules/
│ └─ .pnpm/
│ └─ foo@1.0.0 -> ../../global-store/foo@1.0.0
└─ package.json
project2/
├─ node_modules/
│ └─ .pnpm/
│ └─ foo@1.0.0 -> ../../global-store/foo@1.0.0
└─ package.json
在这个示例中,.pnpm
目录是 pnpm 用来存储符号链接或硬链接的目录。在每个项目的 node_modules
目录下,都有一个指向全局存储库中的 foo@1.0.0
的硬链接或符号链接。这样,在两个项目中都安装了 foo@1.0.0
,但实际上只在磁盘上存储了一份 foo@1.0.0
的拷贝,大大节省了磁盘空间。