手寫 Element Plus:用Monorepo架構(gòu)搭建Element Plus

一、前言

Element Plus 組件庫(kù)中擁有非常強(qiáng)大的功能,而這些功能又是極其龐大的,顯然這個(gè)時(shí)候需要工程化管理。接下來我將向你介紹 Element Plus 組件庫(kù)的目錄項(xiàng)目結(jié)構(gòu),開發(fā)環(huán)境是如何搭建和介紹 monorepo 在架構(gòu)組件庫(kù)是如何使用的。

現(xiàn)在前端很多項(xiàng)目都使用 monorepo 來管理代碼,Vue3 和 Element Plus 是比較有代表性的。所以掌握這種管理項(xiàng)目代碼的方式是很有必要的。

二、從Element Plus源碼項(xiàng)目入門理解pnpm的monorepo

Element Plus 中很多模塊之間可以直接單獨(dú)使用,不需要運(yùn)行某個(gè)模塊就能使用,這個(gè)好處就是由 monorepo 帶來的,使用它能夠大大的降低項(xiàng)目模塊之間的耦合度。

一)Element Plus中的monorepo

Element Plus 中主要使用的是 pnpm 的 monorepo ,只需要在根目錄下新建 pnpm-workspace.yaml 文件,并聲明想要在全局使用的工作區(qū)就可以了。

