身為前端工程師,元件管理是不論選用哪一個前端框架的開發人員都需要面臨的挑戰。能不能妥善規劃與管理元件,很大程度上決定了專案未來的走向。好的元件規劃可以讓開發更有效率,反之,如果沒有妥善應用元件或是濫用元件,可能都會是一場萬劫不復的災難。
在面對全新專案時,你會選擇使用 UI Library 嗎?你要怎麼知道你選擇的、你擅長的 UI Library 是不是適合你的專案呢?
如果能有一套 UI Library 完全貼合專案需求,那就太好了。但現實總是殘酷的,幾乎沒有任何一個專案可以完全依賴某一套 UI Library 來滿足需求。更多時候,我們需要使勁地「魔改」,利用各種權重的 CSS 覆蓋 UI Library 原有的樣式,甚至用盡各種曲折離奇的黑魔法來讓 UI Library 變成我們想要的樣子。改到最後開始懷疑人生,想著自己寫可能還比較快,但頭已經洗下去了,只能摸摸鼻子繼續施展魔法。然後,那天 UI Library 更新時,我們又得重新來過。
使用 UI Library 確實可以幫助我們快速完成專案,但站在長期維護的角度,我們需要思考的是:我們是在使用 UI Library,還是被 UI Library 使用?
我們來嘗試維護一套自己的 UI Library 吧!
也許你不一定認同我的看法,但我認為,身為一個優秀的前端工程師,即使沒有自己手刻一套 UI Library 的打算,也應該要了解每個元件應該怎麼實現,應該涵蓋哪些功能,有哪些細節是我們需要特別留意的。花點時間深入理解每個元件的運作原理,對於如何善用 UI Library 也會有更深刻的體悟。
那麼,是不是從此就捨棄 UI Library 呢?倒也不是,UI Library 仍然是我們的好夥伴,但我們需要知道如何與它合作,而不是被它束縛。在我的習慣中,UI Library 更像是一本又一本的參考書,透過這些參考書,我們可以了解到一個看似簡單的元件背後隱藏了多少細節,也可以學習到更多前端技巧與資料管理的方法。
說了這麼多,來談談我會如何撰寫「為你自己寫 Vue Component」這個系列文章吧。
這是一個針對 Vue 所設計的 UI Library 分享,是我真正會套用在專案中使用的元件。除了使用 Vue 3 之外,有以下幾個名詞在開始前能夠先有些概念會更好:
我相信 Composition API 已經不是什麼新鮮的名詞了,所以不需要多作解釋。在這個系列文章中,將會完全使用 Composition API 來撰寫元件。
TypeScript 是 JavaScript 的超集,這表示任何合法的 JavaScript 程式碼在 TypeScript 中也是有效程式碼。Typescript 為 JavaScript 引入了型別系統,除了能進行型別檢查外,也讓我們實作出來的元件有更清楚的「程式碼提示」。
在這個系列文章中,我們將使用 TypeScript 來定義元件的型別。這不僅能幫助我們透過型別檢查排除潛在的低級錯誤,還能透過明確的型別提示,讓與我們合作的開發人員即使不深入了解實作細節,也能輕鬆掌握如何正確使用這些元件。
原子設計是一種網頁設計方法,由 Brad Frost 在 2013 年時發表的文章中提出。這個方法將網頁設計分為五個不同的層級:
在我的開發經驗中,我沿用了這五個層級,並且重新定義了每個層級的權責與劃分標準,可能跟 Brad Frost 的定義會有一點不同,但我想核心精神是相似的。
原子(Atoms):
元件的最小單位,如:<AtomicButton>
、<AtomicBreadcrumb>
、<AtomicPagination>
,在這個分類中的元件具有最高的重用性,甚至只要調整 CSS 的設定就可以跨專案使用。因此,這裡的元件可以貼近但不專為特定需求服務。
分子(Molecules):
可能是由原子組成的元件,更多時候是與特定專案高度耦合的元件,牽涉到專案獨有的業務邏輯。儘管如此,元件仍然可以在專案中被重複使用,因此我們需要盡可能不讓這裡的元件與後端 API 或資料模組耦合。
組織(Organisms):
不僅僅是與專案耦合,更可能與特定的模板、頁面高度整合。與分子的主要區別是,這裡的元件會直接與後端 API 或資料模組有所耦合,因此這裡的元件更難在專案中被重複使用。在這裡的元件不再是為了重用而存在,主要目的反而是將複雜邏輯隔離開來。
模板(Templates):
在開發習慣上,我通常會直接進入頁面(Pages),但如果遇到高度重複的頁面結構,模板層就會起到很大的作用。在設計上,可能會從頁面層取得資料,再傳入模板層,這樣可以讓我們更容易重複使用模板。
頁面(Pages):
頁面是最好劃分的部分,在 Vue 專案中,它就是 Route Component。這裡的元件是最高層的元件,負責整合所有的元件,並且與後端 API 進行溝通。這裡的元件完全不考慮重用,因為它是最高層的元件,目的是將所有元件整合在一起,並且與後端 API 進行溝通。
這個系列文章的內容只會聚焦在原子(Atoms)這個層級,因此在設計時會盡可能將元件設計得更加通用,這樣可以讓我們在未來的專案中更容易重複使用這些元件。
隨著 Tailwind CSS 與 UnoCSS 的興起,BEM 這個名詞可能已經不再那麼熱門了。但我個人認為 BEM 仍然是一個很好的命名規則,這個系列文章中會使用 BEM 的規範來命名元件。
BEM 是由 Block(區塊)、Element(元素)與 Modifier(修飾符)組合而成。這個命名規則是由 Yandex 的一群前端工程師所提出的,目的是讓 CSS 的命名更有意義、更有邏輯,也更容易維護。
在命名上,會由下列兩種符號來區分:
__
雙底線:用來區分 Element,表示 Block 內的元素。--
雙減號:用來區分 Modifier,表示 Block 或 Element 的狀態。以按鈕為例,一個按鈕的命名可能會是這樣:
<button class="atomic-button atomic-button--primary atomic-button--small">
<span class="atomic-button__prepend"></span>
<span class="atomic-button__content">按鈕</span>
<span class="atomic-button__append"></span>
</button>
這樣的命名方式可以讓我們相對清楚地知道這個元件是什麼,有哪些狀態,也有哪些元素組成。
在這個系列的內容中,每個元件都是一個獨立的 Block,而元件內的 Element 在本系列文章中會使用「區塊」表示,這是為了與 HTML 的元素有所區別。
SCSS 是基於 CSS 語法的超集,它是 Sass(Syntactically Awesome Style Sheets)的一部分。SCSS 增加了許多強大的功能,如巢狀(Nesting)、變數(Variables)、混合(Mixins)、迴圈(Loops)等,使我們可以用更簡單的方式設定 CSS。
簡單的 SCSS 語法如下:
變數(Variables):
$button-height: 40px;
SCSS 的變數與 CSS 的變數不同,記得不要搞混了,下面是 CSS 的變數:
.atomic-button {
--button-height: 40px;
}
混合(Mixins):
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.atomic-checkbox {
&__input {
@include sr-only;
}
}
迴圈(Loops):
.atomic-button {
&--contained {
@each $color, $value in $color-map {
.atomic-button--#{$color} {
background-color: rgba($value, 1);
}
}
}
}
這個系列文章中將使用 SCSS 來設定,不熟悉也沒關係,遇到不懂的地方再查就好了。
Tailwind CSS 應該不會在接下來的內容中再次出現,但儘管使用了 BEM 來命名元件,更多時候我其實會搭配 Tailwind CSS 一起使用。以上面的範例為例,我可能會這樣寫:
.atomic-button {
@apply inline-flex items-center justify-center;
@apply h-[var(--button-height)];
@apply text-sm text-center;
@apply rounded-md;
}
搭配 SCSS 的字串插值(#{}
),我們可以更方便地使用 Tailwind CSS 的 utility class,這樣可以更快地完成元件的撰寫,並且達到全站統一的效果。
但鑒於 Tailwind CSS 與 SCSS 的混合魔法對於不熟悉的人來說可能更難以理解,因此在這個系列文章中會以純 SCSS 來撰寫元件,但對於整個環境的 Reset 還是沿用 Tailwind CSS 的設定。
@tailwind base;
最後,感謝你願意點開連結開始閱讀這一系列的文章。這不是一個新手向的教學,對我而言,很多細節都是經過無數次修改、反覆驗證與腦力激盪後的結果。這些元件囊括了許多 UI Library 中我覺得優良的部分,出處不僅限於 Vue 這個框架。因此,如果你是 Vue 的初學者,我仍然推薦你跟著這個我們一起學習,你可能不會一次就上手,但它會是在你前端旅程中的一劑大補帖。
不論你是否是初學者,或是對元件設計非常有心得,不論你是寫 Vue 還是 React,我都希望「為你自己寫 Vue Component」這個系列文章能夠對你有所幫助。文章中有任何疑問或寫錯的地方,歡迎討論、指正。
感謝高見龍大大願意讓我使用「為你自己寫 Vue Component」這個名字,這絕對是整個系列文章中最難寫的部分了!
原來 @apply
可以分段使用,不用全部寫在一起 XD,學到惹!
是的!可以分段使用,我會將相同類型的集中在一起,這樣看起來也比較舒服一點。
寫得很棒,觀念很適合新手(我)入門,最近一直被UI library套殺,沒辦法吻合客製需求XD。
想請教作者大大,如果不採取SCSS-BEM命名style,利用<style scoped>
讓元件樣式封裝起來,會有什麼缺點嗎,感恩🧡
感謝你!💚
我自己在專案開發時幾乎不會選擇 scoped
,原因之一是 <style scoped>
的運作方式是使用 data-v-hash
來增加 CSS 的權重,如果沒有良好的規範的話還是有機率被污染的,例如:
.container .menu {
font-size: 1.5rem;
}
.menu[data-v-hash] {
color: red;
}
考量到這種情境,我個人覺得 BEM 還是比較能有效避免污染的問題,想要覆蓋元件樣式的話也會是在「有意識」的狀態下進行,也比較能減少不小心覆蓋到其他元件的樣式的情況。
希望有幫助到你。
謝謝大大🧡,最近參考Vuetify發現它們家,也是採SASS客製化變數,給開發者調整樣式。
用scoped封裝樣式發現明顯缺點,除了汙染外,元件樣式調動的參數開口會放到props上,會有一坨props XD。