用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。
执行 npm init, npm install express,可以看到node_modules目录如下:
可以看到,npm2的node_modules是嵌套的。
这种方式的优点就是模块依赖关系清晰。
缺点也比较明显:
当时npm还没解决这些问题,社区就出来一个新的解决方案了,那就是 yarn。
yarn生成的node_modules目录如下:
将所有内部依赖平铺到最外面一层,解决了上述嵌套方案的缺陷。
后面npm3 + 也采用类次方案实现了。
所有的依赖都被拍平到node_modules目录下,不再有很深层次的嵌套关系。这样在安装新的包时,根据 node require 机制,会不停往上级的node_modules当中去找,如果找到相同版本的包就不会重新安装,解决了大量包重复安装的问题,而且依赖层级也不会太深。
之前的问题是解决了,但仔细想想这种扁平化的处理方式,它真的就是无懈可击吗?并不是。它照样存在诸多问题:
怎么理解第一条中的不确定性呢?
假如现在项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的:
那么 npm/yarn install 的时候,通过扁平化处理之后,究竟是这样
还是这样的?
答案是: 都有可能。取决于 foo 和 bar 在 package.json中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。
这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x才出现)还是yarn.lock,都是为了保证 install 之后都产生确定的node_modules结构。
Phantom dependencies幽灵依赖
Phantom dependencies 被称之为幽灵依赖或幻影依赖,解释起来很简单,即某个包没有在package.json 被依赖,但是用户却能够引用到这个包。
比如yarn打包:
A依赖B, B依赖C,,那么 A 就算没有声明 C 的依赖,由于有依赖提升的存在,C 被装到了 A 的node_modules里面,在A里面引入C,没什么问题的。
但是,
第一, B 的版本是可能随时变化的,假如之前依赖的是C@1.0.1,现在发了新版,新版本的 B 依赖 C@2.0.1,那么在项目 A 当中 npm/yarn install 之后,装上的是 2.0.1 版本的 C,而 A 当中用的还是 C 当中旧版的 API,可能就直接报错了。
第二,如果 B 更新之后,可能不需要 C 了,那么安装依赖的时候,C 都不会装到node_modules里面,A 当中引用 C 的代码直接报错。
还有一种情况,在 monorepo 项目中,如果 A 依赖 X,B 依赖 X,还有一个 C,它不依赖 X,但它代码里面用到了 X。由于依赖提升的存在,npm/yarn 会把 X 放到根目录的 node_modules 中,这样 C 在本地是能够跑起来的,因为根据 node 的包加载机制,它能够加载到 monorepo 项目根目录下的 node_modules 中的 X。但试想一下,一旦 C 单独发包出去,用户单独安装 C,那么就找不到 X 了,执行到引用 X 的代码时就直接报错了。
npm 也有想过去解决这个问题,指定**–global-style**参数即可禁止变量提升,但这样做相当于回到了当年嵌套依赖的时代,一夜回到解放前,前面提到的嵌套依赖的缺点仍然暴露无遗。
回想下,npm3+和yarn为什么要做node_modules 扁平划处理,不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?
那如果不复制文件,比如通过link。
打开node_modules如下:
可以看到,该目录结构比较清晰,只有一个我们直接依赖的包express,并没有把express需要引用的包平铺开,并且也不在express包下面嵌套。
同时还有个 .pnpm 目录,目录结构如下:
.pnpm
├── lock.yaml
├── node_modules
│ ├── .bin
│ ├── accepts
│ ├── array-flatten
│ ├── body-parser
│ ├── bytes
│ ├── call-bind
│
│
├── registry.npmmirror.com+accepts@1.3.8
│ └── node_modules
├── registry.npmmirror.com+array-flatten@1.1.1
│ └── node_modules
├── registry.npmmirror.com+body-parser@1.20.1
│ └── node_modules
├── registry.npmmirror.com+bytes@3.1.2
│ └── node_modules
├── registry.npmmirror.com+call-bind@1.0.2
│ └── node_modules
.pnpm 以平铺的形式储存着所有的包,正常的包都可以在这种命名模式的文件夹中被找到:
.pnpm/+@/node_modules/
// 组织名(若无会省略)+包名@版本号/node_modules/名称(项目名称)
我们称.pnmp为虚拟存储目录,该目录通过
来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,所以它不会存在之前提到的 幽灵依赖 问题!
那么它如何跟文件资源进行关联的呢?又如何被项目中使用呢?
答案是Store + Links!
pnpm
资源在磁盘上的存储位置。
由于每个磁盘有自己的存储方式,所以Store会根据磁盘来划分。 如果磁盘上存在主目录,存储则会被创建在 /.pnpm-store;如果磁盘上没有主目录,那么将在文件系统的根目录中创建该存储。 例如,如果安装发生在挂载在 /mnt 的文件系统上,那么存储将在 /mnt/.pnpm-store 处创建。 Windows系统上也是如此。
可以在不同的磁盘上设置同一个存储,但在这种情况下,pnpm
将 复制包 而不是 硬链接 它们,因为硬链接只能发生在同一文件系统同一分区上。
如果是 npm 或 yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。
pnpm 是怎么做到如此大的性能提升的呢?一部分原因是使用了计算机当中的 Hard link ,它减少了文件下载的数量,从而提升了下载和响应速度。
通过hard link, 用户可以通过不同的路径引用方式去找到某个文件,需要注意的是一般用户权限下只能硬链接到文件,不能用于目录。
pnpm 会在Store(上面的Store) 目录里存储项目 node_modules 文件的 hard links ,通过访问这些link直接访问文件资源。
举个例子,例如项目里面有个 2MB 的依赖 react,在 pnpm 中,看上去这个 react依赖同时占用了 2MB 的 node_modules 目录以及全局 store 目录 2MB 的空间(加起来是 4MB),但因为 hard link 的机制使得两个目录下相同的 2MB 空间能从两个不同位置进行 CAS寻址 直接引用到文件,因此实际上这个react依赖只用占用2MB 的空间,而不是4MB。
如何判断是否是hard link?
- mac和linux中,hard link的文件和普通文件无异,node甚至无法区分hard link文件;
- 可以通过
ls -i
列出文件信息,第一个参数就是文件的inode
值,其中,具有相同inode
节点的多个文件互为hard link文件;- 这样 就可以通过不同项目观察同一个依赖文件是不是一样的 inode 就能确定是不是同为 hard link了。
由于hark link只能用于文件不能用于目录,但是pnpm的node_modules是树形目录结构,那么如何链接到文件? 通过symbolic link(也可称之为软链或者符号链接)来实现!
pnpm在全局通过Store来存储所有的node_modules依赖,并且在.pnpm/node_modules中存储项目的hard links,通过hard link来链接真实的文件资源,项目中则通过symbolic link链接到.pnpm/node_modules目录中,依赖放置在同一级别避免了循环的软链。
如何判断是否是symbolic link?
打开文件夹,显示简介,mac会有展示,如下图:
官方给了一张原理图,
从上图可以看出,项目直接依赖 bar, bar又依赖 foo包。
pnpm下载依赖时,首先在node_modules下创建一个bar@1.0.0的symbolic link,链向图示位置,然后在.pnpm中平铺所有依赖,观察bar@1.0.0/node_modules/,里面的bar文件hard link真实文件,foo又是symbolic link到上层foo@1.0.0,这样bar就能够根据路径找到foo了。
根据计数引用原理删除不需要的依赖包
首先 npx 是一个工具,旨在帮助完善使用npm注册表中的软件包的体验-npm使得超级容易安装和管理注册表中托管的依赖项,npx使得使用CLI工具和托管在该注册表中的其他可执行文件变得容易 注册表。 到目前为止,它大大简化了许多需要使用纯npm进行一些程序化的事情:
直接调用项目安装的模块
npm install -D mocha // 项目中安装mocha测试模块
// 一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts字段里面,
// 如果想在命令行下调用,必须像下面这样。
// 项目的根目录下执行 node-modules/.bin/mocha --version
// 如果使用npx
npx mocha --version
// npx运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
控制调用本地/远程模块
npx --no-install http-server
npx --ignore-existing create-react-app my-react-app
执行一次性命令,避免全局安装
$ npx create-react-app my-app 安装一个临时的 create-react-app 并且调用它,不会污染全局安装
场景:
比如尝试一个cli工具,可能这辈子只能用到这一次,不想这次使用完后还一直存在电脑硬盘里占用内存。
步骤:
使用不同版本的Node(同样适用于其他库的指定版本)
npx -p node@ node -v 可以用于一次性运行node版本
执行Github源码
# 执行 Gist 代码
$ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
# 执行仓库代码
$ npx github:piuccio/cowsay hello
这是一个切换node版本的一个工具。和 n
命令类似。
- n 是一个需要全局安装的 npm package。所以在使用n时,必须得有一个版本的node环境。
- 在安装的时候,n 会先将指定版本的 node 存储下来,然后将其复制到/usr/local/bin。
- nvm 是一个独立软件包,需要单独使用它的安装逻辑。
- 在使用 nvm 安装 node 的时候,nvm 将不同的 node 版本存储到 ~/.nvm// 下,然后修改 $PATH,将指定版本的 node 路径加入,这样我们调用的 node 命令即是使用指定版本的 node。
- nvm 的全局模块存在于各自版本的沙箱中,切换版本后需要重新安装,不同版本间也不存在任何冲突。