chenxiaochun / blog

🖋️ChenXiaoChun's blog
179 stars 15 forks source link

深入理解 yarn 中的 nohoist 机制 #72

Open chenxiaochun opened 4 years ago

chenxiaochun commented 4 years ago

当前的问题是什么?

首先,让我们先快速回顾一下在一个独立项目中的hoist工作机制:

为了减少项目中的依赖包冗余,很多项目开发者会使用某种hoist机制将公共的依赖包提取出来,并将其展开集中到同一个目录下。

下图中左边的示例是一个常见的普通项目依赖树结构:

通过使用hoist机制,我们可以尽可能消除对A@1.0B@1.0两个包的重复依赖安装,同时还不会改变对B@2.0的依赖维护关系。而我们知道,大多数模块爬虫、loaders和打包器都是通过遍历项目中的node_modules来定位依赖包的。

那么在monorepo类型的项目中,引入了一种新的层级结构,它不再需要必须使用node_modules来建立模块间的依赖关系。在这种项目中,模块可以分散在项目中的多个位置:

yarn workspaces可以通过将公共依赖包提升到所有子项目的父目录的node_modules中,以实现共享这些依赖包的机制。而且如果这些依赖包彼此之间也有依赖关系时,使用这种优化方式的好处会更优。

无法找到模块!

并不是所有的模块爬虫都会遍历模块的软链接。因此,有可能我们在每个子项目中进行编译打包时,会出现模块无法找到的情况。

看上图右侧的项目结构,我们在实际打包时,可能会出现以下情况:

在这个monorepo项目中,依赖包可能存在于任意位置。它就需要模块爬虫能够遍历每一个node_modules目录才可能找到对应的模块。

为什么这些模块的位置不能固定下来呢?

事实上有很多开发都提出过此类问题的解决办法,例如:可以建立多个根目录、可以自定义模块映射、更智能的模块遍历模式等。但不管怎样,还是有一些原因使得问题难以解决:

  1. 并不是所有的第三方库都会适配 monorepo 环境。
  2. 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_modulespackages等路径。

进一步说明

让我们通过下面一个伪项目结构,来说明nohoist机制是如何阻止react-native模块被提升的。在下面的monorepo项目中主要有 A、B、C 三个子项目:

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

package.json文件们于monorepo的根目录下

...
"name": "monorepo",
"private": true,
"workspaces": {
  "packages": ["packages/*"],
  "nohoist": ["**/react-native", "**/react-native/**"]
}
...

nohoistworkspaces只能在private: true的项目中工作。

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

**/react-native,这会告诉yarn不要去提升react-native模块,不管它的位置在哪里。

**/react-native/**,这会告诉yarn不要去提升react-native的任何依赖包。

这两种模式结合起来就是告诉yarn不要去提升react-native以及它的所有依赖包。

如何关闭nohoist

只要在一个私有的package.json中添加了nohoist配置,yarn默认就会使用它。

如果想关闭nohoist,一般有三种方式:

  1. 可以在package.json中移除nohoist的相关配置。
  2. .yarnrc中添加workspaces-nohoist-experimental: false标识。
  3. 使用yarn config set workspaces-nohoist-experimental false命令来关闭它。

原文资料

dbl520 commented 3 years ago

"workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > gl-unified-permission > compression-webpack-plugin@5.0.2" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > compression-webpack-plugin@6.1.1" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > gl-unified-permission > sass-loader@8.0.2" has unmet peer dependency "webpack@^4.36.0 || ^5.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > mq-web > sass-loader@7.3.1" has unmet peer dependency "webpack@^3.0.0 || ^4.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > mq-web > html-webpack-plugin@3.2.0" 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 > script-ext-html-webpack-plugin@2.1.3" 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 > uglifyjs-webpack-plugin@2.2.0" has unmet peer dependency "webpack@^4.0.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > webpack-cli@4.5.0" has unmet peer dependency "webpack@4.x.x || 5.x.x". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > terser-webpack-plugin@4.2.3" 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 > eslint-import-resolver-webpack@0.13.0" has unmet peer dependency "webpack@>=1.11.0". warning "workspace-aggregator-1917a6ce-f14f-4b68-a050-0f6f7e8482d9 > pf-user-awareness-web > webpack-cli > @webpack-cli/configtest@1.0.1" has unmet peer dependency "webpack@4.x.x || 5.x.x".

image

安装多个项目提示的,有影响吗

chenxiaochun commented 3 years ago

看起来都是warning,应该没有影响

TLovers commented 2 years ago

能禁止某一个包里面的依赖吗? 好比有A 和B 两个文件夹 都依赖了vue, A依赖的是vue2 B依赖的是vue3 我只想禁止vue3的提升 而vue2正常提升 可以吗 我试了下好像并不行 不知道是不是我使用的姿势不正确

chenxiaochun commented 2 years ago

@Lovercz ,我也不太清楚。你如果有好的解决方案,也可以告诉我哈😀

taoliujun commented 2 years ago

A依赖B,B依赖C 当yarn add A的时候,C却未在 root node_modules下出现,是什么机制呀?

vvizden commented 1 year ago

同问

radiorz commented 1 year ago

Good job

SoldierAb commented 1 year ago

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

诸如此类的依赖问题可以展开。 已经在业务项目中弃用这种模式了

NameWjp commented 11 months ago

感觉真正能用在生产中的两组模式:pnpm(推荐的)or lerna + yarn (nohoist 模式)