blog
blog copied to clipboard
深入理解 yarn 中的 nohoist 机制
当前的问题是什么?
首先,让我们先快速回顾一下在一个独立项目中的hoist工作机制:
为了减少项目中的依赖包冗余,很多项目开发者会使用某种hoist机制将公共的依赖包提取出来,并将其展开集中到同一个目录下。
下图中左边的示例是一个常见的普通项目依赖树结构:

通过使用hoist机制,我们可以尽可能消除对[email protected]和[email protected]两个包的重复依赖安装,同时还不会改变对[email protected]的依赖维护关系。而我们知道,大多数模块爬虫、loaders和打包器都是通过遍历项目中的node_modules来定位依赖包的。
那么在monorepo类型的项目中,引入了一种新的层级结构,它不再需要必须使用node_modules来建立模块间的依赖关系。在这种项目中,模块可以分散在项目中的多个位置:

yarn workspaces可以通过将公共依赖包提升到所有子项目的父目录的node_modules中,以实现共享这些依赖包的机制。而且如果这些依赖包彼此之间也有依赖关系时,使用这种优化方式的好处会更优。
无法找到模块!
并不是所有的模块爬虫都会遍历模块的软链接。因此,有可能我们在每个子项目中进行编译打包时,会出现模块无法找到的情况。
看上图右侧的项目结构,我们在实际打包时,可能会出现以下情况:
- 在
monorepo根目录无法找到[email protected]依赖包。因为[email protected]存在于package-1的node_modules下,而它是以软链接的形式存在于monorepo根目录的node_modules下。 - 从
package-1无法找到[email protected]模块。因为[email protected]实际存在于monorepo根目录中的node_modules下。
在这个monorepo项目中,依赖包可能存在于任意位置。它就需要模块爬虫能够遍历每一个node_modules目录才可能找到对应的模块。
为什么这些模块的位置不能固定下来呢?
事实上有很多开发都提出过此类问题的解决办法,例如:可以建立多个根目录、可以自定义模块映射、更智能的模块遍历模式等。但不管怎样,还是有一些原因使得问题难以解决:
- 并不是所有的第三方库都会适配 monorepo 环境。
- js 有大量的第三方库。而这些大量的第三方组成的工具链也就成了最”薄弱“的一环。因为只要有一个模块没有进行适配,就可能导致整个工具链都没法使用了
什么是nohoist?
那有没有一种简单的机制能够使这些不兼容的库可以正常在 monorepo 中工作呢?
那就是yarn提供的nohoist机制,这种机制在lerna中也有相关演示。
nohoist机制可以使workspace去自定义处理那些不兼容hoist模式的第三方库。只要你进行了配置,它就不会把这些再模块提升到根目录。它们还是被放在原来的子项目中,就像运行在一个标准的、没有workspace的工程一样。
一些注意事项
nohoist虽然很有用,但是它还是有一些缺点。最明显的就是指定的nohoist模块依然会被重复安装在多个目录中,这也就完全违背了上面我们提到的多种好处。
建议在指定nohoist的范围时,尽量越小、越精确越好。
如何使用它?
nohoist的使用方法非常简单。在package.json中进行定义相应的规则即可。yarn是从1.4.2版本开始提供此功能的。
下面是它的类型定义:
export type WorkspacesConfig = {
packages?: Array<string>,
nohoist?: Array<string>,
};
使用示例:
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/react-native", "**/react-native/**"]
}
nohoist支持使用 glob 语法匹配模块路径依赖目录。模块目录是一个虚拟目录,不用带上node_modules和packages等路径。
进一步说明
让我们通过下面一个伪项目结构,来说明nohoist机制是如何阻止react-native模块被提升的。在下面的monorepo项目中主要有 A、B、C 三个子项目:

在执行yarn install命令以前,它们的文件目录结构如下:

package.json文件们于monorepo的根目录下
...
"name": "monorepo",
"private": true,
"workspaces": {
"packages": ["packages/*"],
"nohoist": ["**/react-native", "**/react-native/**"]
}
...
- 正确设置
private属性
nohoist和workspaces只能在private: true的项目中工作。
- glob 模式匹配
在yarn内部,它会基于每个模块的原始依赖包(在被提升之前的结构)构建一个虚拟的模块路径。如果nohoist匹配到了其中的路径,它就会被离其最近的子项目所代替。
- 模块路径
在 A 项目中:
monorepo/A
monorepo/A/react-native
monorepo/A/react-native/metro
monorepo/A/Y
在 B 项目中:
monorepo/B
monorepo/B/X
monorepo/B/X/react-native
monorepo/B/X/react-native/metro
在 C 项目中:
monorepo/C
monorepo/C/Y
nohoist模式
**/react-native,这会告诉yarn不要去提升react-native模块,不管它的位置在哪里。
**/react-native/**,这会告诉yarn不要去提升react-native的任何依赖包。
这两种模式结合起来就是告诉yarn不要去提升react-native以及它的所有依赖包。
如何关闭nohoist?
只要在一个私有的package.json中添加了nohoist配置,yarn默认就会使用它。
如果想关闭nohoist,一般有三种方式:
- 可以在
package.json中移除nohoist的相关配置。 - 在
.yarnrc中添加workspaces-nohoist-experimental: false标识。 - 使用
yarn config set workspaces-nohoist-experimental false命令来关闭它。
原文资料
- https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
"workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > gl-unified-permission > [email protected]" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > [email protected]" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > gl-unified-permission > [email protected]" has unmet peer dependency "webpack@^4.36.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > mq-web > [email protected]" has unmet peer dependency "webpack@^3.0.0 || ^4.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > mq-web > [email protected]" has unmet peer dependency "webpack@^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > mq-web > [email protected]" has unmet peer dependency "webpack@^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > mq-web > [email protected]" has unmet peer dependency "webpack@^4.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > [email protected]" has unmet peer dependency "[email protected] || 5.x.x". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > [email protected]" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > greeting-card-ui > @vue/eslint-config-airbnb > [email protected]" has unmet peer dependency "webpack@>=1.11.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > webpack-cli > @webpack-cli/[email protected]" has unmet peer dependency "[email protected] || 5.x.x".

安装多个项目提示的,有影响吗
看起来都是warning,应该没有影响
能禁止某一个包里面的依赖吗? 好比有A 和B 两个文件夹 都依赖了vue, A依赖的是vue2 B依赖的是vue3 我只想禁止vue3的提升 而vue2正常提升 可以吗 我试了下好像并不行 不知道是不是我使用的姿势不正确
@Lovercz ,我也不太清楚。你如果有好的解决方案,也可以告诉我哈😀
A依赖B,B依赖C 当yarn add A的时候,C却未在 root node_modules下出现,是什么机制呀?
同问
Good job
A依赖B,B依赖C 当yarn add A的时候,C却未在 root node_modules下出现,是什么机制呀?
确实,node_modules 依赖树是复杂、多层的。类似 vue-cli、create-react-app、react 等这类依赖较为确定的项目适合去做Monorepo。 而庞大的业务项目集中,不同项目的依赖做组成的依赖树在workpaces的组织下不见得是稳定的。
比如某个package-A, 其package.json如下:
{
"devDependencies": {
"vite-plugin-svg-icons": "^2.0.1",
"vite": "^2.8.0"
}
}
而vite-plugin-svg-icons的peerDependencies 中指定 vite 依赖
{
"peerDependencies": {
"vite": ">=2.0.0"
}
}
此时包含其他package-B 、 package-C, 它们有共同依赖:
{
"devDependencies": {
"vite": "^4.8.0"
}
}
此时通过workspace 安装后
- <rootDir>
- packages
|- package-A
|_ node_modules
|_ vite ^2
|- package-B
|_ package-C
- node_modules
|- vite ^4
|_ vite-plugin-svg-icons ^2
这时导致 vite-plugin-svg-icons 引用的vite是<rootDir>/node_modules/vite^4
而不是 <rootDir>/packages/package-A/node_modules/vite
诸如此类的依赖问题可以展开。 已经在业务项目中弃用这种模式了
感觉真正能用在生产中的两组模式:pnpm(推荐的)or lerna + yarn (nohoist 模式)