包管理工具
说明
npm,yarn和pnpm、软硬链接、嵌套安装与扁平安装、npx、.npmrc笔记总结
# npm
npm(“Node 包管理器”)是 JavaScript 运行时 Node.js 的默认程序包管理器。
它也被称为“Ninja Pumpkin Mutants”,“Nonprofit Pizza Makers”,以及许多其他随机名称,你可以在 npm-expansions (opens new window) 上探索这些名称。
npm 由两个主要部分组成:
- 用于发布和下载程序包的 CLI(命令行界面)工具
- 托管 JavaScript 程序包的 在线存储库 (opens new window)
# yarn
Yarn 是由 Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具,是为了弥补 npm 的一些缺陷而出现的
安装过的包会被保存进缓存目录,以后安装就直接从缓存中复制过来,这样做的本质还是会提高安装下载的速度,避免不必要的网络请求。
# 嵌套安装
npm@3 之前,node_modules 中的每个依赖项都有自己的node_modules文件夹,在package.json中指定了所有依赖项。例如下面所示,项目依赖了foo
,foo
又依赖了bar
,依赖关系如下图所示
node_modules
└─ foo
├─ index.js
├─ package.json
└─ node_modules
└─ bar
├─ index.js
└─ package.json
2
3
4
5
6
7
8
上面结构有两个严重的问题:
package中经常创建太深的依赖树,这会导致 Windows 上的目录路径过长问题
当一个package在不同的依赖项中需要时,它会被多次复制粘贴并生成多份文件
# 扁平安装
优点:
减少不必要的安装,假如A和B都需要依赖C那么扁平的安装就可以让A和B都能使用C,不需要在各自内部文件夹单独安装C
缺点:
幽灵依赖:由于扁平化导致源码可以访问本不属于当前项目所设定的依赖包;
项目里使用了,但是未在项目的package.json 中定义的包。就是幽灵依赖。 按着原理来说 未在package.json中定义的包 不会被下载,但是它偏偏就存在了,也被下载了,这又是为什么呢? 因为依赖提升,造成的副作用
比如说我们这里有一个项目,安装了 A 这个库,版本是 v1,但是 A 库又依赖一个 B 库,版本也是 v1。
我们项目里明明没有手动安装这个 B 库,但是在项目里边仍然可以去导入它并且使用,这就产生了幽灵依赖。
一旦有一天因为某种原因,我们要把 A 库进行升级,升级的 v2 的版本,v2 这个版本有可能要使用 B 库的 v2 版本,于是 B 库也会跟着升级,而 B 库升级之后,它里边有些 API 可能有变动,那么就会导致我们之前用 B 库的代码全部出问题了。
NPM分身:不同版本的情况下有大量的依赖可能被重复安装,这种场景在monorepo 多包场景下尤其明显,这也是
yarn workspace
经常被吐槽的点,另外扁平化的算法实现也相当复杂,改动成本很高
项目中有packageA、packageB、packageC、packageD。packageA依赖packageX 1.0和packageY 1.0,packageB依赖packageX 2.0和packageY 2.0,packageC依赖packageX 1.0和packageY 2.0,packageD依赖packageX 2.0和packageY 1.0
- package X => 1.0版本
- package Y => 1.0版本
- package A
- package B
- packageX 2.0
- packageY 2.0
- package C
- packageY 2.0
- package D
- packageX 2.0
packageX 2.0和packageY 2.0被重复安装多次,从而造成 npm 和 yarn 的性能一些性能损失。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 软链接和硬链接
软链接:类似于Windows的快捷方式,会创建新的文件和 inode,但是软链接文件inode指向源文件的 inode
硬链接:只能用于文件不能用于目录,不会创建额外 inode,它和源文件共用同一个 inode,修改源文件之后,硬链接中的文件内容也同时发生了变更
- 删除源文件不会影响硬链接文件的访问(因为inode还在)
- 删除源文件会影响软链接文件的访问(因为指向的inode已经不存在了)
- 硬链接不占用磁盘空间,软链接占用的空间只是存储路径所占用的极小空间
# pnpm
使用 npm 时,依赖每次被不同的项目使用,都会重复安装一次。 而在使用 pnpm 时,依赖会被存储在内容可寻址的存储中,所以如果你用到了某依赖项的不同版本,只会将不同版本间有差异的文件添加到仓库。 例如,如果某个包有100个文件,而它的新版本只改变了其中1个文件。那么 pnpm update
时只会向存储中心额外添加1个新文件,而不会因为仅仅一个文件的改变复制整新版本包的内容。
pnpm 本质上就是一个包管理器,这一点跟 npm/yarn 没有区别,但它具有两个优势:
- 包安装速度极快;
- 磁盘空间利用非常高效。
pnpm在安装依赖包时,不像npm和yarn那样将每个项目的依赖包存储到各自的node_modules中,而是将依赖包存储到系统的全局存储库中(通常在 ~/.pnpm-store 目录下)使用 pnpm 对项目安装依赖的时候,如果某个依赖在 store目录中存在了话,那么就会直接从 store 目录里面去 hard-link硬链接减少了文件下载的数量,从而提升了下载和响应速度。避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。接着在项目中则通过symbolic link
软链接到.pnpm/node_modules
目录中
pnpm:网状 + 平铺的node_modules结构
.pnpm 目录之外的其实是我们在日常开发中实际引用的是依赖,但是它对于 pnpm 来说它是软链接,最终软链接实际引用的还是 .pnpm 中的真实依赖。解决“幽灵依赖”的问题,只有声明过的依赖才会以软链接的形式出现在 node_modules 目录中。在实际项目中引用的是软链接,软链接指向的是 .pnpm 的真实依赖,所以在日常开发中不会引用到未在 package.json 声明的包
.pnpm
(虚拟存储目录)以平铺的形式储存着所有的包,正常的包都可以在这种命名模式的文件夹中被找到,该目录通过<package-name>@<version>
来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升,如果 有 peer 依赖(peer dependencies),那么它可能就会有多组依赖项,所以我们为不同的 peer 依赖项创建不同的解析
- foo-parent-1
- bar@1.0.0
- baz@1.0.0
- foo@1.0.0
- foo-parent-2
- bar@1.0.0
- baz@1.1.0
- foo@1.0.0
node_modules
└── .pnpm
├── foo@1.0.0_bar@1.0.0+baz@1.0.0
│ └── node_modules
│ ├── foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ ├── baz -> ../../baz@1.0.0/node_modules/baz
│ ├── qux -> ../../qux@1.0.0/node_modules/qux
│ └── plugh -> ../../plugh@1.0.0/node_modules/plugh
├── foo@1.0.0_bar@1.0.0+baz@1.1.0
│ └── node_modules
│ ├── foo
│ ├── bar -> ../../bar@1.0.0/node_modules/bar
│ ├── baz -> ../../baz@1.1.0/node_modules/baz
│ ├── qux -> ../../qux@1.0.0/node_modules/qux
│ └── plugh -> ../../plugh@1.0.0/node_modules/plugh
├── bar@1.0.0
├── baz@1.0.0
├── baz@1.1.0
├── qux@1.0.0
├── plugh@1.0.0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
安装子包的依赖
除了进入子包目录直接安装pnpm add pkgname
之外,还可以通过过滤参数 --filter
或-F
指定命令作用范围。我在packages目录下新建两个子包,分别为tools
和mini-cli
,假如我要在min-cli
包下安装react
,那么,我们可以执行以下命令
pnpm --filter mini-cli add react
# pnpm workspace 工作空间协议
默认情况下,如果可用的packages
与已声明的可用范围相匹配,pnpm 将从工作区链接这些packages
。 例如,如果 bar
中有 "foo":"^1.0.0"
的这个依赖项,则 foo@1.0.0
链接到 bar
。 但是,如果 bar
的依赖项中有"foo": "2.0.0"
,而foo@2.0.0
在工作空间中并不存在,则将从npm registry安装foo@2.0.0
。 这种行为带来了一些不确定性。
pnpm
从版本 3.7 开始支持工作区协议workspace:
。当使用此协议时,pnpm 将拒绝解析除本地工作区 package
之外的任何内容。 因此,如果您设置为 "foo": "workspace:2.0.0"
时,安装将会失败,因为 "foo@2.0.0"
不存在于工作空间中。这个特性在monorepo当中特别有用。
{
"dependencies": {
"foo": "workspace:*",
"bar": "workspace:~",
"qar": "workspace:^",
"zoo": "workspace:^1.5.0"
}
}
被解析为
{
"dependencies": {
"foo": "1.5.0",
"bar": "~1.5.0",
"qar": "^1.5.0",
"zoo": "^1.5.0"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以通过修改配置link-workspace-packages
来改变包的使用方式(远端下载 or 本地)。link-workspace-packages
有三个值
- true 默认
- false 禁用后,可实现仅从
registry
安装
# package-lock.json文件
![image-20230321084803087](/assets/img/image-20230321084803087.b0abc25e.png)
- -S是--save的简写表示开发和部署上线都会用到 dependencies属性
- -D是--save-dev的简写表示开发用到但部署上线不会用到 devDependencies属性 如babel webpack
- peerDependencies属性 表示对等依赖 依赖一个包必须以另一个宿主为前提 比如element-plus 依赖Vue3
- 版本依赖管理 ^x.y.z表示x保持不变yz安装最新版本 ~x.y.z表示xy保持不变z安装最新版本
# npx
npx是一个工具,npm v5.2.0引入的一条命令(npx),一个npm包执行器,旨在提高从npm注册表使用软件包的体验
调用项目安装的模块
npx 的原理就是运行的时候,会到node_modules/.bin
路径和环境变量$PATH
里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH
,所以系统命令也可以调用。
一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts
(opens new window)字段里面, 如果想在命令行下调用,必须像下面这样。
# 项目的根目录下执行
node-modules/.bin/mocha --version
# npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。
npx mocha --version
2
3
4
避免全局安装模块
除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app
这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
npx create-react-app my-react-app
上面代码运行时,npx 将create-react-app
下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app
。
利用 npx 可以下载模块这个特点,可以指定某个版本的 Node 运行脚本。它的窍门就是使用 npm 的 node 模块
npx node@0.12.8 -v v0.12.8
1
2
上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
# .npmrc
npmrc是npm的配置文件,它位于用户主目录下的.npmrc
文件或项目根目录下的.npmrc
文件。npmrc文件由一系列键值对组成,用于配置npm在执行命令时的行为和参数
定义镜像源,管理依赖存储路径、保存开发依赖...
深入了解npmrc:使用与配置指南 - 掘金 (juejin.cn) (opens new window)
# npm install 过程
图片勘误: 压缩到node_modules改为解压到node_modules
- 解析package.json文件:根据项目中的package.json文件确定需要安装的依赖包及其版本。
- 下载依赖包:从NPM仓库下载需要安装的依赖包及其相关依赖包。
- 安装依赖包:将下载下来的依赖包解压缩,并将其复制到项目目录的node_modules文件夹下。
- 执行依赖包的安装脚本:如果依赖包中包含installer或preinstall脚本,那么在安装后会被执行。
- 生成依赖关系树:根据依赖包的依赖关系,生成依赖关系树。
- 保存依赖信息:将安装的依赖包及其版本号保存到package-lock.json或yarn.lock文件中,以便在下次安装时使用。
# peerDependencies
peer 依赖,也叫同等依赖,或者叫同伴依赖,用于指定当前包(也就是你所要开发的包)使用的这个依赖要兼容的宿主环境中这个依赖的版本。用于解决插件与所依赖包不一致的问题。防止项目安装该插件时,多次安装相同的库
比如开发vue相关的插件时,需要将vue这一依赖作为peerDependencies项,这样,当插件在vue项目中使用时,vue这个依赖,就会被安装一次了。
# 参考
peers 是如何被处理的 | pnpm官网 (opens new window)
pnpm快到碗里来! - 掘金 (juejin.cn) (opens new window)
[译]用 PNPM Workspaces 替换 Lerna + Yarn - 掘金 (juejin.cn) (opens new window)
什么是 npm —— 写给初学者的编程教程 (freecodecamp.org) (opens new window)
npm和yarn的区别,我们该如何选择? - 简书 (jianshu.com) (opens new window)