构建
一、认识
基于 PNPM
构建 Monorepo
多项目, 目录结构如下:
pnpm-monorepo/
├── apps/
│ └── reactA/
│ └── src
│ └── package.json
│ └── tsconfig.json
│ └── reactB/
│ └── src
│ └── package.json
│ └── tsconfig.json
├── libs/
│ ├── utils/
│ │ └── src
│ │ └── package.json
│ │ └── tsconfig.json
│ └── components/
│ └── src
│ └── package.json
│ └── tsconfig.json
├── pnpm-workspace.yaml
└── package.json
二、创建目录文件
2.1 创建根目录
1. 进入根目录,执行 pnpm init
, 生成 package.json
, 根目录的 package.json
可以指定工作区以及添加共享的开发依赖项。
2. 手动创建 pnpm-workspace.yaml
, 这个文件用于定义 pnpm
工作区的结构,指定哪些目录下的包是工作区的一部分。, 根据你的目录结构,pnpm-workspace.yaml
应该如下:
packages:
- "apps/*"
- "libs/*"
2.2 创建 apps/appA
1. 进入 appA
目录, 执行 pnpm init
, 生成 package.json
, package.json
需要指定它们的依赖关系以及对工作区包的引用。
{
"name": "appa",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"utils": "workspace:*",
"components": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"typescript": "^5.2.2",
"vite": "^5.2.0"
}
}
2. 手动创建 tsconfig.json
: 每个子项目应该有自己的 tsconfig.json
文件。确保 TypeScript
配置正确以便它能处理项目之间的引用。
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"baseUrl": "./src",
"paths": {
"*": ["*", "src/*"],
// "utils": ["../../libs/utils/src"],
// "components": ["../../libs/components/src"]
},
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
2.3 创建 apps/appB
同 apps/appA
2.4 创建 libs/utils
1. 进入 utils
目录, 执行 pnpm init
, 生成 package.json
, package.json
需要指定它们的依赖关系以及对工作区包的引用。
{
"name": "utils",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
2. 手动创建 tsconfig.json
: 每个子项目应该有自己的 tsconfig.json
文件。确保 TypeScript
配置正确以便它能处理项目之间的引用。
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
2.5 创建 libs/components
同 libs/utils
三、安装项目依赖
3.1 安装所有依赖
进入项目根目录,执行 pnpm install
, 安装根目录和工作区中所有的依赖
pnpm install
在 Monorepo
构建中,pnpm
使用工作区的机制来管理和链接项目中的多个包。工作区配置(通过 pnpm-workspace.yaml
或 package.json
的 workspaces
字段指定)使得 pnpm
能够识别并处理 Monorepo
中的所有包。当你在根目录下执行 pnpm install
时,pnpm
会进行以下步骤:
-
识别并解析所有工作区:
pnpm
会读取根目录下的pnpm-workspace.yaml
文件或package.json
中的workspaces
配置,识别出所有的工作区包(即,项目中的各个子包)。pnpm
会根据工作区配置解析所有子包,并构建一个包含所有工作区包依赖的图。 -
依赖解析:
pnpm
会解析根目录和工作区包中的package.json
文件,确定所有的依赖项。包括:外部依赖 即从公共注册表(如npm
或yarn
)下载的依赖项。和 工作区依赖 ,即工作区包之间的相互依赖(如reactA
依赖utils
和components
) -
下载外部依赖:
pnpm
会从注册表下载并安装所有的外部依赖到根目录下的node_modules/.pnpm
文件夹,并将其链接到各个工作区包的node_modules
中。 -
链接工作区依赖
: 对于工作区包,pnpm
使用符号链接将这些包链接到实际的工作区路径中,而不是复制整个包到每个工作区的node_modules
中。这节省了磁盘空间并提高了依赖管理的效率。 -
生成
pnpm-lock.yaml
:pnpm
生成并更新pnpm-lock.yaml
文件,记录所有依赖项的精确版本和安装信息,以确保一致的安装结果。
为什么要安装所有依赖
: 因为要确保所有工作区包的依赖项都被正确安装和链接,以保持一致的开发环境。而且可以保证在 Monorepo
中,多个项目可能需要共享或互相依赖包。安装所有依赖项确保开发时能够正确地解析和使用这些依赖。通过一次性安装所有依赖,pnpm
可以高效地处理工作区包之间的依赖关系,而不是在每次运行时手动处理这些依赖。
3.2 安装根目录依赖
通过 pnpm add [package] -w -S/-D
来根目录
3.3 安装工作区依赖
通过 pnpm --filter 工作区名称(package.json 中的 name)add [package] -S/-D
来安装到指定的工作区
四、启动、构建工作区
通过 pnpm --filter 工作区名称(package.json 中的 name) 命令
或者直接进入到指定工作区运行命令即可
五、问题汇总
5.1 解决 NPM 中的幽灵依赖问题
NPM
扁平结构: 在 NPM
的扁平结构中,node_modules
目录尽可能地将依赖项安装在根目录中,以减少嵌套层级。这种扁平化的安装方式旨在避免过深的目录结构和长路径问题。PNPM
是非扁平结构,没有幽灵依赖的问题,所以以前不在 项目的 package.json
中声明但是却在项目中通过 import
引入访问的依赖会导致编译报错。
解决方案: 在项目目录中,声明这些幽灵依赖。
5.2 解决依赖版本冲突,确保依赖项的一致性
在 PNPM
的 Monorepo
项目中,当子项目中的依赖版本不一致时,PNPM
会将不同版本的包分别安装在每个项目中。由于 node_modules
中使用了符号链接,不同版本的包可能在子项目中被引入,从而导致依赖解析和模块加载的问题。
例如:假设 app-a
和 app-b
使用不同版本的 lodash
,由于 PNPM
使用了符号链接,这些版本的 lodash
会分别被安装在 app-a
和 app-b
的 node_modules
中。虽然这些版本的 lodash
通过符号链接指向相同的全局存储位置,但在 Webpack
处理这些符号链接时,可能会出现问题。
Webpack
使用相应的插件进行代码压缩和混淆。假设 app-a
和 app-b
中的 lodash
被混淆为标识符 a
,由于 lodash
版本不同,这个标识符 a
可能指向不同的实现。即使它们在代码中使用相同的标识符,实际引用的 lodash
版本却不同,导致功能不一致或运行时错误。
解决方案如下:
-
解决模块版本冲突,确保版本一致:
-
overrides
:overrides
是在pnpm
中用于解决版本冲突和确保依赖一致性的一种功能。你可以在根目录的package.json
中使用overrides
字段来指定特定依赖的版本。 -
.pnpmfile.cjs
是pnpm
提供的自定义配置文件,可以用来实现更复杂的依赖处理逻辑,如动态覆盖版本、调整依赖结构等。 -
基于
resolve.symlinks
避免符号链接路径的问题: 在Webpack
配置中,将resolve.symlinks
设置为false
可以避免因符号链接路径导致的问题。这将确保Webpack
不会因为符号链接指向的不同版本或路径而引发依赖解析和功能不一致的问题。