跳至主要內容

node_modules

Cap原创大约 6 分钟node

原文链接https://zhuanlan.zhihu.com/p/137535779open in new window

package

包含了package.json,使用package.json定义的一个package。通常是对应一个module,也可以不包含module。

module

能被require的,就是一个module,只有当module里面包含package.json的时候,它才叫package。

Dependency Hell

依赖地狱。
当A,C都依赖B,A依赖的是B的1.0.0版本。C依赖的是B的2.0.0版本。在B版本能支持多版本共存的情况下,npm如何解决保证让A,C都加载到自己想要的版本?
npm的解决方式是通过加载依赖时路径的查找算法和node_modules的目录结构这两者来配合解决的

查找算法

递归向上查找node_modules里面的package

eg.
如果在 '/home/ry/projects/foo.js' 文件里调用了 require('bar.js'),则 Node.js 会按以下顺序查找:

  • /home/ry/projects/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

-- 递归向上
-- 就近原则

node_modules的目录结构

image.png
image.png

nest mode(npm v2)

根据上面dependency hell的解决方案,我们可以想到如果acorn-jsx和acorn-dynamic-import同时依赖一个package的不同版本,只要在他们自己的目录下维护就好了。因为是就近原则。但是如果此时有另外一个模块,也依赖这个同acorn-jsx相同版本的package,那么就会导致同时存在两个相同版本的package。
如果依赖的过多,会导致大量空间被浪费。这就是臭名昭著的node_modules hell

flat mode(npm v3)

同样,这个模式,是利用向上递归查找的原则,解决nest mode的重复依赖问题。它把重复的依赖提取为公共依赖,放到上一层的node_modules。
但是,如果有四个模块,其中两个依赖了1.0.0版本,另外两个依赖了2.0.0版本,那么不论是把1.0.0放到上一层还是把2.0.0放到上一层,都会造成某个版本依赖两次。这时你可能会想:为啥不把1.0.0和2.0.0都放到上一层,这不就只要install一次吗。如果都放到上一层,我怎么保证我拿到的是1.0.0版本还是2.0.0版本? 这叫做doppelgangers

版本重复问题

版本重复及同时存在多个版本,会出现什么问题?

全局types冲突

一些package会修改全局的类型定义,全局的types形成了命名冲突。解决方式就是自己控制包含哪些加载的

破坏单例模式

Phantom dependecy

对比以上flat mode会比nest mode节省很多空间,同时也带来了phantom dependecy的问题。什么是phantom dependecy?
我们把一个库使用了不属于其dependencies里的package称之为phantom dependecy。我理解:A,C模块依赖1.0.0,现在把1.0.0提升一层,那么在AC的dependencies里面肯定没有1.0.0
另外,在同一个库里面,有可能引用的依赖不在dependencies里面而是在devDependencies里面,我们本地开发运行没有问题,但是发布的话别人下载安装依赖就会有问题了。
并且在使用monorepo管理项目的情况下,问题更加严重。一个package不但可能引入dev环境下的phantom dependecy,也有可能引入其他package的依赖。
在基于npm或者yarn的node_modules的结构下,doppelganger 和 phantom dependency这两个问题似乎并没有太好的解决方式。

Semver(语意化版本)

semver的提出主要用于控制每个依赖package的影响范围,能实现系统的平滑升级和过度。
image.png
前面加个^表示npm install 的时候都会安装符合0.18.0约束的最新依赖。
问题是。并不是所有的库都会遵循。所以。。。
如果直接写死axios的版本依赖,但是不能保证axios的依赖也是写死。所以,packge-lock.json和yarn的lock文件就是实现这样的方式。
如上图的package.json里面声明的axios依赖,我们在生成的package-lock.json文件中可以看到。axios所有的依赖及其依赖的依赖的版本都在lock文件中锁定了。这样其他人来使用这个package就能复现版本。
但是当我们第一次安装创建项目时或者第一次安装某个依赖的时候,此时即使第三方库里含有lock文件。但是npm install 并不会去读取三方依赖的lock,所以还是有可能触发bug。

Resolutions 救火队长

如果你某天安装了一个新的webpack-cli,却发现这个webpack-cli并不能正常工作,经过一番定位发现,是该cli的一个上游依赖portfinder的最近一个版本有bug,但是该cli的作者在休假,没办法及时修复这个cli,但项目赶着上线该怎么处理?yarn提供了一个叫做https://classic.yarnpkg.com/en/docs/selective-version-resolutions/open in new window的机制,使得你可以忽略dependency的限制,强行将portfinder锁定为某个没有bug的版本,以解燃眉之急
npm本身没有提供resolution机制,但是可以通过npm-froce-resolution这个库实现类似机制

determinism

determinism指的是在给定package.json和lock文件下,每次重新install都会得到同样的node_modules的拓扑结构。

PNPM

相比于yarn尽可能的将package放到root level,pnpm则是只将显式写明的dependency的依赖写入root-level的node_modules,这避免了业务里错误的引入隐式依赖的问题,即解决了phantom dependency

pnpm不仅仅能保证一个项目里的所有package的每个版本是唯一的,甚至能保证你使得你不同的项目之间也可以公用唯一的版本(只需要公用store即可),这样可以极大的节省了磁盘空间。核心就在于pnpm不再依赖于node的递归向上查找node_modules的算法,因为该算法强依赖于node_modules的物理拓扑结构,这也是导致不同项目的项目难以复用node_modules的根源。(还有一种干法,就是使用代码的地方写死依赖的版本号,这是deno的干法)

cargo(全局store的包管理系统)

CJM/ESM

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJs 是单个值导出,ES6 Module可以导出多个
  4. CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层
  5. CommonJs 的 this 是当前模块,ES6 Module的 this 是 undefined