前言
关于node包管理的问题总结,之前都是看很多文章但是没有系统的总结要点,导致很多知识点都忘记了。这篇文章用于总结node包管理的一些问题点。 还有一些关于pnpm的常见问题集也值得看一下
- npm@3为什么需要扁平化
- 扁平化带来的弊端
- pnpm比起npm、yarn的优势是什么
- pnpm/npm下载依赖包结构对比,以及不兼容包结构对比
- pnpm如何清除掉全局无用的包
- 如何将npm迁移到pnpm项目
- 在项目里指定pnpm包管理
- CI环境下的优化
- 幽灵依赖问题
npm dedupe解决的问题npm pack打包测试版本的压缩包npm version版本生成
npm@3为什么需要扁平化
首先来看看npm@2有什么问题。该版本采用了嵌套的形式来解决版本冲突问题。这也导致了一些问题即依赖层级过深以及依赖项无法复用问题。如下A\C两个依赖包都有依赖项B无法复用
├─ A
├─ node_modules
| ├─ B@1.0.0
└─ C
├─ node_modules
| ├─ B@1.0.0
为了解决npm@2存在的问题,npm@3实现了依赖扁平化(也没彻底解决可复用以及层级深的问题)。如下A\C两个依赖包都有依赖项B@1.0.0而D依赖包依赖于B@2.0.0最终构建的结构如下(B依赖那个版本在平层,那个版本在Node_modules里,取决于解析的先后顺序):
├─ A
├─ B@1.0.0
├─ C
└─ D
├─ node_modules
| ├─ B@2.0.0
扁平化带来的弊端
- 模块可以访问他们不依赖的软件包,幽灵依赖问题
- 扁平化依赖树的算法非常复杂
- 一些依赖包必须复制到项目的node_modules目录里(不兼容时)
- 依赖树的结构不确定(
安装顺序对依赖树影响特别大)
pnpm的优势是什么
1.解决了幽灵依赖问题
2.速度: pnpm执行的速度更快、3倍的速度,如下图
3.体积: 将包存储在本地磁盘上,在我们创建的项目里使用硬链接的方式,从global store直接链接依赖。(这也是速度快的原因、对于npm和yarn如果有100个项目使用lodash就会有100份lodash拷贝项目目录里)
实际的依赖存储路径可以通过命令pnpm store path或者查看.modules.yaml文件。 也可以通过pnpm store来修改你需要存储依赖的目录地址
执行pnpm install,因为之前装过对应的模块,直接从磁盘里面获取

pnpm/npm下载依赖包结构对比,以及不兼容包结构对比
对于npm@3之前的包结构存在依赖层级过深以及包无法被复用 (A、B依赖包,同时依赖C包,无法被复用).来看看pnpm和npm改进之后的包结构
- pnpm包结构(非展平依赖树,而是依赖包与其依赖项组合在一起,避免了层级过深问题。依赖包与依赖项是通过
符号链接的形式将他们链接在一起。而node_modules的依赖包的文件是来自内容存储的硬链接) pnpm所有依赖的软连接都放置在node_modules/.pnpm/中的对应目录. 把依赖包与依赖包都处于在同一级别避免了循环的软链- 硬链接:
- 符号链接: 下面以express的accepts包结构为例:
.pnpm
└─ node_modules
├─ accepts -> registry.npmmirror.com+accepts@1.3.8
├─ registry.npmmirror.com+accepts@1.3.8
├─ node_modules
| ├─ accepts // 相关内容
| | // 依赖包与其依赖项组合在一起 (以符号链接的形式链接到外层对应的文件,解决了扁平化带来问题.不展开依赖树)
| ├─ negotiator -> registry.npmmirror.com+negotiator@0.6.3
| └─ mime-types -> registry.npmmirror.com+mime-types@2.1.35
└─ registry.npmmirror.com+mime-types@2.1.35
├─ node_modules
| ├─ mime-types // 相关内容
| | // 依赖包与其依赖项组合在一起 (以符号链接的形式链接到外层对应的文件,解决了扁平化带来问题)
| ├─ mime-db -> registry.npmmirror.com+mime-db@1.52.0
...
// 不兼容包 (我在项目里安装accepts@1.0.0版本与上面的1.3.0 不兼容)
.pnpm
├─ registry.npmmirror.com+accepts@1.0.0
├─ registry.npmmirror.com+accepts@1.3.8
...
- npm包结构(扁平化之后)
node_modules
├─ accepts
├─ mime-types
├─ express
...
// 不兼容 (安装accepts1.0.0 和 express => accepts在该包内部)
├─ accepts@1.0.0
├─ express
├─ node_modules
| ├─ accepts@1.3.8
pnpm如何清除掉全局无用的包
使用方法pnpm store prune。它提供了一种用于删除一些不被全局项目所引用到的 packages 的功能。假如之前有一个项目引用了lodash@1.0.0,此时将该项目里的lodash更新为1.1.0。那么全局store目录存储的lodash@1.0.0就不再被引用,应该将它移除掉。(节省本地磁盘开销)
pnpm store prune

