一、前言
Element Plus 组件库中拥有非常强大的功能,而这些功能又是极其庞大的,显然这个时候需要工程化管理。接下来我将向你介绍 Element Plus 组件库的目录项目结构,开发环境是如何搭建和介绍 monorepo 在架构组件库是如何使用的。
现在前端很多项目都使用 monorepo
来管理代码,Vue3 和 Element Plus 是比较有代表性的。所以掌握这种管理项目代码的方式是很有必要的。
二、从Element Plus源码项目入门理解pnpm的monorepo
Element Plus 中很多模块之间可以直接单独使用,不需要运行某个模块就能使用,这个好处就是由 monorepo
带来的,使用它能够大大的降低项目模块之间的耦合度。
一)Element Plus中的monorepo
Element Plus 中主要使用的是 pnpm 的 monorepo ,只需要在根目录下新建 pnpm-workspace.yaml 文件,并声明想要在全局使用的工作区就可以了。
Element Plus 在 pnpm-workspace.yaml
文件中声明了 packages/*
、docs
、 play
、 internal/*
模块。
-
packages/*
:核心组件功能模块。包括packages
目录下的components (组件源码)
、constants (全局常量)
、directives (组件自定义指令)
、hooks (全局hooks)
、local (组件全局语言)
、test-utils (测试工具函数)
、theme-chalk (组件全局样式)
、utils (全局工具函数)
。packages
目录下所有的文件夹对应的都是一个独立的模块。 -
docs
: Element Plus 的官方文档模块,它由vitepress
构建的。 -
play
: Element Plus 组件的运行文件,创建的组件在这个文件下运行,由vite --template vue-ts
构建的单独项目。 -
internal/*
:组件的内置文件,组件的eslint
的配置文件和dist
打包文件目录。
二)如何从零构建一个Element Plus 项目目录
1.初始化项目
如果安装了 pnpm
可以执行以下命令,没有则需要安装 pnpm
。
pnpm init
初始化后 package.json
文件可以更改成自定义 name
和 private : true。
在根目录下创建 pnpm-workspace.yaml
文件,声明想要在全局使用的模块。
packages:
- 'packages/*'
- play
- docs
在根目录下安装 vue
和 typesrcipt
,在根目录下需要用 -w
表示是在根目录下安装依赖。否则提示以下错误。
pnpm i vue typescript -w
在根目录下初始化 typescript
类型声明,执行 pnpm tsc --init
命令,初始化后进行以下基础配置。
{
"compilerOptions": {
"module": "ESNext", // 打包模块类型 ESNext
"declaration": false, // 默认不要声明文件
"noImplicitAny": true, // 支持类型不标注可以默认any
"removeComments": true, // 删除注释
"moduleResolution": "node", // 安装node 模块来解析
"esModuleInterop": true, // 支持es6, commonjs 模块
"jsx": "preserve", // jsx 不转
// "noLib": true, // 不处理类库
"target": "ES6", // 遵循ES6
"sourceMap": true, //
"lib": ["ESNext", "DOM"], // 编译时用的库
"allowSyntheticDefaultImports": true, // 允许没有导出的模块中导出
"experimentalDecorators": true, // 装饰语法
"forceConsistentCasingInFileNames": true, // 强制区分大小写
"resolveJsonModule": true, // 解析 json 模块
"strict": true, // 是否启用严格模式
"skipLibCheck": true // 跳过类库检测
},
"exclude": [
// 排除掉哪些类库
"node_modules",
]
}
配置 .npmrc
文件,使安装在根目录下的依赖全部提升到根级别上,防止出现依赖树混乱和其他潜在的版本问题。以下配置就能够解决这个问题,这个问题也被称为幽灵依赖。
shamefully-hoist=true
为什么会出现幽灵依赖,解决它的过程是什么?
为什么出现幽灵依赖:
pnpm 使用的是符号链接(symlinks)来管理依赖。它将所有包集中存储在一个全局存储区中(pnpm store
) , 默认情况下,pnpm 遵守严格的依赖解析规则,只将直接依赖(declared dependencies)安装到 node_modules
下,而不会自动提升子依赖(transitive dependencies)。这种行为减少了重复安装和文件冲突,但可能导致某些工具或代码找不到依赖 。
在 npm 或 yarn 中,某些依赖可能在项目中隐式被使用(比如直接从子依赖中引用),这被称为“幽灵依赖”(phantom dependencies)。这些依赖实际上并没有声明在 package.json
的 dependencies
或 devDependencies
中,但在传统的 node_modules
平铺结构下,它们可以被直接引用。
如果项目中某些代码或工具依赖于幽灵依赖,但这些依赖并没有显式声明在 package.json
中,pnpm 默认无法解决这些模块,导致运行时报错(如 MODULE_NOT_FOUND
)。
shamefully-hoist=true
的设置告诉 pnpm 将所有安装的依赖提升到项目的根 node_modules
下,即使它们是子依赖。这会模拟 npm 或 yarn 的平铺依赖树行为,从而解决某些代码找不到模块的问题。
解决过程:
项目目录结构
project/
├── package.json
├── .npmrc
├── src/
│ └── index.js
└── node_modules/
package.json
配置
{
"name": "example-project",
"version": "1.0.0",
"dependencies": {
"react-scripts": "^5.0.0"
}
}
在这里,react-scripts
是一个典型的依赖,它依赖了 webpack
等工具,但 webpack 并未在 example-project
的 package.json
中显式声明。
src/index.js
中的代码
import webpack from 'webpack'; // 使用 react-scripts 内部的 webpack
console.log('Loaded webpack version:', webpack.version);
使用 pnpm 安装依赖(未启用 shamefully-hoist
)
pnpm install
此时,pnpm 会严格遵守模块的依赖关系,并将 webpack
安装到 node_modules/react-scripts/node_modules/webpack
下,而不会将它提升到 node_modules/
的根目录。
目录结构如下:
project/
├── node_modules/
│ ├── react-scripts/
│ │ └── node_modules/
│ │ └── webpack/
运行代码:
node src/index.js
结果:
Error: Cannot find module 'webpack'
原因:
-
require('webpack')
只能在根目录的node_modules/
下查找模块。 - 由于 pnpm 没有将
webpack
提升到根目录,因此代码无法找到webpack
。
在 .npmrc
文件中添加以下内容:
shamefully-hoist=true
运行以下命令重新安装:
pnpm install
此时,pnpm 会将所有子依赖提升到 node_modules
的根目录,目录结构如下:
project/
├── node_modules/
│ ├── react-scripts/
│ ├── webpack/
运行代码:
node src/index.js
结果:
Loaded webpack version: 5.75.0
2.搭建Element Plus目录结构
1) 配置packages目录
新建 components
、hooks
、utils
、themechalk
文件,在相应的目录下完成初始化,执行 pnpm init
命令。
依次注册上面四个文件夹,并更改 name 的值,也可以不更改。初始化新项目后,在根目录下执行注册已初始化的包。这样做的好处就是可以在全局中直接通过引入 name
值来获取暴露出的模块,可以不必按照绝对路径进行导入。
pnpm i @test/components @test/utils @test/themechalk @test/hooks -w
在成功注册后,根目录中 package.json
的开发依赖有以下模块,可以将 "workspace:*"
更改 "workspace:*"
代表全部版本。
2)配置play目录
这个目录主要是用来测试编写的组件库使用情况,好比直接在项目里用 Element Plus 组件,检验组件的功能。在根目录下执行以下命令。
pnpm create vite@latest play --template vue-ts
3)配置docs目录
编写组件文档主要是使用 vitepress
,在 docs
目录下初始化 vitepress
,具体的使用细节可以移步到 vitepress
官方文档 https://vitepress.dev/
3.Element plus 组件中 TypeScript 的全局配置
前面提到的 tsconfig.json
初始化不会区分生产环境的核心模块和一些其他模块,所以需要在编译时对模块进行划分。这样可以把庞大的组件库类型分成多个小模块,提高了编译效率和降低耦合度。
配置公共 typescript
配置项 tsconfig.base.json
:
{
"compilerOptions": {
"outDir": "dist",
"target": "es2018",
"module": "esnext",
"baseUrl": ".",
"sourceMap": false,
"moduleResolution": "node",
"allowJs": false,
"strict": true,
"noUnusedLocals": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"removeComments": false,
"rootDir": ".",
"types": [],
"paths": {
"@fz-mini/*": ["packages/*"]
}
}
}
组件包部分配置项 tsconfig.web.json:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"jsx": "preserve",
"lib": ["ES2018", "DOM", "DOM.Iterable"],
"types": [],
"skipLibCheck": true
},
"include": ["packages"],
"exclude": [
"node_modules",
"**/*.md"
]
}
组件 play
部分配置项 tsconfig.play.json
文件:
{
"extends": "./tsconfig.web.json",
"compilerOptions": {
"composite": true,
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"allowJs": true
},
"include": [
"packages",
// playground
"play/main.ts",
"play/env.d.ts",
"play/src/**/*"
]
}
最后在 tsconfig.json
引入这三个不同包的 typescript
配置, tsconfig.json
文件有一个顶级属性 "references"
,它支持将 TypeScript
的程序项目分割成更小的组成部分。
{
"files": [],
"references": [
{ "path": "./tsconfig.web.json" }, // 组件包部分
{ "path": "./tsconfig.play.json" }, // 组件 play 部分
{ "path": "./tsconfig.vitest.json" } // 组件测试部分
]
}
每个引用的 path
属性可以指向包含 tsconfig.json
文件的目录,也可以指向配置文件本身。经过上面的设置,就等于是在 typescript
层又把我们的组件库项目分成了三个部分。每个配置文件又有 tsconfig.base.json
相同配置,通过 extends 引入可以减少大量的重复配置。
三、总结
- 理解 monorepo 的作用和用途
- 初始化一个 Element Plus 源码框架
- 配置 Element Plus 部分目录结构
愿诸君慢慢变好,一起加油。