
前端模块化的最后一块拼图——浏览器中如何运行模块
前端模块化的演变包括CJS、AMD、CMD、UMD和ESM,现代浏览器支持ESM,webpack负责模块处理和打包,确保在浏览器中高效运行模块,支持代码拆分和Tree Shaking以优化性能。
pnpm是一个高效且节省空间的前端包管理器,通过改进的非扁平node_modules目录和硬链接机制优化依赖管理。与npm和yarn相比,pnpm在性能和兼容性上表现优越,尤其在缓存情况下安装速度更快。pnpm的安装过程分为解析、目录结构计算和链接依赖项三个步骤,采用符号链接解决幽灵依赖问题,并通过硬链接机制减少硬盘占用。pnpm还支持monorepo,并提供了操作全局store的命令。
pnpm(performant npm)是一个主打快速和省空间的包管理器。它使用改进的非扁平node_modules目录和硬链接和符号链接优化依赖管理过程,个人体验下来比起yarn-v2的PnP机制会遇到的兼容性问题更少,是我现阶段最喜欢的包管理器。
如果对npm和yarn-v2没有了解,建议先去按顺序看对应的那两篇文章,会更好地理解这里地一些概念。
既然自称perfomant,就需要跑分作为证据。下图为pnpm对比各包管理器(npm-v9,yarn-PnP)的性能对比图:
总得来说,pnpm是兼容性、性能和空间三者一个较好的平衡方案。
根据pnpm官方的指导,和yarn2一样,都建议使用node内置的corepack工具安装pnpm。
$ corepack prepare pnpm@latest --activate
$ pnpm -v
如果安装成功,可以看到pnpm版本如下:
首先,个人觉得pnpm走上了和yarn2不同的一条路,它们都从npm/classic-yarn出发,尝试从不同方向、用不同方案去改进依赖管理的体验和性能。最终,yarn选择了完全开辟一条新的路子(PnP),而pnpm选择了在原有架构(node_modules)上进行修补和改进,具体一点就是它们都用自己的方法解决了原有方案的问题,如幽灵依赖和依赖冲突(这两点是你可以在博客中重点体会二者解决方案差异的点)。
因此,这里主要讨论的是pnpm相对于npm/classic-yarn的改进,而不是和yarn2的对比。然而,PnP和pnpm的普及率似乎昭告了pnpm是暂时成功的那一个。
pnpm安装依赖项(以pnpm install
为例)一般分为三步:
值得注意的是,在pnpm中这三个阶段是在多任务上并行的(类似于CPU的动态流水线),可以节约大量时间,如下图所示:
以上这三个阶段将在后文反复地被提到,请一定在脑子里有个大致的印象。
使用 npm 或 Yarn Classic 安装依赖项时,所有的包都被提升到模块目录的根目录。 这样就导致了一个问题 —— 幽灵依赖,源码可以直接访问和修改依赖,而不是作为只读的项目依赖。
pnpm 的解决方案是使用符号链接将项目的直接依赖项添加到模块目录的根目录中。说起来有点拗口,实际上就是你的node_modules根目录中不再将所有的次级依赖平铺出来,而是只有你当前项目的直接依赖。
这样做的好处是显而易见的——解决了“幽灵依赖”问题。但是你先别急,pnpm和npm-v2的解决方案可不一样,它没有傻傻地嵌套安装所有所需的依赖,而是通过一些特殊的目录结构加上符号链接将嵌套的层数控制在了两层(一个可接受的范围内)。
那么它链接到了哪里呢?答案是node_modules/.pnpm/<name>@<version>/node_modules/<name>
,所有的包真正的存储目录是.pnpm中对应文件夹的node_modules下的对应名称文件夹(有点拗口)。
你或许也注意到了,包本身存放的地方并没有在<name>@<version>
根目录下,而是在其node_modules中和其他依赖并列存放,这是为什么呢?
node_modules
中或在任何其它在父目录 node_modules
中是没有区别的。pnpm在实际管理包的物理存储时仍采用了平铺管理的模式:在.pnpm中的所有包都是平铺的,但是因为外部访问不到所以没有关系。
但这还不是全部,因为.pnpm中仍然有链接的存在:假想我们有一个包A,它依赖了B和C,那么**在****.pnpm/A/node_modules
**中就也有符号链接形式的B和C,指向.pnpm目录下真正的B和C。
以上就是pnpm的非扁平目录的全部真相了,虽然乍一看这种嵌套的node_modules会有点奇怪,但它也有些好处:
总结:pnpm使用了表层嵌套+底层平铺的方法组织目录结构,同时用符号链接作为技术实现,这样既解决了“幽灵依赖”问题,又维持了两层嵌套结构的稳定性,是非常巧妙的设计。
现在,我们在实践中看看pnpm的node_modules结构。
mkdir pnpm-test
cd pnpm-test
pnpm init
pnpm add express
打开node_modules看看:
可以看到express确实以符号链接(在Windows中以junction的方式实现)的形式存在在根目录,且没有其他的间接依赖存在。
其中.modules.yaml
里面存放着间接依赖的列表,如下图所示:
双击express,我们就进入了真正的存放目录(但上方的路径并没有变化),因此为了观察真正的目录结构我们需要进入.pnpm中,发现这里的包确实都是平铺的,且与.modules.yaml
中的顺序是对应的:
我们找到express,打开其目录只有一个node_modules,进入这个node_modules,我们可以看到和前面的叙述一致:所有间接依赖以符号链接的形式放在node_modules中供调用,其本体也存储在这个node_modules/express目录中。
这里补充解释一下前面说的“包本身及其依赖的包被放置在一个文件夹下可以优化查询和解析过程”就是指它本体存储中其实不含有node_modules(比如这里node_modules/express中就不再有node_modules),因此根据node规则就会自动向上一级目录的node_modules中寻找依赖,这里全是它自己的依赖而不像.pnpm中有全部的依赖,就减小了查找范围,优化了性能。
由于非扁平结构和链接特性,pnpm 中任何一个包都拥有自己的一组确定版本的依赖项,而不会像扁平依赖中受到其他包的影响。但在处理peerDependencies
时是一个例外:peer 依赖项(peer dependencies)会从依赖图中更高的已安装的依赖项中解析(resolve),因为它们与父级共享相同的版本。
Windows直至Vista(NT6)之前都完全不支持符号链接(symbolic link),后面的符号链接不能说不能用,只能说有问题。因此pnpm采用了junction方式(类似于快捷方式)来在Windows上实现符号链接的功能。
看了上面的介绍后,你内心可能还有个疑问:就这?说好的省空间呢?这只是把依赖用软链接组织起来了呀,.pnpm中该装的依赖铺平了不是一个不少吗?
别急别急,马上就介绍pnpm降低硬盘占用的杀手锏:硬链接机制 —— 让所有的包都只存一份!
实际上在上面提到的“本体”(如node_modules/express中存放的实际文件)都是硬链接(hard link),指向根目录(Windows下就是当前盘符根目录)中的.pnpm-store这个目录。
以Linux为例,硬链接(hard link)实际上就是指硬链接的n个文件有完全相同的inode结点;而软链接/符号链接则是指两个文件有不同的inode结点,一份为本体,一份为引用(即inode结点中存放的是指向另一份文件的指针)。
上面的指向图中其实已经能看出硬链接的过程,但下面这张图更清晰地反映了引入硬链接后整个node_modules的实际引用箭头:
node_modules
仍能看到其占用的真实容量,但实际上只占用了一份空间(也就是一个磁盘上两个位置的同一份空间)。(来源:pnpm-FAQ)pnpm在处理全局缓存~/.pnpm-store/v3/files
时并没有直接将整个包作为存储单位放在缓存中,而是更进一步,将其拆分为时并没有直接将整个包作为存储单位放在缓存中,而是更进一步,将其拆分为**一个个文件块(chunk)**一个个文件块(chunk) 进行存储,然后再对其进行哈希和索引操作。 进行存储,然后再对其进行哈希和索引操作。
这样做的好处就是不仅同一个版本的包可以只存储一份内容,甚至不同版本的包也可以通过diff算法实现增量更新存储。
直观的说就是,如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update
时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。
pnpm采用的这种组织方式叫做content-addressable(基于内容的寻址),如下图所示:
这样解释可能还是不好理解,我们直接打开对应目录,随便看一个文件试试:
files
中还是按照Hash(SHA-512)的格式进行组织和存储,前两位用于目录,后面的位用于文件名使用pnpm store <cmd>
命令可以操作全局store,这也是pnpm特有的命令。
add
:直接向store全局添加一个包prune
:删除没有使用的包path
:返回当前Store的路径这里后续补充pnpm搭配rush的monorepo体验,对比yarn+lerna的体验。
pnpm add
对应的包添加即可.npmrc
配置文件中添加nodeLinker=hoisted
将创建一个和npm类似的扁平化目录(如React Native开发或不支持符号链接的部署环境)作者: ChlorineC
创建于: 2023-06-09 15:02:00
更新于: 2025-01-11 21:00:00
版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
前端模块化的演变包括CJS、AMD、CMD、UMD和ESM,现代浏览器支持ESM,webpack负责模块处理和打包,确保在浏览器中高效运行模块,支持代码拆分和Tree Shaking以优化性能。
Bun 1.0是一个新的JavaScript运行时,作为Node.js的替代品,提供更快的包管理和开发解决方案。尽管目前在生产环境中尚不成熟,但它集成了多种工具,支持统一模块标准和Web API,具有显著的性能优势。Bun的包管理器速度比pnpm快4倍,支持原生JSX和TS,且具备构建工具的潜力。整体来看,Bun在运行时和工具链上均有加速效果,但仍需进一步发展以满足生产需求。
本文探讨了在React中使用react-use-websocket库来实现WebSocket功能的最佳实践,包括如何封装WebSocket逻辑、处理连接状态、发送和接收消息,以及使用ArrayBuffer传输文件。此外,文中还介绍了Socket.IO的工作原理及其与WebSocket的区别,提供了Node.js后端服务器的示例代码,展示了如何实现低延迟的双向通信和进度反馈。
本文介绍了前端包管理器npm的发展历史、功能和重要性。npm不仅是一个包管理器,还是前端项目管理的基础,管理依赖、项目信息和脚本。文章详细探讨了npm的历史、模块化概念、依赖管理的演变,以及如何使用npm管理项目和发布自己的npm包。尽管npm在现代前端开发中逐渐被yarn和pnpm等新工具取代,但其核心思想和机制仍然对开发者有重要的学习价值。