原文链接:Flat node_modules is not the only way,2020.05.27,by Zoltan Kochan
新用户经常问我关于 pnpm 创建的 node_modules
结构为什么如此奇怪。为什么它不是扁平化的?所有子依赖(sub-dependencies)在哪里?
我假设读者已经熟悉 npm 和 Yarn 创建的扁平化
node_modules
。如果你不明白为什么 npm 3 必须从 v3 开始使用扁平化 node_modules,可以在《为什么我们应该使用pnpm?》中找到一些原因。
那么,为什么 pnpm 的 node_modules
与众不同呢?让我们创建两个目录,在其中一个目录中运行 npm add express
,在另一个目录中运行 pnpm add express
。下面是第一个目录的 node_modules
顶层内容:
.binacceptsarray-flattenbody-parserbytescontent-dispositioncookie-signaturecookiedebugdepddestroyee-firstencodeurlescape-htmletagexpress.bin accepts array-flatten body-parser bytes content-disposition cookie-signature cookie debug depd destroy ee-first encodeurl escape-html etag express.bin accepts array-flatten body-parser bytes content-disposition cookie-signature cookie debug depd destroy ee-first encodeurl escape-html etag express
你可以在这里看到整个目录。
而下面就是通过 pnpm 创建的 node_modules
中所得到的内容:
.pnpm.modules.yamlexpress.pnpm .modules.yaml express.pnpm .modules.yaml express
你可以在这里查看。
那么所有的依赖项都在哪里呢?node_modules
文件夹中只有一个名为 .pnpm
的文件夹和一个名为 express
的符号链接。我们只安装了 express
,所以这也我们在应用程序唯一能够访问的包。
在这里阅读更多关于 pnpm 严格性好处的信息。
让我们来看看 express
里面有什么:
▾ node_modules▸ .pnpm▾ express▸ libHistory.mdindex.jsLICENSEpackage.jsonReadme.md.modules.yaml▾ node_modules ▸ .pnpm ▾ express ▸ lib History.md index.js LICENSE package.json Readme.md .modules.yaml▾ node_modules ▸ .pnpm ▾ express ▸ lib History.md index.js LICENSE package.json Readme.md .modules.yaml
express
没有 node_modules
?express
的所有依赖在哪里?
诀窍在于 express
只是一个符号链接。当 Node.js 解析依赖项时,它使用它们的真实位置,会忽略符号链接。但你可能会问,express
的真实位置在哪里呢?
这里: node_modules/.pnpm/express@4.17.1/node_modules/express。
好的,现在我们知道了 .pnpm/
文件夹的目的。.pnpm/
将所有包存储在一个扁平化的文件夹结构中,因此每个包都可以在以这种模式命名的文件夹中找到:
.pnpm/<name>@<version>/node_modules/<name>.pnpm/<name>@<version>/node_modules/<name>.pnpm/<name>@<version>/node_modules/<name>
我们称之为虚拟存储目录(virtual store directory)。
译注:pnpm 的虚拟存储目录最初是 .registry.npmjs.org///node_modules/ 方式,后来才改成上述结构。因为 .pnpm 具有标识意义,类似于项目中的 .vscode、.github 的作用
这种扁平结构避免了由 npm v2 创建的嵌套 node_modules
引起的长路径问题,但与 npm v3、4、5、6 或 Yarn v1 创建的扁平 node_modules
相比,它又保持了包的隔离性。
现在让我们来看一下 express
的真实位置:
▾ express▸ libHistory.mdindex.jsLICENSEpackage.jsonReadme.md▾ express ▸ lib History.md index.js LICENSE package.json Readme.md▾ express ▸ lib History.md index.js LICENSE package.json Readme.md
这是一个骗局吗?它还缺少 node_modules
!pnpm 的 node_modules
结构的第二个技巧是,包的依赖关系与其真实位置所在的目录层级相同。因此,express
的依赖关系不在 .pnpm/express@4.17.1/node_modules/express/node_modules/
中,而是在 .pnpm/express@4.17.1/node_modules/ 中:
▾ node_modules▾ .pnpm▸ accepts@1.3.5▸ array-flatten@1.1.1...▾ express@4.16.3▾ node_modules▸ accepts▸ array-flatten▸ body-parser▸ content-disposition...▸ etag▾ express▸ libHistory.mdindex.jsLICENSEpackage.jsonReadme.md▾ node_modules ▾ .pnpm ▸ accepts@1.3.5 ▸ array-flatten@1.1.1 ... ▾ express@4.16.3 ▾ node_modules ▸ accepts ▸ array-flatten ▸ body-parser ▸ content-disposition ... ▸ etag ▾ express ▸ lib History.md index.js LICENSE package.json Readme.md▾ node_modules ▾ .pnpm ▸ accepts@1.3.5 ▸ array-flatten@1.1.1 ... ▾ express@4.16.3 ▾ node_modules ▸ accepts ▸ array-flatten ▸ body-parser ▸ content-disposition ... ▸ etag ▾ express ▸ lib History.md index.js LICENSE package.json Readme.md
所有 express
的依赖都是指向 node_modules/.pnpm/
中适当目录的符号链接。将 express
的依赖放在上一级可以避免循环符号链接。
所以你可以看到,尽管 pnpm
的 node_modules
结构一开始看起来不寻常:
- 它完全兼容 Node.js
- 包和它们的依赖关系又能被很好地分组
译注:这就是 pnpm 非常巧思的地方,利用了 Node.js 查找依赖时的寻址策略(查找当前目录或上层目录中的 node_modules 目录,以此类推),配合符号链接:既解决了 npm 扁平化带来的间接依赖暴露问题又做到了依赖分组,同时借助全局存储(global store)大大节省了硬盘空间。
对于具有 peer dependencies
的包,结构稍微复杂些,但思路是相同的:使用符号链接创建一个嵌套并具有扁平化的目录结构。