如何将npm迁移到pnpm项目
既然pnpm优点这么多,那么如何将已有的npm迁移成pnpm项目也很简单。在项目里执行pnpm import将npm的lock文件生成pnpm-lock.yaml文件。重新pnpm install即可
pnpm import

在项目里指定pnpm包管理
为了规范团队,为了防止开发人员使用其他的包管理器npm/yarn install。通过配置npx only-allow ~
npx only-allow 包管理器名称
"scripts": {
"preinstall": "npx only-allow pnpm"
}
CI环境下的优化
通过执行npm ci,相对于npm install命令,他有如下几个不同点:
npm ci要求项目必须存在package-lock.json或npm-shrinkwrap.json文件npm ci完全依据package-lock.json文件安装依赖。保证团队之间使用一致性的依赖版本npm ci完全依据package-lock.json。因此在安装过程中就不需要解决依赖满足问题以及构造依赖树的问题npm ci会先删除项目中的node_module再安装npm ci无法安装单个依赖包npm ci如果lock文件与package.json冲突,则报错npm ci不会更新lock文件
幽灵依赖问题
假设A包依赖于B包,此时我在项目里只安装了A包,但是我可以在模块里B的语法。 具体可以查看该文章
npm install A
├─ A
└─ B
index.ts
import fn from 'B' // 合法的
为什么pnpm不存在幽灵依赖问题
- 使用符号链接和硬链接构建node_modules
- 每个包的依赖会被精准放在其自身的node_modules下,不会提升到根目录
- pnpm的node_modules中,只有package.json显式声明的依赖会出现在根目录,其他子依赖会被严格隔离在.pnpm目录中
node_modules/
├── A # 项目直接依赖
└── .pnpm/ # 所有依赖的真实存储位置
├── A@1.0.0/
│ └── node_modules/
│ ├── A
│ └── B -> ../../B@1.0.0 # B 是 A 的依赖,仅对 A 可见
└── B@1.0.0/
└── node_modules/
└── B
npm dedupe解决的问题
假设A@1.0.0版本依赖B@1.0.0版本、c@1.0.0版本依赖于B@2.0.0。在npm中安装顺序对于构建依赖树的影响特别大. 如下A包先更新
├─ A
├─ B@1.0.0
└─ C
├─ node_modules
| ├─ B@2.0.0
紧接着我更新了A@2.0.0此时依赖于@B2.0.0.但是由于顶层存在@B1.0.0那么更新A时它的依赖项B会在A的node_module里,且B@1.0.0并没有被销毁。如下结构:
├─ A
├─ node_modules
| ├─ B@2.0.0
├─ B@1.0.0
└─ C
├─ node_modules
| ├─ B@2.0.0
上面的依赖结构很明显不是我们想要的,我们想要的应该是如下的结构,如何实现呢,执行npm dedupe即可
├─ A
├─ B@2.0.0
└─ C
npm dedupe减少重复依赖
npm pack打包测试版本的压缩包
有时候我们在开发包的时候,暂时还不想要发布到线上,但是第三方业务有想要先用着,此时就可以通过npm pack打包一个本地版本的压缩包丢给他们即可。 如果自己本地开发就通过npm link来实现
pnpm pack
// 在项目中使用
pnpm install ~(压缩包路径)

npm version
当我们开发sdk时可能会存在三种环境测试环境、预发布环境、生成环境。那么我们应该如何管理sdk的版本号呢?
场景: 假设现在有个需求版本迭代为1.1.0。此时我们是需要经过测试环境、预发布环境的测试通过才能够将这个SDK发布到生成环境,在测试阶段可能会发现问题需要修复,如果一直版本号都是1.1.0那么我们将无法确定是那次的修改存在问题。为了解决这个问题,我的解决方案是引入-beta.x (测试环境)和-rc.x (预发布环境)版本号的规定
// 原始版本号为1.0.0
version: 1.0.0
// 生成测试阶段版本
// 当version不存在--preid=x,会通过prepatch、preminor、premajor设置对应前三位版本号
npm version prepatch --preid=beta // 1.0.1-bate.0
npm version preminor --preid=beta // 1.1.0-bate.0
npm version premajor --preid=beta // 2.0.0-bate.0
// 当版本号为1.0.1-bate.0再次执行
npm version prepatch --preid=beta // 1.0.1-bate.1
// 正式版本 (测试版本1.0.1-bate.1会变成下面情况)
npm version patch // 1.0.1
npm version minor // 1.1.0
npm version major // 2.0.0
经过上述可以知道生成测试版本通过npm version pre~ --preid=x,生成正式版本通过npm version ~
总结
看了很多文章,到最后都忘记,哈哈哈。
这里强烈推荐看一下国外一个博主出的视频讲解。可以使用YouTube中英文字母谷歌插件来看视频。
这里推荐一款快速删除node_modules的脚手架支持多包模式下的删除npkill。