【Vue】1856- 一文揭秘Vue3組件庫的優(yōu)雅打包與細節(jié)

我認(rèn)為,前端工程化更像是個命題作文,沒有標(biāo)準(zhǔn)答案,能解決問題即可。所以,本文旨在以我解決問題的實戰(zhàn)經(jīng)過來分享一種組件庫的打包方式,希望大家都會有所啟發(fā)、有所收獲!廢話少說,直接開始吧。
現(xiàn)狀&目標(biāo)
看過我文章的同學(xué)應(yīng)該知道很久之前我搞了個組件庫,還寫了些文章——快上車!從零開始搭建一個屬于自己的組件庫![1]...之類的分享給大家。但其實組件庫一直處于一個發(fā)展階段,還有很多東西都不成熟,就好比今天的主角——組件庫打包。
這里先簡單回顧一下之前的組件庫打包:
-
直接配置 vite.config文件。只配置了lib模式打包 -
運行 pnpm run build,其實就是執(zhí)行vite build簡單粗暴完成組件庫打包 -
輸出 umd、iife、es、cjs模式的產(chǎn)物文件 -
輸出一個 style.css樣式文件
這是最終打包完的 dist 包下的目錄結(jié)構(gòu):
簡單點開看一下 es 包的產(chǎn)物代碼:
好了,以上就是之前的組件庫打包的方案和結(jié)果,簡單又好用,簡直了。但是為什么現(xiàn)在的我要選擇升級打包方式呢?是基于什么樣的痛點?這一個我會在后文慢慢道來。這里,大家暫且先跟我一起看看本文的實戰(zhàn)目標(biāo)。
講到實戰(zhàn)目標(biāo),不得不以優(yōu)秀的開源項目為標(biāo)桿。這里我就直接按照 element-plus 的產(chǎn)物格式作為目標(biāo)了。跟大家簡單的看看它的打包產(chǎn)物的結(jié)構(gòu),如下圖所示:
其中再點開它的 es 目錄看看究竟:
非常工整的結(jié)構(gòu),也就是打包前的原項目結(jié)構(gòu)。感興趣的可以去 unpkg[2]或者自己裝一個在node_modules中查看。這里我們直接分析產(chǎn)物結(jié)構(gòu)如下:
-
dist。放整包的,簡單理解為一個打包所有代碼并壓縮的.min.js -
es。es包,產(chǎn)物按項目的目錄結(jié)構(gòu)生成。簡單理解為只將ts、.vue文件編譯成js -
lib。跟上面的 es 的一樣,只是這里是 cjs 模式的 -
theme-chalk。樣式代碼,各組件的css文件和一個整體的index文件 -
package.json。emmm...這個大家自己翻譯吧 -
*.d.ts??膳浜?vscode 的 voloar 插件實現(xiàn)代碼提示 -
*.json。用于 webstorm 的代碼提示
所以,上述就是本文的目標(biāo)了,我也要通過對組件庫打包的升級改造,讓組件庫的打包產(chǎn)物跟上述結(jié)構(gòu)、功能相似。不過,本文只著重分享組件庫的打包,代碼提示相關(guān)的實踐并不會涉及,并且我打算另外寫一篇文章來分享組件庫的代碼提示實戰(zhàn)。
分離CSS
這一小節(jié),我將為大家解開上一小節(jié)我遺留下來的一個問題:我是基于什么樣的痛點才要升級組件庫的打包?畢竟這種大佬都不愿意投入資源的非kpi項目,我有那時間摸摸魚不香嗎是吧~
回到本小節(jié)的主題,為什么要分離css?那就得看之前是怎么開發(fā)的了。
如上圖所示,這是最基本的 vue 開發(fā)模式了:template + script + css。這樣寫法的組件,通過 vite 的 lib 模式直接打包,可以得到產(chǎn)物:**.js 和一個 style.css,這一點沒問題吧,前文已經(jīng)講過了。
這樣有什么樣的問題?這樣會導(dǎo)致所有組件的樣式都被打包進了同一個 css 文件里,按需引用對于樣式文件來說就不存在了。當(dāng)然,這也只是其中一個問題,而且還是個小問題而已,我肯定不會因此而升級打包方式和改變項目組成結(jié)構(gòu)的,所以我們接著往下看先。
如果之前有看過我另一篇組件庫實戰(zhàn)文章:組件庫實戰(zhàn)——按需加載工程化[3] 的同學(xué)應(yīng)該知道,那時候的解決方案其實也是有缺陷的,這也是當(dāng)前遇到痛點必須解決的地方。當(dāng)然,沒看過的也沒關(guān)系,這里我簡單的說明一下:
在使用element-plus按需導(dǎo)入[4]的項目中,如果直接使用自己組件庫的組件(我是vc-前綴的),會丟掉原本el-組件的樣式。比如說我直接在代碼中使用 vc-button,而他又依賴el-button的樣式,此時就會丟失el-button的樣式。原因就是按需引入的插件匹配 vc-button 的組件時并不知道需要引入 el-button 的樣式,于是我的自己寫一個插件工具來實現(xiàn)這一點。
如上圖所示,這個插件工具——resolver當(dāng)解析到 template 中有 vc-button 的時候,會去 import vcButton 的組件代碼和 import el-button 的樣式代碼。(此時 vc-button 的樣式文件是全局引入的。因為打包后只有一個 style.css)
雖然這個自動導(dǎo)入的問題是解決了,但是又遺留了另一個問題就是組合組件的樣式問題。比如說此時我的 vc-button 不僅用了 el-button,還組合了 el-tag、el-select 等組件使用,那上述插件工具就沒用了,因為缺少import另外兩個組件的樣式。
如需在使用組合組件的時候樣式能正常引入,那你得告訴插件這個組件用了哪些組件的樣式是吧?那這個要怎么做呢?其實我們可以參考老大哥——element-plus 的實現(xiàn)。因為他就有這樣的案例,比如說他的 select 組件,就是多個 el- 組件組合成的。我們?nèi)タ匆幌滤脑创a:
可以看到 select 目錄下有一個 style 目錄,點開里面的文件可以看到其做了一個樣式的集成。引用了 input、tag、popper 等組件的樣式。雖然之前參與這個項目的時候沒怎么留意過這個 style 目錄(也不知道是干嘛的),但是現(xiàn)在大概可以猜出他就是一個樣式關(guān)系表,做整合的。結(jié)合上述說明,我們重新畫個圖來看看就很清晰了:
這里,我們可以看出 import 樣式那一塊上不再是直接用 el-button 的樣式了,而是用了 vc-button的樣式索引文件(是個js文件),而這個索引文件呢,引用了各種它所需要的樣式文件。如此一來,之前遺留下來的缺陷問題也就引刃而解了。
這也是將 css 拆分出來的好處,讓他形成一個 原子css 的概念。將每一個組件的樣式都單獨抽出來寫,每一個組件的樣式就是一個原子css,這樣組合使用的時候也就很方便了。
然后,這里再順便提一下為什么這次的更新打包,要保留原本的目錄結(jié)構(gòu)來打包?;蛘哒f為什么 element-plus 產(chǎn)物的 es、lib 目錄下是保留了原項目結(jié)構(gòu)的。我們來看看它官網(wǎng)的其中一個介紹:
我個人猜測,如果需要手動引入樣式的話,還是要知道去哪里引用的對吧?總不能打包完代碼就亂成一團,然后用戶需要手動引入的時候不知道去哪里引入了...
好了,這一小節(jié)說得有點長,我簡單總結(jié)一下:
-
解釋了自建組件庫使用 unplugin之類插件[5] 實現(xiàn)自動按需導(dǎo)入的樣式引入問題。 -
解釋了為什么要分離 css?核心解決組合組件的樣式問題,順便解決按需加載的體量問題。 -
順便分析了為什么組件庫打包完要保留原項目結(jié)構(gòu)。
編寫打包腳本
前文鋪墊了這么多,終于輪到本文的重頭戲了。那這一小節(jié),我們主要實現(xiàn)幾個目標(biāo)點:
-
使用 gulp 串并聯(lián)工作流完成打包任務(wù) -
打包出 全局dist、es、lib 的產(chǎn)物。其中es、lib目錄要保持原目錄結(jié)構(gòu) -
抽離css,并且編譯打包css(這里我用的是scss)
這里因為我們要對不同格式根本打包,再配置成 vite.config 并直接通過 vite build 來打包肯定是不夠方便的了,所以我借助 gulp 來完成一個簡單的打包腳本。正式進入打包環(huán)節(jié)前,先來解決工具選擇的問題。目前我個人意向的是 vite 和 rollup,我是如何選擇的呢?在此,我先擼了個圖:
從圖中大概可以看出來,vite 雖然生產(chǎn)環(huán)境默認(rèn)使用 rollup 來構(gòu)建,但相比于 rollup,它是更為上層的。它會有更多的集成,比如說集成了對 ts 的支持,對 scss、less 等支持,還有各種基礎(chǔ)的插件集成(如支持直接 require 模塊),當(dāng)然,他還預(yù)置了一些通用的 rollup 配置。
講這么多,簡單來說就是 vite 更上層,使用方便,適合懶人;**rollup 更底層,使用靈活**,適合有激情愛折騰的大佬!我當(dāng)然是選擇了前者~如果說想使用 rollup 的話,建議大家直接參考 element-plus 的打包吧,它就是基于 rollup 寫的打包腳本。
我們直接看基于 vite 寫打包腳本的基本格式:
import { build } from 'vite'
function buildScript () {
build({
plugins: [],
build: {
outDir: 'xxx',
lib: {
entry: 'xxx'
}
},
})
}
其實也很簡單,安裝 vite,然后 import 它暴露出來的 build 函數(shù),并對其中做一些配置。這些配置就跟我們平時配置 vite.config 文件是一樣的,一把梭哈,基本沒什么難度。接下來我們看看其中每一步的一些核心點吧。
1. 打包 es、lib 包并保留原結(jié)構(gòu)
關(guān)于 es、lib 包,我們依舊是使用 vite 的 lib 模式去構(gòu)建[6](詳細可點擊鏈接去了解)。這里簡單的列一下基礎(chǔ)的配置:
{
plugins: [
vue(),
vueJsx()
],
build: {
outDir: join(vcElementPlusRoot, 'dist', 'es'),
lib: {
entry: files,
formats: ['es'],
},
rollupOptions: {
external: ['element-plus', 'vue', 'vue-router', '@element-plus/icons-vue']
}
}
}
其中的 plugins 配置中,因為我們打包的是 vue3 組件庫,所以會用到 vitejs/plugin-vue[7] 等相關(guān)的 vue 插件。
build 配置中,我們主要看 lib.entry 即可。這里的入口是可以傳數(shù)組(多個)的,如下圖的說明:
這里可以通過一個工具——fast-glob[8]拿到所有的入口,包括 style 目錄中的索引文件的入口(這一點后面會提到)。
如以下寫法,就能拿到組件庫的全部入口了:
const files = await glob('**/*.{js,ts,vue}', {
cwd: vcElementPlusComponentsRoot,
absolute: true,
onlyFiles: true,
})
獲取的結(jié)果如下截圖(大家可以自己試著玩玩):
剩下的就是 rollupOptions 配置,當(dāng)我們想實現(xiàn)打包后保留原項目結(jié)構(gòu),必須配置:
-
preserveModules[9]。此模式將使用原始模塊名稱作為文件名為所有模塊創(chuàng)建單獨的塊,而不是創(chuàng)建盡可能少的塊。(感興趣的點擊進去看看吧,這里我隨便找個翻譯軟件翻譯的) -
preserveModulesRoot[10]。當(dāng)output.preserveModules為true時,應(yīng)該從output.dir路徑中剝離的輸入模塊的目錄路徑(同上,感興趣點鏈接了解吧)
ok,基本上有了這些之后,打包 es、lib 的任務(wù)就完成了。最終腳本代碼如下:
await build({
resolve: {
alias: VcElementAlias()
},
plugins: [
vue(),
vueJsx()
],
build: {
outDir: join(vcElementPlusRoot, 'dist', 'es'),
lib: {
entry: files,
formats: ['es'],
},
rollupOptions: {
external: ['element-plus', 'vue', 'vue-router', '@element-plus/icons-vue'],
output: {
preserveModules: true,
preserveModulesRoot: vcElementPlusComponentsRoot,
exports: 'named'
}
}
}
})
接著,我運行一下打包看看打包后的效果:
為了方便大家看產(chǎn)物結(jié)構(gòu),我用不同顏色的框框劃分了打包后的產(chǎn)物結(jié)構(gòu),可以看到基本符合我們的預(yù)期了。(cjs模式的打包跟es類似的,所以我就不展開lib目錄的打包過程和產(chǎn)物了)
2. 打包 dist 代碼
這一點相比上一點來說要更加的簡單,其實就是我們改版前的那種。無腦配置個輸出模式為 umd 和 iife 就完成了。簡單看看相比前文的差異的配置:
build: {
outDir: join(vcElementPlusRoot, 'dist'),
lib: {
entry: file,
formats: ['umd', 'iife'],
name: VC_ELEMENT_PLUS_CAMELCASE_NAME,
fileName: format => `${VC_ELEMENT_PLUS}.${format}.js`
}
...
}
其余的基本沒什么不同,不過注意這里要配置 name 和 fileName。當(dāng)然,這些你不配置的話,打包也不會成功,并且會有報錯提示,只要根據(jù)報錯提示完成對應(yīng)的配置后,問題也不大。我打算把這兩個文件放在 dist 的根目錄中,跟 es、lib 目錄同級。
最終打包出來的結(jié)果如下:
也是符合預(yù)期的,我們接著往下走。
3. 編譯&打包CSS
這一步,我借助了gulp-sass[11]插件。它的作用是:用于將 Sass 代碼編譯成 CSS 代碼。在正式講打包之前,我給大家看看我抽離的樣式文件大概成什么樣:
我將原本都各自寫在組件內(nèi)的樣式抽離出來,放在一個 theme 的目錄下的。每一個 scss 文件以組件名命名,他們就是一個個組件的原子css。然后我會在組件對應(yīng)的 style 索引文件中這樣引用樣式(直接引用 scss 文件):
大家也可能注意到了,在 theme 目錄下,也有一個 index.scss 文件,它的代碼是這樣的:
沒錯,它其實就是一個總的樣式文件。緊接著,我們直接看看關(guān)于 scss 的編譯、打包腳本如何實現(xiàn)吧:
import gulpSass from 'gulp-sass'
import gulp from 'gulp'
import dartSass from 'sass'
import { vcElementPlusRoot } from '../../utils/path';
export async function sassCompiler () {
const sass = gulpSass(dartSass)
return await gulp.src(`${vcElementPlusRoot}/theme/*.scss`) // 入口
.pipe(sass.sync()) // 編譯
.pipe(gulp.dest(`${vcElementPlusRoot}/dist/theme`)) // 輸出目錄
}
對于上述代碼,他的作用就是編譯所有的 scss 文件,并且將他們都打包進 index 文件中。因為關(guān)于 gulp-sass 、 gulp 等這些工具,我也是要用的時候才去寫,平時也了解不多,所以我就不多展開他們的一些用法、寫法了,大家感興趣可以自己去研究一下。
最后也是來看看打包后的效果:
可以看到,所有的 scss 文件都被編譯成了 css 文件,此時,我們打開一下 index.css 文件大概看看成什么樣:
我沒有做代碼壓縮,所以大家可以一目了然,應(yīng)該是所有組件的 css 都被打進來了,沒問題!
4. 通過 gulp 編排任務(wù)
其實這就是一個工作流工具,有了解過 CI/CD 的同學(xué)應(yīng)該很清楚它是干嘛的了。當(dāng)然,還是那句話,我并不是常年使用 gulp 的,所以了解得也并不多,這里使用也就是為了解決問題,達成目的,所以我也不會過多的展開對 gulp 的講解。大家感興趣的可以去他的官網(wǎng)[12]詳細看看。
因為前面我們已經(jīng)把打包任務(wù)都分別實現(xiàn)了,最后通過 gulp 做一個串聯(lián)而已。所以我還是直接上代碼吧:
import { series, parallel } from 'gulp'
import { cleanDist, elpBuildModules, elpBuildBundle, sassCompiler } from './tasks'
export default series(
cleanDist, // 刪除上次的dist
parallel(
elpBuildModules, // 并行執(zhí)行 es、lib 打包
elpBuildBundle, // 并行執(zhí)行 全局dist 打包
sassCompiler // 并行執(zhí)行 scss 的編譯打包
)
)
整個 gulpfile 就這么點,簡單來說它做的事情就是刪除上次的 dist,然后進行 es、lib、dist、scss 的編譯打包工作。
彩蛋——rollup插件改動樣式索引文件
不知道大家發(fā)現(xiàn)沒,前文兩個地方我都埋了點伏筆:
-
入口為什么要包含 style目錄中的索引文件 -
組件 style目錄中的索引文件直接引用scss文件:
相信已經(jīng)很明顯了,因為直接引用 scss 文件作為樣式文件在瀏覽器中無法直接使用!所以我們需要對其做一些改動。**開發(fā)環(huán)境中,因為 vite 天然支持 scss**(只要安裝了sass的包就行,不用任何插件配置),所以我們在開發(fā)環(huán)境使用樣式時,直接 import 我們的索引文件(再次提醒索引文件是個js)是沒問題的,比如:
import {vcButton} from '@xxx'
import '@xxx/button/style/index.js'
但是如果此時到了瀏覽器環(huán)境直接使用的話就不行了,因為 index.js 中 import 的是一個 scss 的文件。所以我們還需要自己寫一個 rollup 插件,在打包的時候?qū)⑺饕募玫穆窂阶鲆稽c改動。
這個插件的目標(biāo)就是**將 scss 替換成 css**,如:
-
import '@lizhife/vc-element-plus/theme/back.scss' -
import '@lizhife/vc-element-plus/theme/back.css'
當(dāng)然,對應(yīng)的import路徑配置那些也要配置好,不然可能在路徑上也要有所改動。這里我就基于 rollup 的 resolveId[13] 鉤子。當(dāng)然,這一段在 vite 官網(wǎng)[14]也能看到。
簡單說說 resolveId 鉤子的作用,他能拿到你所有 import 的包名、路徑,并在參數(shù)中提供給你。所以基于此,我們可以這樣來寫這個插件:
export function rollupPluginCompileStyleEntry (): Plugin {
const themeEntryPrefix = `${PREFIX}/${VC_ELEMENT_PLUS}/${THEME}`
return {
name: 'rollup-plugin-compile-style-entry',
resolveId (id) {
// 匹配是否滿足 @xxx/vc-el.. 開頭的字符
if (!id.startsWith(themeEntryPrefix)) return
return {
// 將 scss 字符替換成 css
id: id.replaceAll('.scss', '.css'),
external: 'absolute',
}
}
}
}
這個插件的核心就如注釋那樣了,匹配一個固定開頭的字符(比如這里是 @lizhife/vc-element-plus),**將這個字符串的 .scss 替換成 .css**。我們直接看結(jié)果,看看使用了這個插件后的效果如何:
可以看到,import 的最終結(jié)果變成了 xx.css,這也符合我們的期望,完美~當(dāng)然啦,記得把插件配置上,不然就白搞了:
plugins: [
rollupPluginCompileStyleEntry(),
vue(),
vueJsx()
],
彩蛋——alias配置
當(dāng)在項目中使用自身依賴時,需要注意配置alias。
Error: [vite]: Rollup failed to resolve import "@lizhife/vc-element-plus" from '...'
當(dāng)遇到上述的一些因為包名而導(dǎo)致的無法解析的問題,可以通過配置 alias 來解決,特別是一些開發(fā)環(huán)境和打包完之后有所變動的。相關(guān)的我也在這里說太多了,之前的文章也有提到這一點。
涉及的插件簡介
這里我會介紹本次實戰(zhàn)中會用到的各種插件、工具和他們的作用簡介,希望可以幫助大家更清晰地了解本文的內(nèi)容。另外我會附上每個插件的gayhub地址,感興趣的同學(xué)可以戳進去詳細了解。
-
@esbuild-kit/cjs-loader[15]:
-
支持在 gulpfile 使用 esm(import、export)的模塊化寫法 -
支持在 gulpfile 使用 ts
-
fast-glob[16]
-
提供了一種快速、靈活的方式來匹配文件和目錄。
-
@vitejs/plugin-vue[17]:
-
支持 vite 解析 .vue后綴的單文件組件(SFC),類似 webpack 中我們用的vue-loader
-
@vitejs/plugin-vue-jsx[18]:
-
支持 vite 解析 jsx/tsx。同第3點,并且二者是放在同一個倉庫中的
-
gulp-sass[19]
-
一個 gulp 插件,用于將 Sass 代碼編譯成 CSS 代碼
寫在最后
文章內(nèi)容有點長,大家點贊關(guān)注慢慢看~關(guān)于組件庫打包的內(nèi)容輸出,之前就有小伙伴催更了,但是因為之前沒啥使用上的問題,并且這一塊投入也麻煩,所以一直沒搞。組件庫慢慢發(fā)展到現(xiàn)在,組件數(shù)量慢慢上升,發(fā)展遇到瓶頸了所以需要升級一下組件庫的架構(gòu)和打包。當(dāng)然,后續(xù)有相關(guān)的組件庫實戰(zhàn)我會持續(xù)的輸出文章分享。
最后,如果本文有哪些寫得不對的地方,大家盡管指出。希望這篇文章在你的工程化道路上有所啟發(fā)。再重申一下,工程化是開放性作文,思路、方案有很多,能解決問題的就是可行的,并沒有標(biāo)準(zhǔn)答案。
作者:
回復(fù)“加群”,一起學(xué)習(xí)進步
