手写 Element Plus:用Monorepo架构搭建Element Plus

一、前言

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/*docsplayinternal/*模块。

  • 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 文件可以更改成自定义 nameprivate : true。

在根目录下创建 pnpm-workspace.yaml 文件,声明想要在全局使用的模块。

packages:
 - 'packages/*'
 -  play
 -  docs

在根目录下安装 vuetypesrcipt ,在根目录下需要用 -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.jsondependenciesdevDependencies 中,但在传统的 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-projectpackage.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'

原因:

  1. require('webpack') 只能在根目录的 node_modules/ 下查找模块。
  2. 由于 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目录

新建 componentshooksutilsthemechalk 文件,在相应的目录下完成初始化,执行 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 部分目录结构

愿诸君慢慢变好,一起加油。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容