0.背景
1.操作系统中的文件系统相关概念:
文件系统中存储一个文件需要分别存储两个部分:
1.文件自身的内容信息;
2.描述该文件的属性信息: 这里就是存储文件的元信息,因为元信息是存储在inode区域,所以又可以称为文件的inode信息。(其中属性信息属于文件元信息,元信息就是属性描述,例如:信息大小,创建时间、文件名),如下所示为其中在存储方面,不仅仅是block存储区域满了就不能存了,如果inode区域满了我们也同样不能存储新文件了。 文件查找过程 -- 当我们打开一个文件想要读取文件信息的过程:
1.系统首先找到文件名对应的 inode 号码;
2.然后通过 inode 号码获取inode信息;
3.然后根据 inode信息拿到block区地址,然后去block去读取文件数据。这一查找过程就是文件查找的过程。
但是当我们的文件目录很深,不想一层一层打开的时候,我们希望桌面上有一个文件,打开后直接对应对应文件。
而硬连接和软连接就是为了提高这一查找过程的体验。(注意:这里连接和复制的不一样,复制是在block区域开辟新的存储空间存储,而软硬连接则对block区域不变,指向同一个区域,但是怎么指或者指的路径则是软硬连接要干的事情)
2.了解硬连接和软连接
一个新工具的出现往往是为了解决旧工具的问题,那么pnpm的出现同样也是为了解决npm自身的一些问题,而npm常常被诟病的就是它的下载机制和包安装存储机制:
1.串行下载:下载机制上就是它的串行下载,串行的通病就是前面失败后面的阻塞。
2.储存空间浪费:如果100个项目都依赖lodash包,那么每个项目的node-module都需要下载lodash,那么磁盘中就有100个loadsh包。那这就会很浪费本地的磁盘空间。
3.支持monorepo:这个对多项目管理很重要
3.幽灵依赖:这个虽然是优点,但是出现的几率小,所以可以认为是附加优势。但是我们需要知道为什么会出现幽灵依赖,因为npm的依赖树构建是采用‘扁平化’方式导致的。
一、软硬链接概念
在linux中,链接的方式有两种:硬连接和软连接,而创建连接的主命令是 :ln (不带参数默认是创建硬连接,带上参数 -s 即为soft软连接)
1.硬连接
文件A的inode信息就是文件B的inode信息,那么就称为文件a是文件b的硬连接
| 标题 | |
|---|---|
| 定义 | 新增一个指向内容block的文件 |
| 创建硬连接命令 | ln file1 file2 (此时可以认为file1是源文件,创建一个file2硬连接到file1) |
| 表现特点 | 如果文件A和文件B是硬连接的关系,那么文件A的修改后的内容,文件B打开的内容也将更新(因为inode节点号相同 ==> 底层block区域内容更改了) |
| 硬连接使用场景 | 创建一个硬连接之后就可以防止误删源文件导致底层block数据丢失。此外,文件删除的逻辑是:当文件的硬连接个数为0的时候,理论上应该block区域被释放。但是实际上不会立马释放,而是在有新的数据需要使用这块存储block或者做磁盘检查的时候才会被真正的释放,如此数据就再也找不回来了。(数据恢复的原理) |
2.软连接
读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)
| 标题 | |
|---|---|
| 定义 | 类似于window中的快捷方式 |
| 创建硬连接命令 | ln -s file1 file2 (创建一个file1的硬连接file2,且file2不能已存在) |
| 被创建的软连接文件-file2 | 1. 本质上是一个特殊的文件,这个文件将源文件的位置信息。源文件的inode存在block中,然后找到block内容后再根据内容找源文件位置,再去读取源文件的内容block; 2.文件大小就是源文件的路径大小 |
| 表现特点 | 如果file1被删除了,那么file2访问不到数据。 |
3.两者的区别
总结:两者区别在于是否文件a和文件b共用block数据区地址,如果不共用是否文件a的block数据内容就是文件b的路径,进而系统会自动去block数据区去拿数据。
也就是说,硬连接是指电脑文件系统中的多个文件平等的共享同一个文件存储单元。软连接是指一个文件保存了目标文件的访问路径(系统拿到目标文件访问路径再访问存储单元内容)。一个保存存储单元,一个保存目标文件的路径。
硬连接是自身文件的inode指向和被硬连接文件的inode相同,共用一个block数据区域。
软连接是被软连接文件有自己的inode区域和block数据区域,而软连接的文件的也有自己独立的inode和 lock数据区域。inode和block数据都不共用,且inode和block内容信息也不同。软连接的blcok数据区域存放的是被软连接文件的路径,inode则是对应的元信息。也就是说,文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。
参考文献: www.ruanyifeng.com/blog/2011/1…
二、pnpm
2.1 pnpm的原理,为什么 pnpm 比 npm 和 yarn 好?
2.1.1 npm的下载机制、依赖树的构建、查找机制
npm下载机制(npm install):
node-module依赖树的构建:如果下载的是node-module中不存在的新的安装包,那么肯定是要放在第一层的。而只有第一层中已经存在这个名称的包的时候:
(1) 如果版本符合json文件的版本范围,那么就不下载了;
(2) 如果不符合,那么就在对应的层级下安装。显然,第二种情况是node-module依赖树层级变深的主要原因。
npm存在的问题总结
- 未彻底解决重复依赖安装问题:对于相同名称相同版本的情况,安装包是在npm3之后采用的扁平化,但是对于同一名称不同版本的话,就会在下面继续嵌套构建不同版本的包。按照npm查找机制「从下往上查找」,如果不同版本的包都安装扁平化,就会导致都去扁平化那一层寻找,就导致不知道使用哪个版本。因此对于安装相同名称不同版本的依赖包依然会导致 嵌套过深 && 重复安装 问题(没有彻底解决)。
需要注意的是,重复依赖安装的现象导致了两个问题:1.重复安装会占据磁盘空间;2.重复安装的文件地址路径会过长,在 Windows 系统下会文件路径过长出现一些问题。- 下载机制问题:采用的是同步下载机制,导致下载慢或者下载阻塞。 因此提高npm除了立刻反应出他的作用,还需要知道他目前的两大缺点:构建树中的重复依赖问题和安装慢的问题。
2.1.2 yarn 解决了 npm 哪些问题?
对于上面npm存在的两大问题,npm只是解决了同步下载慢的问题,并没有解决彻底解决重复依赖安装问题。
对于npm下载慢的问题。npm的下载机制是采用同步下载,后一个等待前一个下载完成才会去下载,这就导致了下载时间过长。而yarn解决的方式就是采用异步下载,这就使得下载时间大大缩短,具体区别如下:
- 1.yarn安装速度快,因为它是异步执行安装依赖;npm是同步执行,它需要先安装好前面的包再接着安装。
- 2.yarn安装过程信息很干净,npm会罗列很多其它包的信息,看过去感觉不直观。
- 3.yarn安装后是有个yarn.lock文件,这个文件是会锁定你安装的版本,别人安装时会直接读取yarn.lock文件,这样可以保证安装的依赖的版本是一样的,npm在5.x.x的版本也引入了这个机制,它的文件叫package-lock.json。
总结:yarn只是解决npm同步安装问题,但yarn和npm一样,同样存在着重复依赖安装和嵌套深的问题。
2.1.2 现有的 npm 和 yarn 存在什么问题?
同上:yarn只是解决npm同步安装问题,但yarn和npm一样,同样存在着
1.重复依赖安装
2.嵌套过深 3.幽灵依赖
2.1.3 pnpm的解决办法
1.嵌套过深问题 - 依然保持node_module依赖树的严格规范,只不过这个依赖树只是查找树,通过软连接找到.pnpm中硬连接文件,进而最终找到内容
2.重复依赖安装问题 - 通过统一在store中存储依赖,而不是在依赖树对应的文件路径中存储依赖,进而巧妙避开了重复安装问题
3.幽灵依赖问题 - 由于是严格按照项目和依赖内对应的package.json文件对应的层级嵌套来严格构建的依赖树,因此不使用npm中提供的扁平化算法导致的依赖提升,故不存在幽灵依赖问题。
首先pnpm采用了与npm和yarn不同的依赖包管理方式,npm会将下载的依赖存储在本地项目目录下(npm下载的时候也会去npm cache中查看时候有压缩缓存,没有则去远端下载)。
而pnpm采用的是 store + link的依赖管理方式:重点「软硬链接在pnpm中的体现」
store就是依赖的实际存储位置(Mac/linux是的store是在{home dir}>/.pnpm-store/v3),这个store是全局的,因此可以实现npm不能实现的项目共享,节省了磁盘空间。link是指符合链接(SymbolicLink)和硬链接(hardLink)。
例如,通过pnpm安装exporess后,项目自身的node_module中就会出现一个exporess文件夹(如图一所示), 但是这个文件夹只是一个软连接,这个软连接指向的地方是.pnpm文件中对应的文件(如图2所示),而.pnpm目录中的一级文件都是通过硬连接直接连到store中的。
图一
图2
这样就可以兼容node文件的查找机制,也不会有幽灵依赖的问题
(1)如何解决npm重复安装依赖问题
首先我们想一想,npm中的扁平化算法的目的是什么?就是为了解决重复依赖复制多次占据空间,以及嵌套深这两个问题。对于第一个问题「重复依赖复制多次」,pnpm解决方案是采用软硬链接方式,因为pnpm所有的包都是放在store目录下的,所以只要有重复的只需要通过软硬链接导过去就行。
(2)如何解决“幽灵依赖”问题:
背景
1. 什么是「幽灵依赖」? 其实简单来说就是 项目里使用的库依赖另一个库,但是未在项目的package.json 中定义的包。就是幽灵依赖。即我们下载的依赖包中有可能又依赖了其他依赖项,那么按照npm下载机制和扁平化原则,就会将这个依赖包的依赖包“拍”在node_module中。这就导致了不是项目的依赖包也会拍在node_modulde中(可能在第一层中),那么如果项目中有个文件引入这个依赖包的依赖包,按照npm查找机制也不会报错,这就可能会导致开发者误判这个项目中package.json中设置的项目自己依赖,导致某些玄学的报错。
2. 为什么会造成幽灵依赖 ?依赖树递归+扁平化导致的依赖提升。npm install的安装机制会递归package.json构建依赖树,因为依赖提升,造成的副作用
3. 幽灵依赖可能会导致的问题
- 版本问题
比如一个项目用的A这个库,版本是v1, 然后A这个库依赖B库,版本也是v1,B这个库就是我们没有声明的一个库,但是我们仍然在项目中去进行使用。但是如果有一天,我们对A这个库进行了升级,比如说升级到了v2的版本,然后它可能需要对应B库的v2版本,于是B库也跟着莫名其妙地进行升级了,而B库升级之后,它里面的api可能会发生变动,那么可能会导致我们之前的用B库的代码出现问题。简而言之就是,幽灵依赖升级了,对应的API变更,而项目应用无感知仍使用老的API导致报错。
- 依赖丢失
现在A是一个开发时依赖,也就是到了生产环境里,是不会安装A这个依赖的,那么B也就不会安装,但是在开发的时候却使用了B,但是生成的时候B却没有了,所以这会发生依赖丢失的问题
pnpm 主要通过硬连接和软连接来实现模块管理工作,采用的依赖策略是:消除依赖提升、规范拓扑结构。 也就是说pnpm的依赖树的结构是严格按照递归依赖构建的,不存在什么依赖提升的说法,也不会出现现在node_module中存在非项目package.json中依赖,都是严格按照每个依赖树递归下的结构构建的依赖树。
那么有人会问,那不是又和npm2.0时候一样,不同树分支下的相同依赖包会被重复下载嘛?不是的,pnpm是的依赖树只是一个查找树,查找到之后会通过链接方式找到文件inode,进而去store中寻找下载到文件。即只有依赖查找树,而依赖文件都是在store中。相比于npm中的查找树就是文件树,就会导致文件重复下载。这样就
(3)pnpm中的node_module结构
pnpm的依赖树的结构是严格按照递归依赖构建的,不存在什么依赖提升的说法,也不会出现现在node_module中存在非项目package.json中依赖,都是严格按照每个依赖树递归下的结构构建的依赖树。
那么有人会问,那不是又和npm2.0时候一样,不同树分支下的相同依赖包会被重复下载嘛?不是的,pnpm是的依赖树只是一个查找树,查找到之后会通过链接方式找到文件inode,进而去store中寻找下载到文件。即只有依赖查找树,而依赖文件都是在store中。相比于npm中的查找树就是文件树,就会导致文件重复下载。
node_modules 中每个包的每个文件都是来自内容可寻址存储的【硬链接】,即.pnpm 目录中的一级文件都是通过硬连接直接连到store中的。
举例如下:
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ ├── bar -> <store>/bar
│ └── qar -> ../../qar@2.0.0/node_modules/qar
├── foo@1.0.0
│ └── node_modules
│ ├── foo -> <store>/foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│
└── qar@2.0.0
└── node_modules
└── qar -> <store>/qar
如上所示,假设项目package.json文件中只存在一个依赖foo,且依赖foo下面bar,bar下面还有依赖qar。
此时,node_modules文件夹中只存在项目的直接依赖foo(不会存在依赖提升bar和qar)和.pnpm。
- foo文件是通过软连接到.pnpm中的foo@1.0.0。然后foo@1.0.0/node_modules/foo一个硬连接到store存储文件中。
- 然后foo的依赖bar包会在foo的node_module下,通过foo的node_module下的bar查询软连接到 -> .pnpm/bar@1.0.0/node_modules/bar文件,这个文件再硬连接到store。
- 然后bar的依赖包qar会在bar的node_module中,通过查询bar的node_module找到qar的软连接(即在.pnpm中找到对应的qar包,然后在node_module中找到qar的硬连接文件,进而找到store下的内容 )
(4)peer dependence 如何被处理
2.2 发布npm包时候本地调试时,npm link干了什么事?
npm link使用场景:
开发npm模块时候的本地调试 -- 当我们需要发布一个npm包的时候,往往需要先在本地运行调试。但是我们还没调试发布之前npm install得到的是老版本的包。那么我们难道需要在每个使用这个包的文件去更改引用地址(打到本地npm模块地址嘛?)如果是业务文件还好,那如果说node-module中其他npm包也引用了这个npm包呢?难道我们需要去更改node-module中的引用地址嘛? 显然这种更改地址的方式不是一种优雅的解决方案。而我们希望的就是调试可以无感,让我们项目本地调试的时候,引用这个npm包的时候自动打到本地开发的那个npm包,不用改任何引用路径。而这种方案就需要npm link来实现。 npm link是一种把包链接到包文件夹的方式,即:可以在不发布npm模块的情况下,调试该模块,并且修改模块后会实时生效,不需要通过npm install进行安装
npm 常规使用理解
如下所示,我在project_npmlink文件下有两个项目:module1 和 project1,
使用方法:(本地的npm包为npmA,目录在desk/npmA)
1. 项目和模块在同一个目录下,可以使用相对路径
npm link ../module
2. 项目和模块不在同一个目录下
1.cd npmA 所在的根目录下,执行命令:npm link(全局link,先将npm-A包链接到全局路径下)
2.cd <应用B所在的根目录下>,执行命令:npm link npmA(模块名) (将npm引入到项目依赖中)
解除link
解除项目和模块link,项目目录下,npm unlink 模块名
解除模块全局link,模块目录下,npm unlink 模块名
pnpm安装与使用环境问题踩坑(node-v16.0版本以上) blog.csdn.net/Mr__proto__…
总结
一、目前npm存在的问题
1)npm下载慢的问题,原因是因为npm下载方式是串行下载;
2)npm下载重复依赖的下载机制导致一个大问题 - 重复依赖下载导致存储空间浪费:原因是由于node_module 会严格按照依赖树结构下载依赖,如果出现重复就会导致重复下载安装,为此npm3.0之后采用扁平化算法解决了相同名称相同版本重复下载问题,但是对于相同包名不同版本的依赖依然会进入依赖树深成重复下载。因此这就形成了npm目前下载机制的现状:扁平化算法解决同包同版本重复下载问题,同包不同版本依然进入嵌套树重复下载(重复下载问题依然存在)。
而npm目前的下载机制又会衍生出两个子问题:
1.1) 一个就是扁平化衍生的“幽灵依赖”问题
1.2) 另一个就是原来的重复依赖老问题二、pnpm出现主要是解决npm的哪些问题
pnpm解决npm和yran依赖包问题的核心思想就是:依赖包的关系树结构和存储结构分开(存储结构是全局扁平存储,依赖结构严格层级关系不用扁平化)
- 对于重复依赖问题,采用硬链接,所有依赖都统一存储到./pnpm/store中,如果遇到相同的依赖直接通过硬链接指向即可
- 对于幽灵依赖问题,废除扁平化算法,保持原有的严格node_module依赖树层级嵌套格式(这样就保证了层级结构的严格,文件查找的时候就不会出现幽灵依赖了),此外node_module中才有软连接到.pnpm/node_module目录(找到对应的硬连接,通过硬连接找到store),这样就实现了依赖树结构和下载的分离方式 -> 解决了幽灵依赖,又不会重复下载.