背景
今年在github前端领域star上升速度比较主要有以下几个:
- Svelte, 一个将MVVM架构放到构建应用程序的编译阶段实现的框架,让你开发更少的代码实现更多的功能
- Typescript, 几乎所有的前端框架不约而同的支持了Typescript,一个JavaScript的超集,支持变量类型声明
- pnpm,一个现代化的npm包管理工具,采用link方式去全局管理包,这是本文介绍的重点。
因此,选一个和我们项目中开发相关的作为一个知识扩展点,应该就是pnpm了。
其实从事前端这么多年,自从npm包出现以后,前端工程化就一路顺畅,但是随之也带来很多问题,包括但不限于:
- 版本管理难
- 安装速度慢
- 多个项目安装依赖占用空间大
- …
那么其实我们应该了解完整的npm包管理生态,然后才可以得知为什么pnpm打击了哪些痛点,以及如何应用到我们的实际项目中。
npm
之前已经在《从npm版本依赖到Monorepo大仓项目》已经介绍过npm是什么,定义如下:
npm,Node Package Manager的缩写,也就是“Node的包管理器”。
npm(“Node 包管理器”)是 JavaScript 运行时 Node.js 的默认程序包管理器。
也了解它的版本是如何管理的,但是我们还缺乏对它的核心功能需要了解,具体了解以下几个方面:
npm install xxx
, npm如何将远程的npm包下载到我们的本地node_modules
npm ls
, npm如何如何维护npm包在本地的关系的npm run xxx
,npm如何执行我们的脚本命令
首先,我们需要知道npm由三部分组成:
- npm包管理网站
- npm-cli,命令行工具
- npm包注册中心,npm registry
接下来就是注册账号,以及包的发布,其中版本管理这块,是遵循APR版本规范(major.minor.patch
版本模型规范),具体可看这里npm版本管理。
npm包管理
目前npm包下载方式分为两种,一种是全局,一种是当前项目,其都会放在node_modules
目录下。下面通过以下几方面去讲述npm是如何管理npm包的:
- 循环依赖
- 版本冲突
- 同个版本包收敛
我们以这个下面这个包管理树为例:
1 | foo |
我们从上面可以看出:
blerg
有两个版本分别是1.2.5
和1.x
,这是同个次版本的包放在不同地方bar
版本是1.2.3
,但是依赖blerg
和baz
,但是依赖baz
包的版本是2.x
,这是版本冲突baz
版本是1.2.3
,,但是所依赖的包中有个依赖bar
这就是循环依赖
那么npm是如何解决这类问题的呢?npm会将上述包管理树优化成如下所示:
1 | foo |
那么npm的遵循策略是什么呢?其实还是APR版本规范(major.minor.patch
版本模型规范)。
- 第一,会将依赖树打平,从树结构变成一级结构,解决了循环依赖问题
- 第二,会将主项目依赖的npm包提到一级,然后对于相同次版本
minor
中的包统一版本,这就是同个版本包收敛,节省空间 - 第三,接着将同一个包,但是不同版本的包一次放到对应依赖包内,形成二级依赖(node_modules),这就解决了版本冲突
require加载顺序
- 加载核心模块,如:fs、path等
- 加上对应文件后缀,优先级为:test.js > test.json > test.node
- 搜索路径,如果有指定路径则按照路径去找,如:require(‘./test’) 则在当前目录寻找
- 如果没有指定路径,则从当前目录下往上去找 node_modules文件夹,然后从文件夹里去遍历寻找对应模块名,如果找不到则到上一层node_modules去找,直到最顶层目录
- 首次会加载比较慢,后面node.js 会将缓存相关信息到内存避免二次查询
npm install执行过程
npm run执行过程
以npm run serve
为例,执行过程如下:
痛点问题
npm的管理包已经能解决大部分开发问题了,那么为什么还会有出现yarn
和pnpm
等新型管理工具,主要npm包存在目前难以解决的痛点问题:
- yarn新增yarn.lock,可以解决npm包版本变动问题,目前已被npm引入特性,生成package-lock.json
- npm包多个项目依赖包一致,但每个项目都需要重新安装,不仅耗时,且占用磁盘空间,这是pnpm解决了的问题
- 包经常创建太深的依赖树,这导致 Windows 上的目录路径过长,这是pnpm解决的问题
- 平铺的结构不是 node_modules 的唯一实现方式,pnpm作者描述:目前node_modules大部分为了解决重复依赖包的问题,把npm依赖树进行打平,这样子会产生一些问题:
- 项目可以访问一些不依赖的npm包
- 打平依赖树的算法非常复杂,导致安装时更加慢
- node_modules目录结构十分复杂
这些痛点问题都不是npm和yarn能解决的,因此才会pnpm的出现,接下来我们再了解一下pnpm。
pnpm
官网是这么定义的:
快速的,节省磁盘空间的npm包管理工具
同时还支持以下这些特性:
- 快速: 比其他包管理器快 2 倍
- 高效:
node_modules
中的文件为复制或链接自特定的内容寻址存储库 - 支持
monorepos
: 内置支持单仓多包 - 严格:默认创建了一个非平铺的 node_modules,因此代码无法访问任意包
这里安装教程就忽略了,推荐的用法是直接使用npm安装全局命令:
1 | npm install -g pnpm |
其他安装方式可以参考官网教程:pnpm 安装
实现原理
从作者博客或者github源码,我们可以尝试去推测实现pnpm几个特性所需要关键原理。
更快速的install
pnpm为了加快npm install
,从以下两个方面进行优化:
- 创建一个
.pnpm store
,除了第一次安装需要按照npm包,后续再次安装,将会创建一个符号链接到npm包 - 不打平
npm 依赖树
,节省复杂依赖树打平时间
可以参考官网的架构图,去更好了解pnpm针对node_moduels的管理,如下:
这里有几个设计点需要弄清楚:
- pnpm将所有依赖树的目录,软链接到
.pnpm store
中对应的npm包 node_moduels
将不会有package.json
依赖(devDependencies
和dependencies
)外的npm包node_moduels
的目录结构,和npm依赖树保持一致
通过上述几个点,pnpm将拥有比yarn
和npm
更快的install速度。
额外知识点:
- Linux文件链接分为软链接和硬链接,两者有什么区别?
- Linux中的符号链接,就是我们平时说的软连接,可以针对文件、目录创建,但是源文件删除后链接不可用,命令:
ln -s xxx xxx
- Linux中的硬链接,只能针对文件,但是文件删除仍可使用,命令:
ln xxx xxx
- Windows如何解决符号链接问题?
pnpm采用junctions,具体如下:
- Windows的文件系统是基于NTFS架构,本身就支持hard link 、junctions和symbolic links
- Hard Link和Linux中硬链接没什么区别,同样只支持文件类型创建链接,采用函数:
CreateHardLinkA
DeleteFileA
- Junctions是符号链接,也叫软链接,支持文件、目录创建链接,实现:
junction 命令行
更安全的install
我们从上文可以得在Node.js中,require()
是如何查询对应npm包,是从node_modules
一层层向上查找获取的。
由于pnpm使用非扁平化的node_modules
管理依赖树,因此对于非package.json依赖的npm包,是不会放在项目的node_modules
中,从而使得开发者无法获取无依赖的npm包。具体可以参考下图:
使用可能会遇到的问题
pnpm官网罗列了一些在使用过程可能会遇到的问题,具体有以下几个方面:
peer dependencies 如何解决
npm workplace
首先我们需要先了解一下npm workplace
或者 yarn workplace
,工作空间的概念。
Workspaces
是一个通用术语,指的是 npm cli 中的一组功能,这些功能支持从单个顶级根包中管理本地文件系统中的多个包。
这组功能弥补了从本地文件系统处理链接包的更加简化的工作流程。自动链接过程作为 npm install 的一部分,避免手动使用 npm link 来添加对应该符号链接到当前 node_modules 文件夹中的包的引用。
简单的说,npm workplace
主要用来解决以下几个问题:
- 在同一个git仓库中管理多个npm包
- 同时npm包之间可以存在互相依赖
- 多个子项目会依赖一些公共的npm包
- 减少之前因为
npm link
管理的混乱
peerDependencies
通过上面npm workplace
概念了解,当工作区里有多个子项目需要依赖一些公共的npm包,但是这些公共npm包版本可能大家所需要的都不一样,这个时候就需要peerDependencies
去描述依赖和版本。它是这么定义的:
在开发插件时,你的插件需要某些依赖的支持,但是你又没必要去安装,因为插件的宿主回去安装这些依赖。此时就可以用
peerDependencies
去声明一下需要依赖的插件和版本。如果出问题的话,npm 会有警告来提示使用者去解决版本中的冲突。
因此,peerDependencies
的目的很清晰,就为了解决workplace下不同子项目中公共依赖包版本问题。
所以回到pnpm
中,由于pnpm
是认定项目中只有一组npm
依赖树,那么针对workplace中的 peerDependencies
,它是如何解决的呢?
pnpm
的解决方案是:为不同的peerDependencies
依赖项创建不同的解析。
如何理解这句话呢?参考官网的解释:
当有两个子项目,他们的依赖分别如下:
1 | - foo-parent-1 |
foo-parent-2
或foo-parent-1
公共依赖了baz
,但是要求版本不一样,如果这个时候项目中没有声明peerDependencies
,.pnpm store
中保存的依赖树如下:
1 | node_modules |
如果有声明peerDependencies
,pnpm
会根据不同的peerDependencies
创建对应的链接,.pnpm store
中保存的依赖树如下:
1 | node_modules |
通过创建不同的peerDependencies
的链接,去解决不同子项目中的依赖包版本问题。
hosited配置
pnpm从2022年初支持hoisted
配置,可以在 .npmrc
文件中使用 node-linker=hoisted
设置。
新增该设定,是为了基本上使 pnpm 可以兼容所有与 npm CLI 兼容的 Node.js 技术栈。
删除npm包
这个是个人的疑问,既然将所有的依赖包统一归置到.pnpm store
管理,那么针对一些过期或者无用的npm包什么时候进行删除呢?
pnpm的给出的方案如下:
- 设置npm包的有效期,可以在
.npmrc
配置中设置modules-cache-max-age配置项,单位为:分钟,默认值:10080,7天 pnpm prune
, 从存储中删除未引用的包
其他问题
官网同时也提出未解决的问题:
- 针对
package-lock.json
等锁文件更新,npm允许每次安装同样版本的npm包可能会更新lock文件,从而更新node_modules
文件内容,但是pnpm由于创建隔离布局,无法实时按照package-lock.json
的更新而更新,因此pnpm推荐将使用pnpm import
命令将package-lock.json
等文件转为pnpm-lock.yaml
这个问题就是pnpm没法按照项目现有的package-lock.json
去做自动更新,只能通过pnpm import
去做转换。
总结
pnpm
确实给前端开发带来一些提醒,让我们关注到平时可能已经习惯的问题,比如npm
包管理混乱导致的问题,虽然存在但是我们已经忽略或者已经习惯了。
总结一下学习pnpm
后,在实际项目中引用会给我们带来什么:
- 提高我们本地开发效率,毕竟多个前端项目同时在电脑,每次安装或更新都需要等待一段时间
- 提高项目的编译速度,利用
.pnpm store
在编译机上,不用每次都重新安装 - 提高项目npm依赖的安全性,清晰明白项目中npm依赖结构,不用担心非法npm包在项目中被开发使用
这里联想扩展几个问题点:
- 本文作者: Qborfy
- 本文链接: https://www.qborfy.com/today/20230219.html
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!