使用 Vue 三年后,我終于明白為何“組件設(shè)計”才是真正的災(zāi)難區(qū)
起初寫 Vue 的那會兒,我和多數(shù)新人一樣,心想:“哇,組件也太優(yōu)雅了吧!”
三年回頭望,組件目錄卻像一處填埋場:
維護(hù)這里改壞那里、props 漫天飛、事件橫沖直撞、復(fù)用基本靠復(fù)制粘貼。
那時我才真正意識到——在 Vue 項目里,組件設(shè)計才是名副其實的災(zāi)難地帶。
1. “抽組件”≠“新建個文件夾”
很多初學(xué)者對于“組件化”的理解很直接:“頁面上有重復(fù) UI?好,抽出來做成組件。”
于是你很快得到這樣一個組件:
<!-- TextInput.vue -->
<template>
<input :value="value" @input="$emit('update:value', $event.target.value)" />
</template>接著,需要一個帶圖標(biāo)的輸入框,于是復(fù)制第一份:
<!-- IconTextInput.vue -->
<template>
<div class="icon-text-input">
<i class="icon" :class="icon" />
<input :value="value" @input="$emit('update:value', $event.target.value)" />
</div>
</template>隨后又要校驗、加載態(tài)、提示……結(jié)果目錄里長這樣:
TextInput.vue
IconTextInput.vue
ValidatableInput.vue
LoadingInput.vue
FormInput.vue組件數(shù)量指數(shù)膨脹,卻都只是“剛好能用”,彼此難以復(fù)用。因此,規(guī)模一大就維護(hù)崩盤。
2. 過度抽象:為“復(fù)用”而復(fù)用,最后誰也不敢用
設(shè)想你做了一個“超復(fù)雜”的表格組件:
<CustomTable
:columns="columns"
:data="tableData"
:show-expand="true"
:enable-pagination="true"
:custom-actions="['edit', 'delete']"
/>你自豪地稱它為“通用組件”,可當(dāng)同事嘗試落地時,卻發(fā)現(xiàn)——
- 某頁面不需要操作列,但你的配置無法移除;
- 另一頁需要自定義排序,你的實現(xiàn)卻寫死在內(nèi)部;
- 有的場景遵循 element-plus 的樣式,而你做了另一套 UI;
- 報錯后控制臺紅警刷屏,根本不知道源頭在哪。
于是,大家的選擇是:無視“通用組件”,各自復(fù)制一份代碼再改。結(jié)果反而更多“平行宇宙”。
3. 數(shù)據(jù)向下流、事件向上冒:你真的吃透了 props 與 emit 嗎?
理論很清晰:**父傳子用 props,子通知父用 emit**。然而,現(xiàn)實往往是——
- props層層下鉆 7 級,你已分不清數(shù)據(jù)從哪來;
- 子組件觸發(fā)兩個事件,父組件再把回調(diào)倒回去;
- 開發(fā)者“悄悄”用 provide/inject、ref 或 eventBus 打通旁路通信。
示例看起來沒毛病:
<!-- Grandparent Component -->
<template>
<PageWrapper>
<ChildComponent :formData="form" @submit="handleSubmit" />
</PageWrapper>
</template>
<!-- Child Component -->
<template>
<Form :model="formData" />
<button @click="$emit('submit', formData)">Submit</button>
</template>然而,當(dāng) ChildComponent 被 FormWrapper 包一層、內(nèi)部再嵌 InputList 時,你會發(fā)現(xiàn):
- 到底誰在真正控制formData ?
- submit 事件在多層之間被包裝/防抖/節(jié)流/攔截;
- 想改一個按鈕邏輯,你要翻四個文件。
因此,越到后期,通信成本越像迷宮;最終,團(tuán)隊對“抽組件”開始本能抗拒。
4. 技術(shù)債的主力來源:不敢刪、不敢動
目錄結(jié)構(gòu)也許看似井井有條,但多數(shù)組件共有這些特征:
- 有 10 個 props + 3 個事件,卻沒人知道誰在用;
- 注釋寫著“給 A 頁面用”,實際 B/C/D 也悄悄依賴;
- 輕輕一動,蝴蝶效應(yīng)引爆全局。
于是你只能復(fù)制一份、加個 V2 后綴,舊的也不敢刪:
components/
├── Input.vue
├── InputV2.vue
├── InputWithTooltip.vue
├── InputWithValidation.vue
├── InputWithValidationV2.vue
└── ...
“為了讓別人能維護(hù)我的代碼,我決定——我自己先別動它。”
5. 組件設(shè)計的核心,其實是抽象能力
三年里我學(xué)到的一點:難點不在語法,也不在封裝,而在“如何抽象問題”。
例如,需要做一個“搜索區(qū)”組件:包含輸入框、日期區(qū)間、搜索按鈕。新手的寫法:
<SearchHeader
:keyword="keyword"
:startDate="start"
:endDate="end"
@search="handleSearch"
/>但是,當(dāng)需求變成下拉 + 單選時,你要再造一個組件嗎?更好的做法:結(jié)構(gòu)交給組件,內(nèi)容交給頁面——靠 slot / scoped slot。
<!-- SearchHeader.vue -->
<template>
<div class="search-header">
<slot name="form" />
<button @click="$emit('search')">Search</button>
</div>
</template>
<!-- 使用 -->
<SearchHeader @search="search">
<template #form>
<el-input v-model="keyword" placeholder="Enter keywords" />
<el-date-picker v-model="range" type="daterange" />
</template>
</SearchHeader>因此,組件不必“包辦一切”,而是提供骨架、與頁面協(xié)作。與此同時,你也保留了最大靈活度。
6. 那么,組件該怎么“對”地設(shè)計?
我把經(jīng)驗歸結(jié)為三條樸素但有效的建議:
1) 先厘清職責(zé)邊界:UI?交互?還是業(yè)務(wù)邏輯?
- UI 組件:只管展示(Button/Tag/Card)。
- 交互組件:只封裝人機(jī)動作(Input/Select/Uploader)。
- 邏輯組件:收攏業(yè)務(wù)規(guī)則(篩選區(qū)、分頁器)。
不要讓一個組件同時負(fù)責(zé)渲染 + 業(yè)務(wù) + 請求——那是明確的反模式。
2) **收斂 props 與 emit**:只暴露“必要接口”
- 一個組件的 props 超過 6 個,就需要警惕;
- 事件名若缺乏業(yè)務(wù)含義(例如 click),考慮抽象為語義事件(如 confirm/submit);
- 避免用 ref 去操縱子組件內(nèi)部邏輯——這會讓耦合飆升。
3) 能用 Slot,就別用“超級定制的 Props”
當(dāng)你發(fā)現(xiàn)組件 props 長這樣:
<SuperButton
:label="'Submit'"
:icon="'plus'"
:iconPosition="'left'"
:styleType="'primary'"
:loading="true"
/>是時候換成 slot 了:
<SuperButton>
<template #icon><PlusIcon /></template>
Submit
</SuperButton>因此,用 slot 換靈活度、用語義事件降復(fù)雜度、用職責(zé)拆分控風(fēng)險;最終,你會發(fā)現(xiàn)維護(hù)成本陡降。
結(jié)語:從“最簡單”到“最難的坑”
三年前,我以為組件化是 Vue 里最容易的部分; 三年后,我才懂它其實是最深、最難、坑最多的一環(huán)。
如果你也踩過這些坑——
- 寫得越多,“復(fù)用”反而越復(fù)雜,同事不敢用;
- props 與事件像迷宮,維護(hù)成本居高不下;
- UI 與邏輯緊緊捆綁,牽一發(fā)而動全身;
- 后期組件數(shù)量雪球滾大,技術(shù)債堆成山——
請別讓組件成為你項目的“債務(wù)黑洞”。你是否也遇到過類似問題?歡迎分享你的“災(zāi)后重建”經(jīng)驗。






















