背景
今年在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包的:
- 循环依赖
- 版本冲突
- 同个版本包收敛
我们以这个下面这个包管理树为例:
foo +-- blerg@1.2.5 +-- bar@1.2.3 | +-- blerg@1.x (latest=1.3.7) | +-- baz@2.x | | `-- quux@3.x | | `-- bar@1.2.3 (cycle) | `-- asdf@* `-- baz@1.2.3 `-- quux@3.x `-- bar
我们从上面可以看出:
blerg
有两个版本分别是1.2.5
和1.x
,这是同个次版本的包放在不同地方bar
版本是1.2.3
,但是依赖blerg
和baz
,但是依赖baz
包的版本是2.x
,这是版本冲突baz
版本是1.2.3
,,但是所依赖的包中有个依赖bar
这就是循环依赖
那么npm是如何解决这类问题的呢?npm会将上述包管理树优化成如下所示:
foo +-- node_modules +-- blerg (1.2.5) <---[A] +-- bar (1.2.3) <---[B] | +-- node_modules | +-- baz (2.0.2) <---[C] +-- asdf (2.3.4) +-- baz (1.2.3) <---[D] +-- quux (3.2.0) <---[E]
那么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安装全局命令:
npm install -g pnpm
其他安装方式可以参考官网教程:pnpm 安装
实现原理
从作者博客或者github源码,我们可以尝试去推测实现pnpm几个特性所需要关键原理。
更快速的install
pnpm为了加快npm install
,从以下两个方面进行优化:
- 创建一个
.pnpm store
,除了第一次安装需要按照npm包,后续再次安装,将会创建一个符号链接到npm包 - 不打平
npm 依赖树
,节省复杂依赖树打平时间
可以参考官网的架构图,去更好了解pnpm针对node_moduels的管理,如下:
这里有几个设计点需要弄清楚:
1.pnpm将所有依赖树的目录,软链接到.pnpm store
中对应的npm包
2.node_moduels
将不会有package.json
依赖(devDependencies
和dependencies
)外的npm包
3.node_moduels
的目录结构,和npm依赖树保持一致
通过上述几个点,pnpm将拥有比yarn
和npm
更快的install速度。
额外知识点:
1.Linux文件链接分为软链接和硬链接,两者有什么区别?
- Linux中的符号链接,就是我们平时说的软连接,可以针对文件、目录创建,但是源文件删除后链接不可用,命令:
ln -s xxx xxx
- Linux中的硬链接,只能针对文件,但是文件删除仍可使用,命令:
ln xxx xxx
2.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包。具体可以参考下图: