一、前言
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/* 、docs 、 play 、 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 文件可以更改成自定義 name 和 private : true。
在根目錄下創(chuàng)建 pnpm-workspace.yaml 文件,聲明想要在全局使用的模塊。
packages:
- 'packages/*'
- play
- docs
在根目錄下安裝 vue 和 typesrcipt ,在根目錄下需要用 -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.json 的 dependencies 或 devDependencies 中,但在傳統(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-project 的 package.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'
原因:
-
require('webpack')只能在根目錄的node_modules/下查找模塊。 - 由于 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)
愿諸君慢慢變好,一起加油。