前端依赖报错不用愁,自救技巧大放送!
背景
作为前端开发者而言,在日常工作中最讨厌的事情莫过于之前还能正常运行的项目,今天突然运行不起来了,各种红色刺眼的依赖报错让人应接不暇。依赖报错就像是前端开发者的"宿敌",让人感到头疼和无力,很多人对此退避三舍、束手无策又无可奈何。
这篇文章旨在让读者对于包管理器管理依赖建立起最基本的认识,并可以在日常问题中有一个基本排查思路。
推荐读者:掌握nodejs的使用、有一定的包管理器使用经验的开发者。
依赖的类型
每个前端项目中, package.json
文件内都声明了该项目所用到的所有依赖,一般会有三种类型,dependencies
、devDependencies
、peerDependencies
。网传的一些说法是:
对于 peerDependencies 下面再详细介绍~
dependencies
:项目的生产依赖,生成环境下所依赖的包。devDependencies
:项目的开发依赖,只在开发环境下才会依赖的包。
更有甚者直接简化为:dependencies
:生产依赖,devDependencies
:开发依赖。
那么这里笔者提出一个问题:以一个最简单的react
项目来说,如果把react
、react-dom
等依赖全部移入devDependencies
中,那么这个项目还可以跑得起来吗?
有兴趣的同学可以自行测试,修改后重新执行npm i
、npm run start
即可。
测试过便知,项目的运行只能说一帆风顺、毫无波澜。这里甚至可以下一个结论:
在开发过程中,你甚至可以将dependencies
、devDependencies
混为一谈,也丝毫不会对开发有任何的影响。 那么,网传的dependencies
:生产依赖,devDependencies
:开发依赖到底是怎么来的呢?其实,dependencies
、devDependencies
他们的区别是在构建过程中体现出来的,devDependencies
在包构建过程中并不会被打包进来。而上述问题主要是依赖下载过程,依赖的下载是包管理器的责任,在 install 过程中并不会对不同类型的依赖区别对待。
peerDependencies
peerDependencies
顾名思义同步依赖,例如鱼之于水,草之于土地,以前端视角来看,那就是vuex
之于vue
,react component
之于react
。
需要注意的是,如果你使用的是npm,不同版本对于peer Dependencies的下载行为有所差异:
- 在npm4之前npm会自动帮你下载好peer Dependencies。(Prior to version 4, npm automatically included peer dependencies if they weren't explicitly included. )
- npm4-6: 不会自动安装,但是会报warning
- npm7: 更新了新的包依赖解析算法,因此恢复了自动下载的机制
包依赖文件系统
扁平化依赖(hoist)
假设我们有一个项目,他依赖A和B两个包,而A和B又分别依赖着C,则下载完毕后node_modules
呈现的结构是怎样的呢?
在npm1
、npm2
的时候呈现的是嵌套结构:
bash
node_modules
| A
| └─ node_modules
| └─ C
| ├─ index.js
| └─ package.json
└─ B
└─ node_modules
└─ C
├─ index.js
└─ package.json
这样的嵌套结构会导致以下问题:
- 嵌套结构过长在Windows系统中会产生问题。
- 相同依赖重复下载、具有不同的多个实例。
而自从npm3
过后,包括yarn
,都采用了扁平化依赖的方式进行依赖管理,相比上述的嵌套结构,如今的node_modules
看起来平滑多了,且C不会被重复安装:
bash
node_modules
| A
| └─ node_modules
└─ B
| └─ node_modules
└─ C
├─ index.js
├─ node_modules
└─ package.json
这里对于dependencies
、devDependencies
来说就有着显著区别。假设有一个项目中的依赖关系是这样的:
那么最终安装完毕后的node_modules
结构会变成:
bash
// My Package
node_modules
| A // My Package 的 dependencies
| └─ node_modules
└─ B // My Package 的 devDependencies
| └─ node_modules
└─ C // A 的 dependencies
|
└─ E // B 的 dependencies
即对于app来说,dependencies
、devDependencies
下载后会被全部扁平化,但是二级依赖中的devDependencies
将不会被下载。
这也符合我们上面所说的,因为二级依赖已经属于构建后包,自然不会包含devDependencies
。
扁平化依赖的缺陷---幽灵依赖
扁平化依赖的实现并不是完美的,它也带来了一些新的问题:
- 首先是扁平化算法的复杂度,导致了在工作中一些大项目的依赖安装需要耗时几分钟甚至更久。
- 在我们的第一个例子中(假设我们有一个项目,他依赖A和B两个包,而A和B又分别依赖着C),如果A和B分别依赖的不同版本的C,则被扁平化上来的C究竟是哪个版本呢?
相同依赖的版本不确定性,就是package-lock.json
文件的诞生原因。
- 导致幽灵依赖的非法访问
什么是幽灵依赖呢?
让我们回想一下依赖包的寻路算法:从当前目录的node_modules开始,依次向上层目录的node_modules查询。既然扁平化算法将所有依赖都扁平化到了node_modules中,未包含在package.json
中的依赖也自然放在了node_modules
中,项目自然也可以访问到它们了。
这就是幽灵依赖的由来。这也是项目莫名其妙无法运行的罪魁祸首之一:试想一下,你依赖了幽灵依赖,当某一天你的正式依赖不再依赖该幽灵幽灵依赖,项目不就访问不到它了嘛?
pnpm
什么是pnpm?引用官方的一句介绍:Fast, disk space efficient package manager,跟npm、yarn一样,pnpm也是一个包管理器,但是它更快、利用磁盘空间更加高效。
pnpm解决幽灵依赖
与npm、yarn不同的是,pnpm解决了幽灵依赖的缺陷,保证了项目的稳定性。
举个例子,让我们安装一个vue,得到的依赖目录是这样的:
bash
node_modules
|------vue
|__.pnpm
|__ @vue+compiler-core..
|__ @vue+.....
可见,node_modules
下只包含了package.json
中声明好的依赖,幽灵依赖并不会被hoist上来。但是可以注意到,目录中多了一个.pnpm/**
的结构,而且里面的不正是vue
的依赖包嘛?
pnpm
默认不允许项目中使用幽灵依赖,但是对于二级依赖而言,是默认允许彼此访问幽灵依赖的。官网上也有具体说明。
然而,pnpm
对于项目中的幽灵依赖实际上并没有完全禁止,而是给eslint
、prettier
等插件开了后门。
正是由于npm三方包的鱼龙混杂,pnpm
才不得已而为之。如果你是一位有着代码洁癖的前端工程师,不如坚决地将hoist
设置为false
,即使这会让其他小伙伴在第一次项目初始化时痛苦万分。
当然,你如果不追求这种严谨性,你也可以设置shamefully-hoist
为ture
,此时不管项目还是三方库中的所有依赖都会被hoist到node_modules
中,正如npm、yarn中的行为。
symlink & hardlink
还记得我们之前讨论过的不确定性嘛?假设我们有一个项目,他依赖A和B两个包,而A和B又分别依赖着C),如果A和B分别依赖的不同版本的C。这里我们以express
和koa
都依赖不同版本的http-errors
举例子:
则最终的结构会变成(结构有删减):
bash
├── node_modules
│ ├── .modules.yaml
│ ├── .pnpm
│ │ ├── express@4.18.2
│ │ │ └── node_modules
│ │ │ ├── express -> {store}
│ │ │ │ └── package.json
│ │ │ ├── http-errors -> ../../http-errors@2.0.0/node_modules/http-errors
│ │ │ ├── merge-descriptors -> ../../merge-descriptors@1.0.1/node_modules/merge-descriptors
│ │ │ ├── methods -> ../../methods@1.1.2/node_modules/methods
│ │ │ ├── ...
│ │ ├── http-errors@1.8.1
│ │ │ └── node_modules
│ │ │ │ └── package.json
│ │ ├── http-errors@2.0.0
│ │ │ └── node_modules
│ │ │ │ └── package.json
│ │ └── koa@2.14.2
│ │ └── node_modules
│ │ ├── http-errors -> ../../http-errors@1.8.1/node_modules/http-errors
│ │ ├── koa -> {store}
│ │ │ └── package.json
│ │ ├── koa-compose -> ../../koa-compose@4.1.0/node_modules/koa-compose
│ │ ├── koa-convert -> ../../koa-convert@2.0.0/node_modules/koa-convert
│ │ └── ...
│ ├── express -> .pnpm/express@4.18.2/node_modules/express
│ └── koa -> .pnpm/koa@2.14.2/node_modules/koa
├── package.json
└── pnpm-lock.yaml
这里可以注意到,node_modules
下只有express
和koa
,这也是上节提到的杜绝项目幽灵依赖的办法。更多的是,他们都是软连接,以express
来说实际上连接到了.pnpm/express@4.18.2/node_modules/express
中,而.pnpm/express@4.18.2/node_modules
下也是其依赖包,也都是软连接到了.pnpm/
中。不同版本的http-errors
也放置在了.pnpm/http-errors@xxx
中,这样就保证了包引用的正确性。
值得注意的是,这种结构中,.pnpm/xxx/node_modules/xxx {store}
是一个指向全局的hardlink,这样就避免了依赖重复下载以及软连接嵌套的问题。
依赖报错怎么办?
这里以我实际工作中遇到的案例作为引子:
我使用到了一个react组件SDK,其package.json
如下:
而我的主项目中react
的版本是17.0.2
,这就导致依赖安装后,存在不同版本的react
(node_modules/.pnpm
):
这里导致SDK中使用的是17.0.1
的react-dom
,与项目中的17.0.2
的react
发生版本冲突。
遇到这种情况,我们应该想到有以下四种方式修改依赖:
- overrides
设置overrides
可以让你强行制定依赖包的版本。参考文档:pnpm.io/package_jso...
需要注意的是,该字段应该被设置在根目录
package.json
下,但在monorepo
下无法对单个package
进行设置。
json
{
"pnpm": {
"overrides": {
"foo": "^1.0.0",
"quux": "npm:@myorg/quux@^1.0.0",
"bar@^2.1.0": "3.0.0",
"qar@1>zoo": "2"
}
}
}
- packageExtensions
你可以通过packageExtensions
来修改三方依赖的依赖,举个例子,react-redux
理当将react-dom
添加进peerDependencies
中,但它缺失了,那我们就可以给他添加上:
json
{
"pnpm": {
"packageExtensions": {
"react-redux": {
"peerDependencies": {
"react-dom": "*"
}
}
}
}
}
- pnpm hooks
如果你想要更加精准地控制依赖,你可以使用pnpm hooks
。Hooks 可以被定义在.pnpmfile.cjs
中。.pnpmfile.cjs
文件应该被放置在与lock文件相同的目录中。
参考资料:pnpm.io/pnpmfile
-
hooks.readPackage(pkg, context): pkg
:在pnpm解析完package.json
中的依赖名称清单后调用,用于改变package.json
中的依赖。 -
hooks.afterAllResolved(lockfile, context): lockfile
:允许你在序列化lockfile输出之前对其进行修改,用于改变pnpm-lock.yaml
文件。
- npm alias
npm alias 可以让你以自定义包名来安装npm包。假设你的项目中依赖了lodash
,但是其中有一个bug导致你的项目崩溃。而lodash
官方并没有去修复,而你只能fork其仓库自行修复,并发布了一个lodash-custom-fixed
包。那么,你可以使用lodash
作为alias来去安装lodash-custom-fixed
:
bash
pnpm add lodash@npm:lodash-custom-fixed
不需要更改代码,项目中 lodash
引用都被解析到了 lodash-custom-fixed
。
参考资料:pnpm.io/aliases
总结
在前端开发的日常工作中,依赖管理是一个关键的环节,但经常会遇到依赖安装报错的情况。本文主要梳理当前前端包依赖管理的现状,解析一些广泛传播的技术话题,以及为解决依赖报错问题提供的排查思路。无论你是新手还是资深前端开发者,都不可避免地会遇到这些问题。我们鼓励读者不仅要掌握临时解决问题的方法,更要巩固自己的基础知识,以便更好地独立排查和解决复杂的依赖安装问题。
我是「盐焗乳鸽还要香锅」,喜欢我的文章欢迎关注噢
- github 链接github.com/1360151219
- 掘金账号、知乎账号《盐焗乳鸽还要香锅》