Element Plus 在 pnpm-workspace.yaml 文件中聲明了 packages/* 、docsplay 、 internal/*模塊。

  • packages/* :核心組件功能模塊。包括 packages 目錄下的 components (組件源碼)、constants (全局常量)、directives (組件自定義指令)、 hooks (全局hooks) 、local (組件全局語言)、test-utils (測(cè)試工具函數(shù))、 theme-chalk (組件全局樣式)、 utils (全局工具函數(shù))packages 目錄下所有的文件夾對(duì)應(yīng)的都是一個(gè)獨(dú)立的模塊。
  • docs : Element Plus 的官方文檔模塊,它由 vitepress 構(gòu)建的。
  • play : Element Plus 組件的運(yùn)行文件,創(chuàng)建的組件在這個(gè)文件下運(yùn)行,由 vite --template vue-ts 構(gòu)建的單獨(dú)項(xiàng)目。
  • internal/* :組件的內(nèi)置文件,組件的 eslint 的配置文件和 dist 打包文件目錄。

二)如何從零構(gòu)建一個(gè)Element Plus 項(xiàng)目目錄

1.初始化項(xiàng)目

如果安裝了 pnpm 可以執(zhí)行以下命令,沒有則需要安裝 pnpm 。

pnpm init

初始化后 package.json 文件可以更改成自定義 nameprivate : true。

在根目錄下創(chuàng)建 pnpm-workspace.yaml 文件,聲明想要在全局使用的模塊。

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

在根目錄下安裝 vuetypesrcipt ,在根目錄下需要用 -w 表示是在根目錄下安裝依賴。否則提示以下錯(cuò)誤。

pnpm i vue typescript -w

在根目錄下初始化 typescript 類型聲明,執(zhí)行 pnpm tsc --init 命令,初始化后進(jìn)行以下基礎(chǔ)配置。

{
  "compilerOptions": {
    "module": "ESNext", // 打包模塊類型 ESNext
    "declaration": false, // 默認(rèn)不要聲明文件
    "noImplicitAny": true, // 支持類型不標(biāo)注可以默認(rèn)any
    "removeComments": true, // 刪除注釋
    "moduleResolution": "node", // 安裝node 模塊來解析
    "esModuleInterop": true, // 支持es6, commonjs 模塊
    "jsx": "preserve", // jsx 不轉(zhuǎn)
    // "noLib": true, // 不處理類庫(kù)
    "target": "ES6", // 遵循ES6
    "sourceMap": true, //
    "lib": ["ESNext", "DOM"], // 編譯時(shí)用的庫(kù)
    "allowSyntheticDefaultImports": true, // 允許沒有導(dǎo)出的模塊中導(dǎo)出
    "experimentalDecorators": true, // 裝飾語法
    "forceConsistentCasingInFileNames": true, // 強(qiáng)制區(qū)分大小寫
    "resolveJsonModule": true, // 解析 json 模塊
    "strict": true, // 是否啟用嚴(yán)格模式
    "skipLibCheck": true // 跳過類庫(kù)檢測(cè)
  },
  "exclude": [
    // 排除掉哪些類庫(kù)
    "node_modules",
  ]
}

配置 .npmrc 文件,使安裝在根目錄下的依賴全部提升到根級(jí)別上,防止出現(xiàn)依賴樹混亂和其他潛在的版本問題。以下配置就能夠解決這個(gè)問題,這個(gè)問題也被稱為幽靈依賴。

shamefully-hoist=true

為什么會(huì)出現(xiàn)幽靈依賴,解決它的過程是什么?

為什么出現(xiàn)幽靈依賴:

pnpm 使用的是符號(hào)鏈接(symlinks)來管理依賴。它將所有包集中存儲(chǔ)在一個(gè)全局存儲(chǔ)區(qū)中(pnpm store) , 默認(rèn)情況下,pnpm 遵守嚴(yán)格的依賴解析規(guī)則,只將直接依賴(declared dependencies)安裝到 node_modules 下,而不會(huì)自動(dòng)提升子依賴(transitive dependencies)。這種行為減少了重復(fù)安裝和文件沖突,但可能導(dǎo)致某些工具或代碼找不到依賴 。

在 npm 或 yarn 中,某些依賴可能在項(xiàng)目中隱式被使用(比如直接從子依賴中引用),這被稱為“幽靈依賴”(phantom dependencies)。這些依賴實(shí)際上并沒有聲明在 package.jsondependenciesdevDependencies 中,但在傳統(tǒng)的 node_modules 平鋪結(jié)構(gòu)下,它們可以被直接引用。

如果項(xiàng)目中某些代碼或工具依賴于幽靈依賴,但這些依賴并沒有顯式聲明在 package.json 中,pnpm 默認(rèn)無法解決這些模塊,導(dǎo)致運(yùn)行時(shí)報(bào)錯(cuò)(如 MODULE_NOT_FOUND)。

shamefully-hoist=true 的設(shè)置告訴 pnpm 將所有安裝的依賴提升到項(xiàng)目的根 node_modules ,即使它們是子依賴。這會(huì)模擬 npm 或 yarn 的平鋪依賴樹行為,從而解決某些代碼找不到模塊的問題。

解決過程:

項(xiàng)目目錄結(jié)構(gòu)


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 是一個(gè)典型的依賴,它依賴了 webpack 等工具,但 webpack 并未在 example-projectpackage.json 中顯式聲明。

src/index.js 中的代碼


import webpack from 'webpack'; // 使用 react-scripts 內(nèi)部的 webpack

console.log('Loaded webpack version:', webpack.version);

使用 pnpm 安裝依賴(未啟用 shamefully-hoist

pnpm install

此時(shí),pnpm 會(huì)嚴(yán)格遵守模塊的依賴關(guān)系,并將 webpack 安裝到 node_modules/react-scripts/node_modules/webpack 下,而不會(huì)將它提升到 node_modules/ 的根目錄。

目錄結(jié)構(gòu)如下:

project/
├── node_modules/
│   ├── react-scripts/
│   │   └── node_modules/
│   │       └── webpack/

運(yùn)行代碼:

node src/index.js

結(jié)果:

Error: Cannot find module 'webpack'

原因:

  1. require('webpack') 只能在根目錄的 node_modules/ 下查找模塊。
  2. 由于 pnpm 沒有將 webpack 提升到根目錄,因此代碼無法找到 webpack。

.npmrc 文件中添加以下內(nèi)容:

shamefully-hoist=true

運(yùn)行以下命令重新安裝:


pnpm install

此時(shí),pnpm 會(huì)將所有子依賴提升到 node_modules 的根目錄,目錄結(jié)構(gòu)如下:


project/
├── node_modules/
│   ├── react-scripts/
│   ├── webpack/

運(yùn)行代碼:

node src/index.js

結(jié)果:

Loaded webpack version: 5.75.0

2.搭建Element Plus目錄結(jié)構(gòu)

1) 配置packages目錄

新建 components 、hooks 、utils 、themechalk 文件,在相應(yīng)的目錄下完成初始化,執(zhí)行 pnpm init命令。

依次注冊(cè)上面四個(gè)文件夾,并更改 name 的值,也可以不更改。初始化新項(xiàng)目后,在根目錄下執(zhí)行注冊(cè)已初始化的包。這樣做的好處就是可以在全局中直接通過引入 name 值來獲取暴露出的模塊,可以不必按照絕對(duì)路徑進(jìn)行導(dǎo)入

pnpm i @test/components @test/utils @test/themechalk @test/hooks -w

在成功注冊(cè)后,根目錄中 package.json 的開發(fā)依賴有以下模塊,可以將 "workspace:*" 更改 "workspace:*" 代表全部版本。

2)配置play目錄

這個(gè)目錄主要是用來測(cè)試編寫的組件庫(kù)使用情況,好比直接在項(xiàng)目里用 Element Plus 組件,檢驗(yàn)組件的功能。在根目錄下執(zhí)行以下命令。

pnpm create vite@latest play --template vue-ts

3)配置docs目錄

編寫組件文檔主要是使用 vitepress ,在 docs 目錄下初始化 vitepress ,具體的使用細(xì)節(jié)可以移步到 vitepress 官方文檔 https://vitepress.dev/

3.Element plus 組件中 TypeScript 的全局配置

前面提到的 tsconfig.json 初始化不會(huì)區(qū)分生產(chǎn)環(huán)境的核心模塊和一些其他模塊,所以需要在編譯時(shí)對(duì)模塊進(jìn)行劃分。這樣可以把龐大的組件庫(kù)類型分成多個(gè)小模塊,提高了編譯效率和降低耦合度。

配置公共 typescript 配置項(xiàng) 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/*"]
    }
  }
}

組件包部分配置項(xiàng) 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 部分配置項(xiàng) 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 引入這三個(gè)不同包的 typescript 配置, tsconfig.json 文件有一個(gè)頂級(jí)屬性 "references",它支持將 TypeScript 的程序項(xiàng)目分割成更小的組成部分。

{
  "files": [],
  "references": [
    { "path": "./tsconfig.web.json" }, // 組件包部分
    { "path": "./tsconfig.play.json" }, // 組件 play 部分
    { "path": "./tsconfig.vitest.json" } // 組件測(cè)試部分
  ]
}

每個(gè)引用的 path 屬性可以指向包含 tsconfig.json 文件的目錄,也可以指向配置文件本身。經(jīng)過上面的設(shè)置,就等于是在 typescript 層又把我們的組件庫(kù)項(xiàng)目分成了三個(gè)部分。每個(gè)配置文件又有 tsconfig.base.json 相同配置,通過 extends 引入可以減少大量的重復(fù)配置。

三、總結(jié)

  • 理解 monorepo 的作用和用途
  • 初始化一個(gè) Element Plus 源碼框架
  • 配置 Element Plus 部分目錄結(jié)構(gòu)

愿諸君慢慢變好,一起加油。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容