從零開始搭建一個屬於自己的組件庫!
爲什麼要搭建組件庫?
雖然業界已經有很多成熟優秀的 ui 庫可以供我們使用,也爲我們解決了很多問題。但是基礎的東西總是不能滿足所有業務場景,更多時候我們需要擴展功能來滿足業務的需求,好比 table
需要自定列這樣的~相信這也是很多小夥伴開發時候的場景。
- 跨項目複用。很多時候爲了方便,只是基於當前項目對組件進行二次封裝(反正我是這樣乾的哈哈),然後做其他項目遇到同樣場景時,要麼 copy(經常忘記之前封裝在哪個項目裏了🌚)、要麼重新幹一個... 總是缺少一個統籌的地方,複用很不方便。
- 組件使用文檔。文檔產出對於一線開發來說可能相對比較欠缺,因爲大家都忙於擼業務,文檔這種奢侈品能省一點是一點。這樣導致一個問題就是自己封裝的組件別人不會用、不知道在哪裏用,甚至不知道有這麼個東西。
- 跨團隊共建發展。大多 B 端系統都是以
element
、antd
等 ui 框架爲主,基於各種業務場景,基本都會有自己團隊的二次封裝。其實類似的功能擴展肯定會有的,如果有組件庫把組件都集中起來,就能減少很多重複造輪子的勞動力了!
筆者之前就經常有這樣的痛點,在某個項目裏二次封裝了 el-select
,實現 filterable
的時搜索輸入框移到下拉列表中,避免多選時多個tag
擠壓了搜索框的空間。當時是寫在一個項目裏,然後其他項目也遇到了這樣的需求... 我在十幾個項目裏面尋找、回憶,找回當年封裝的組件,人都麻了......
正好最近在搞雲產品,需要提供給各中後臺統一的樣式、佈局規範以接入,還需要統一擴展基礎組件的能力。於是組件庫的需求的就這麼出來了!基本想做成的就是對 element-ui/plus
、 antd
一些組件進行二次封裝、擴展,並集成到組件庫中,筆者當仁不讓把需求從大佬手上搶過來做。
目前初步搭建起來了一個簡易的組件庫了,可對 element-plus
、 element-ui
的組件進行開發調試,且目前已經實現幾個組件的擴展了~當然,第一版還是有很多工作沒做完、做好,不過沒關係,畢竟不能一下喫成一個胖子。更多實現、優化、還會慢慢迭代做,到時候有空會繼續分享相關的乾貨~
本文會從組件庫的工程架構、文檔、組件開發環境準備、打包、發佈進行分享,組件開發環境主要是 element-ui/plus
的🤣,因爲本期需求只要滿足 vue2
、 vue3
的中臺,所以 antd
的還沒有投入,只好等下期了。與其說分享,其實更是做一個記錄沉澱一下,也是回顧總結~事不宜遲!開始進入主題吧,從 0-1 搭建一個組件庫!
一、項目架構
第一次搞組件庫,彷彿走進一個新的空白領域了。作爲一個沒經驗的小白,當然是得抄作業啦,不不不,應該是 “借鑑”😜。這時候搞個開源的項目來參照參照還是挺香的,於是筆者就去 “學習了” element-plus 項目的架構、代碼組織方式,再結合自己的需求場景就開始幹了。
1. Monorepo
整個工程的代碼組織採用 Monorepo
的組織方式,使用工具 pnpm + workspace
來實現。所以全部項目都是放在一個倉庫裏的,包括文檔、組件。
工程具體分爲以下幾塊,以文檔和組件庫爲兩大類進行分塊:
- 文檔工程(docs)
- 安裝指引
- 組件使用文檔(
elm
、elp
、antd
) - 組件開發文檔
- 組件庫(packages)
element-plus
element-ui
ant-design
voice-components
其中 voice-components
筆者是打算用來做 adapt 層 用的,因爲文檔工具用了 VitePress
(後面會講),它只能支持 vue3
的組件,所以 vue2
、 react
的組件需要做一層適配,這一塊是預留的,暫時可以不關注。
第一版比較簡單,後續如果沉澱出一些工具、打包腳本等,也會再擴展幾個項目放進去 workspace
裏。所以目前就先這樣吧,用着先~
2. 文檔項目結構
抄作業抄作業,這部分跟 element-plus
基本是一致的。
index.md
。!!顧名思義,文檔首頁~.vitepress
目錄:文檔站點工具配置相關,這個後面再展開~- zh-CN 目錄:文檔 md 文件
- components:
組件使用文檔.md
。組件的使用 demo 及案例代碼,相關配置說明 - guide:
組件庫指引文檔.md
。包括組件的安裝指南、開發指南
- components:
- public 目錄:相關靜態資源目錄。
css
、image
等 - build 目錄:放點自己實現的構建腳本、vite 插件啥的
3. 組件庫結構
這部分跟 element-plus
也是基本一致的,具體大家可以參照他們的實現,這裏就記錄個大概,粗略帶過吧。
每一個 ui 框架的結構都一樣,以其中一個爲例記錄:
-
組件項目入口——
根index
。導出當前項目需要導出的所有模塊(可按需引入)。並導出全局安裝方法。(Vue.use(VcComponents)
)可全局註冊export * from './components' export default { install } 複製代碼
-
components:
-
入口文件:index。導出所有組件。
export * from '...' export * from '...' export * from '...' 複製代碼
-
存放全部組件,以組件名作爲文件夾名。
-
-
組件文件夾(以 button 爲例):
- 入口文件:index。導出當前組件,幷包裝
install
方法(主要用於Vue.use
調用時進行全局註冊)。 - 組件文件。實現組件擴展的二次封裝。(這裏建議擴展組件時保留組件的原來用法,這樣可以降低使用時候的學習成本)
- 入口文件:index。導出當前組件,幷包裝
二、組件庫工具
這裏不會面面俱到,只記錄一些用到的核心工具以及核心的用法~就算不是特別細粒度,相信大家要自己動手搞的時候也難不倒你們的!!筆者這麼菜都一樣搞,你們肯定都行!
1. 文檔站點工具——VitePress
對於組件庫來說,文檔可以說是最關鍵的一環了,沒有文檔的組件庫不是真的組件庫~這裏筆者用了幾分鐘去調研(根本就沒怎麼調研),最終決定使用 VitePress 作爲文檔站點工具,目前用的版本是 1.0.0-alpha.4
。(哈哈哈大家不要害怕 alpha 版,用着沒啥毛病)
使用下來基本配置用法在官方文檔中都能找到,已經滿足當前的使用場景了~大家也要採用的話,花點時間去搓一搓就好,整個文檔站點搭建不算難,畢竟只要能跑起來就可以慢慢調整慢慢搞。
核心配置(都放在 .vitepress
目錄下):
-
配置文件:
.vitepress
根目錄的config
文件。其實沒有特別多的配置,主要就是導航欄和菜單欄而已。export default defineConfig({ title: 'voice-ui', description: '', base, head: [ [ 'link', { rel: 'icon', href: '/images/favicon.ico' } ] ], themeConfig: { logo: '/images/favicon.ico', nav, // 配置導航欄 sidebar, // 配置側邊菜單欄 footer // 配置頁腳 } }) 複製代碼
-
nav 配置導航欄配置(文檔鏈接)
export default [ { text: '指南', link: '', activeMatch: '' }, { text: 'element-plus', link: '', activeMatch: '' }, { text: 'element-ui', link: '', activeMatch: '' }, { text: 'ant-design', link: '', activeMatch: '' } ] 複製代碼
-
sidebar 配置側邊菜單欄(文檔鏈接)。具體配置太多就不全貼出來了,這裏的配置在文檔中都能找到。如下這樣配置就是一個
nav
路由對應一個sidebar
菜單。export default { '/zh-CN/guide/': [ { text: '安裝', items: [ { text: 'element-plus', link: '' }, ... ] }, { text: '開發者指南', items: ... } ] } 複製代碼
大概的效果如下,不同 nav 對應各自的側邊欄菜單:
-
/theme/index
中自定義主題 & 全局註冊vue3
組件
-
具體配置參照文檔。這裏的僅是筆者的基本配置~
import { App } from 'vue' import Theme from 'vitepress/theme' import '../../public/css/customStyle.css' // 自定義的主題色文件 import 'element-plus/dist/index.css' import 'element-plus/theme-chalk/dark/css-vars.css' import VcComponent from '@voice-ui/voice-components' // 上文提到的adapt層,導出vue3的組件 export default { ...Theme, enhanceApp ({ app }: {app: App}) { app.use(VcComponent) // 進行組件註冊,這樣我們可以直接在 markdown 中使用組件啦! } } 複製代碼
-
customStyle.css
文件其實就是對VitePress
的一些 css 變量 進行自定義重寫👇:root { --vc-primary-color: #295dfa; ... } :root { --vp-c-brand: var(--vc-primary-color); /* 自定義 VitePress 的主題色 */ ... } 複製代碼
2. 打包工具——Vite
提到這個必須提一嘴:開發真絲滑!是的,包括各項目的 dev
、 build
都是使用 Vite
完成。其實這個沒什麼好說的,大家可能用得比我都熟~所以這裏只簡單帶一帶用了什麼功能~
- 用到的 vue2、 vue3 插件(官方文檔戳):
underfin/vite-plugin-vue2
@vitejs/plugin-vue
- 打包配置——庫模式。基本的都有了。
es
、cjs
、umd
、iife
。(官方文檔戳,具體下文會講) - 配置
alias
。各模塊在 dev、prod 環境中相互引用(官方文檔戳,具體下文會講) - 配置
external
。(vite 配置;rollup 配置)
第一版差不多就這些了,配置上還是比較簡單的。基本可以滿足 dev 開發、build 打包 需求。
三、開發環境
因爲使用的 VitePress
支持在 markdown
中直接使用 vue3
組件,所以 vue2
、 vue3
、 react
相關的開發環境有所不同。基於此, element-plus
的開發環境就沒有單獨搞了,直接在 docs
項目中進行組件開發。
1. vue3 + element-plus
開發環境
這裏也是直接抄作業的,模仿 element-plus
的實現。核心做法:
- 包裝
element-plus組件
一層install
方法 - 在
VitePress
中進行全局註冊 - 在
md文件
中直接使用註冊好的組件,可以直接在文檔中進行開發調試
大概的代碼思路:
-
給組件對象添加
install
方法import { withInstall } from '../../utils' import Button from './button.vue' export const VcButton = withInstall(Button) export default VcButton export * from './' 複製代碼
-
install 方法:接收一個
Vue3
對象,用Vue.component
進行組件註冊export const withInstall = comp => { comp.install = app => { app.component(comp.name, comp) } return comp } 複製代碼
-
文檔項目中,在
.vitepress/theme/index.ts
中進行全局註冊(上文也有提到)export default { enhanceApp ({ app }: {app: App}) { // 這裏能拿到 app ,也就是Vue3的app // VcButton在這裏進行全局註冊 app.use(VcButton) // app.use 就會調用 VcButton的install方法 } } 複製代碼
-
md 中直接使用註冊好的組件
# Button 按鈕 這是一個按鈕 # Element-plus ## Button <vc-button /> 複製代碼
然後就能在頁面上看到了,並且是有熱更新的!這樣我們直接開發調試即可了。
2. vue2 + element-ui
開發環境
vue2
、 react
的開發環境實現思路大致相同(react 的這次還沒搞,以 vue2 爲例就好),就是在當前項目中用 vite 啓動一個 devServer 進行開發,就跟普通的項目開發是一樣的。
-
根目錄建一個
index.html
,指定入口 -
搞個 demo 目錄,其實就是 Vue 項目,newVue 完後掛載到 dom 上。如下:
在 demo-xxx
的 vue 文件中導入開發的組件進行試用、調試。頁面效果如下:
react 的雖然還沒做,但是具體思路也是跟 vue2 一樣的,在 react 自己的項目中起服務進行組件的開發調試~後續做了的話會補充進這裏~
四、組件打包、發佈
目前的打包、發佈實現得比較簡單。大概是統一打包,然後進到每個目錄中去進去 npm 發佈,目前也是隻發佈在內部的 npm 中。
1. 統一打包
爲了打包方便,且契合當前發佈平臺的特性,在整個項目的根目錄中 package.json 的 scripts
中進行了命令整合。這裏後續可能會用腳本的方式去實現,因爲可能在打包的時候要處理一些其他的邏輯。目前第一版大概如下:
{
"scripts": {
"build": "pnpm run build:elp && pnpm run build:elu && pnpm run build:shared",
"build:elp": "pnpm run -C packages/element-plus build",
"build:elu": "pnpm run -C packages/element-ui build",
"build:shared": "pnpm run -C packages/shared build",
"release": "node scripts/release.ts"
}
}
複製代碼
2. 組件庫的打包配置
上文打包工具哪裏有提到過,目前是最簡單版的,打出 es、cjs、umd、iife 格式的包,而且要 external 掉第三方庫。大概配置:
export default defineConfig(async ({ command, mode }) => {
return {
plugins: [ createVuePlugin() ],
build: {
rollupOptions: {
external: ['element-ui', 'vue']
},
lib: {
entry: path.resolve(__dirname, './components/index.js'),
name: 'voiceUi',
fileName: 'vc-element-ui',
formats: ['es', 'cjs', 'umd', 'iife']
}
},
resolve: {
alias: await alias()
}
}
})
複製代碼
3. npm 發佈
其實 npm 包 安裝只是其中一種方式,該組件庫後續還會新增模塊聯邦——MF 的接入方式,這個會在後續進行擴展,到時候做了的話再補充一下或者再寫一篇文章吧~
目前的 npm 發包時候用了個腳本,基本就是進到每個目錄下去執行以下更新版本好,然後執行 npm publish
。(這裏的腳本會結合自己發佈平臺的一些能力去寫的,所以就不貼出來了,大致思路就是這樣)
五、開發時一些注意點
1. Vue 版本衝突導致啓動服務、打包失敗
因爲是使用 Monorepo
代碼組織方式,所以整個項目難免會出現依賴包重合(版本不同)的問題。就好比這整個項目中既裝了 Vue3
、也裝了 Vue2
,可能起項目時會報錯:
Vue packages version mismatch:
- vue@3.xxx
- vue-template-compiler@2.xxx
複製代碼
但是仔細檢查發現當前項目的 node_modules
中的 vue
和 vue-compiler
是同版本的,而且是在當前工程中執行的啓動、打包。查了個 issus ,在 pnpm 文檔中找到了相關的解決方案:
具體文檔地址:pnpm—shared-workspace-lockfile
配置了這個文件後問題就解決了~
2. 配置 alias 解決入口問題
組件庫打包後的入口跟開發時的入口其實是有點不一致的,所以如果我們開發中直接 import xxx from 組件庫名稱
這樣導入組件是會有問題的,畢竟一般情況下我們的入口是配置打包完之後的產物的入口的(一般是 dist、lib 目錄下的 index)。
出於這點,配置個 alias 就很好解決問題了,因爲很多地方用到,筆者直接就封成了個函數:
export async function alias (): Promise<Array<Alias>> {
const projectPath = packagesPath()
const dirArr = await fsPromises.readdir(projectPath)
return dirArr.map(packagePath => {
return {
find: new RegExp(`^@voice-ui\/${packagePath}(\/(dist))?$`),
replacement: path.join(projectPath, `/${packagePath}/index`)
}
})
}
複製代碼
大概作用就是把入口從 dist 下面換到當前工程的對應項目下的 index 入口。
3. 樣式隔離
docs 項目中用的 VitePress
,他會有一些自己的樣式控制,可能會影響到我們需要在文檔中展示的組件。剛好筆者就遇到了這麼一個情況:
如圖所示,table 的樣式變得很奇怪。筆者並沒有對樣式有做什麼處理,就是 element-plus 的 table。用審查元素看了下,主要是 VitePress
也有自己的 table 的樣式,影響到 el-table
的表現了。
我們可以通過重寫樣式解決,但這樣在其他的如 antd
、element-ui
的組件放進來的時候也會有問題。所以最好還是把樣式進行隔離。哈哈哈,其實寫到這裏的時候,筆者還沒有實現樣式隔離,畢竟給大夥寫文章更重要嘛!!
筆者有個大概想法是用 webComponent
去隔離,這隻好做完了再寫出來了😝,讓大家留個念想。
寫在最後
其實組件庫這個東西真是早有早好,如果當前團隊還沒有的話,趕緊搞一個吧。筆者以前也沒意識去搞這個東西(還是太菜了),所以做了很多重複的勞動。每做一些新項目,或者參與別人的項目,經常想用一些自己之前封裝的組件,都很麻煩,有時候爲了不想切來切去,直接就動手寫了。現在想想,要是那時候就做了個組件庫該多好啊~其實做個簡單版的也不是很難,如果是隻需要關注一種前端框架的那就更簡單了,要考慮的東西更少。把整個組件庫搭出來之後,還能找其他小夥伴一起共建,一起維護,不斷強大自己團隊的組件庫,大家一起受益~
真的!還沒有的趕緊搭!這瓜保甜。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://juejin.cn/post/7120893568553582622