——從注冊到使用的完整流程)
本篇定位組件化是 Vue 最核心的開發(fā)范式也是構(gòu)建大型應(yīng)用的基礎(chǔ)。本篇將帶你從零開始理解 Vue 組件的本質(zhì)、注冊方式、Props 數(shù)據(jù)傳遞和類型校驗(yàn)。讀完本篇你將能夠獨(dú)立創(chuàng)建和復(fù)用 Vue 組件并理解組件間數(shù)據(jù)流動(dòng)的基本原則。一、什么是組件1.1 組件的本質(zhì)Vue 組件是可復(fù)用的 Vue 實(shí)例它擁有獨(dú)立的模板、邏輯和樣式。從本質(zhì)上講組件就是一個(gè)帶有特定行為和樣式的 UI 片段??梢赃@樣理解一個(gè) Vue 應(yīng)用就像一棵組件樹根組件App.vue是樹根其他組件是其枝葉每個(gè)組件都是一個(gè)獨(dú)立的小世界封裝了自己的 HTML、CSS 和 JavaScriptApp.vue (根組件) ├── Header.vue │ ├── Logo.vue │ └── NavMenu.vue ├── Sidebar.vue │ └── UserProfile.vue └── Footer.vue1.2 為什么要使用組件優(yōu)勢說明復(fù)用性同一個(gè)組件可以在多個(gè)地方使用避免重復(fù)造輪子可維護(hù)性每個(gè)組件職責(zé)單一易于定位和修復(fù)問題可測試性獨(dú)立組件可以單獨(dú)進(jìn)行單元測試協(xié)作開發(fā)不同開發(fā)者可以并行開發(fā)不同組件減少?zèng)_突關(guān)注點(diǎn)分離模板、邏輯和樣式集中管理清晰明了1.3 組件的兩種編寫風(fēng)格Vue 3 支持兩種組件編寫風(fēng)格風(fēng)格特點(diǎn)適用場景選項(xiàng)式 APIOptions API使用data、methods、computed等選項(xiàng)Vue 2 用戶過渡或偏好傳統(tǒng)風(fēng)格組合式 APIComposition API使用setup和script setupVue 3 推薦邏輯復(fù)用更優(yōu)雅本系列教程將默認(rèn)使用組合式 API 的script setup語法這是 Vue 3 的官方推薦寫法。二、單文件組件SFC的完整結(jié)構(gòu)Vue 的單文件組件Single File Component簡稱 SFC使用.vue擴(kuò)展名一個(gè)文件包含三個(gè)部分script setup langts // 1. 邏輯部分JavaScript/TypeScript // 響應(yīng)式數(shù)據(jù)、方法、生命周期等 /script template !-- 2. 模板部分HTML -- !-- 描述組件的 UI 結(jié)構(gòu) -- /template style scoped /* 3. 樣式部分CSS */ /* scoped 表示樣式僅作用于當(dāng)前組件 */ /style2.1script setup是 Vue 3.2 引入的編譯時(shí)語法糖它極大地簡化了組件的編寫script setup langts // 導(dǎo)入的組件自動(dòng)注冊無需手動(dòng)注冊 import MyButton from ./MyButton.vue // 定義的變量和方法直接暴露給模板 const message Hello Vue! const count ref(0) function increment() { count.value } /script template div p{{ message }}/p MyButton clickincrement點(diǎn)擊次數(shù){{ count }}/MyButton /div /templatescript setup的核心特性頂層綁定自動(dòng)暴露所有變量、函數(shù)、導(dǎo)入的組件都可以直接在模板中使用更簡潔的代碼不需要export default、setup()函數(shù)的返回值等更好的運(yùn)行時(shí)性能編譯時(shí)優(yōu)化減少運(yùn)行時(shí)代碼更好的 TypeScript 支持類型推導(dǎo)更準(zhǔn)確2.2 模板模板部分使用增強(qiáng)的 HTML 語法支持 Vue 的指令和插值template !-- 根元素可以是多個(gè)Vue 3 支持多根節(jié)點(diǎn) -- h1{{ title }}/h1 p v-ifvisible可見內(nèi)容/p button clickhandleClick點(diǎn)擊/button slot/slot /templateVue 3 支持多根節(jié)點(diǎn)Fragment不再需要包裹一個(gè)額外的父元素。2.3scoped屬性讓樣式僅在當(dāng)前組件中生效style scoped /* 這個(gè)類名只在這個(gè)組件中生效 */ .title { color: blue; } /style原理Vue 會(huì)為組件中的每個(gè)元素添加一個(gè)唯一屬性如data-v-7ba5bd90然后通過屬性選擇器限制樣式范圍。/* 編譯后 */.title[data-v-7ba5bd90]{color:blue;}深度選擇器:deep()如果需要穿透子組件樣式style scoped /* 影響子組件中的 .child-class */ :deep(.child-class) { color: red; } /style三、組件的注冊使用組件之前需要先注冊。Vue 支持全局注冊和局部注冊兩種方式。3.1 局部注冊推薦在script setup中直接導(dǎo)入并使用組件自動(dòng)注冊!-- Parent.vue -- script setup // 導(dǎo)入組件自動(dòng)注冊 import ChildComponent from ./ChildComponent.vue import AnotherComponent from ./AnotherComponent.vue /script template div ChildComponent / AnotherComponent / /div /template非script setup方式較少使用script import ChildComponent from ./ChildComponent.vue export default { components: { ChildComponent // 顯式注冊 } } /script3.2 全局注冊在應(yīng)用入口main.ts中注冊全局組件可以在任何子組件中使用無需再次導(dǎo)入// main.tsimport{createApp}fromvueimportAppfrom./App.vueimportGlobalButtonfrom./components/GlobalButton.vueconstappcreateApp(App)// 全局注冊app.component(GlobalButton,GlobalButton)app.mount(#app)然后在任意組件中直接使用template GlobalButton / /template全局注冊的優(yōu)缺點(diǎn)優(yōu)點(diǎn)缺點(diǎn)無需在組件中反復(fù)導(dǎo)入即使未使用也會(huì)被打包增加最終包體積適合基礎(chǔ)組件如按鈕、輸入框依賴關(guān)系不明確不利于維護(hù)一次注冊到處可用可能造成命名沖突最佳實(shí)踐優(yōu)先使用局部注冊僅在確實(shí)需要全局可用如 UI 基礎(chǔ)組件庫時(shí)使用全局注冊。3.3 組件命名規(guī)范場景推薦命名示例組件文件名PascalCase大駝峰MyComponent.vue模板中使用PascalCase 或 kebab-case短橫線MyComponent或my-component全局注冊名稱PascalCase 或 kebab-caseapp.component(MyButton, ...)Vue 官方推薦在模板中使用PascalCase因?yàn)樗菀着c原生 HTML 元素區(qū)分。四、Props父傳子的數(shù)據(jù)傳遞Props屬性是父組件向子組件傳遞數(shù)據(jù)的主要方式。Props 遵循單向數(shù)據(jù)流原則父級(jí) props 的更新會(huì)流向子組件反之則不行。4.1 基本用法!-- 子組件Child.vue -- script setup langts // 定義 props使用 TypeScript 類型聲明 defineProps{ title: string count: number isActive: boolean }() /script template div h2{{ title }}/h2 p數(shù)量{{ count }}/p p狀態(tài){{ isActive ? 激活 : 未激活 }}/p /div /template!-- 父組件Parent.vue -- script setup import Child from ./Child.vue import { ref } from vue const parentTitle ref(Hello Props) const parentCount ref(42) /script template Child :titleparentTitle :countparentCount :is-activetrue / /template關(guān)鍵規(guī)則使用v-bind:綁定動(dòng)態(tài)值靜態(tài)值直接寫屬性名字符串也可以不用:titleHello等價(jià)于:titleHello布爾屬性不帶值默認(rèn)為trueis-active等價(jià)于:is-activetrue4.2 類型聲明與默認(rèn)值在script setup中使用類型聲明定義 propsscript setup langts // 基本類型聲明 defineProps{ name: string age: number isAdmin: boolean hobbies: string[] address?: { city: string; street: string } // 可選屬性? }() /script如果需要默認(rèn)值需要使用withDefaults編譯宏script setup langts interface Props { title: string count?: number items?: string[] status?: pending | success | error } // 為可選屬性設(shè)置默認(rèn)值 const props withDefaults(definePropsProps(), { count: 0, items: () [默認(rèn)項(xiàng)1, 默認(rèn)項(xiàng)2], // 對(duì)象/數(shù)組需使用工廠函數(shù) status: pending }) /script注意withDefaults是 Vue 的編譯宏不需要導(dǎo)入直接在script setup中使用即可。4.3 非script export default { props: { title: { type: String, required: true }, count: { type: Number, default: 0 }, items: { type: Array, default: () [] }, status: { type: String, validator: (value) [pending, success, error].includes(value) } } } /script4.4 Props 的校驗(yàn)規(guī)則Vue 提供了豐富的 prop 校驗(yàn)?zāi)芰cript setup langts // 使用運(yùn)行時(shí)聲明而非僅類型聲明支持校驗(yàn) defineProps({ // 基礎(chǔ)類型 propA: Number, // 多種類型 propB: [String, Number], // 必填 propC: { type: String, required: true }, // 默認(rèn)值 propD: { type: Number, default: 100 }, // 對(duì)象默認(rèn)值 propE: { type: Object, default: () ({ name: 默認(rèn) }) }, // 自定義校驗(yàn) propF: { type: String, validator: (value: string) [small, medium, large].includes(value) } }) /scriptTypeScript 類型聲明 vs 運(yùn)行時(shí)聲明方式優(yōu)點(diǎn)缺點(diǎn)TS 類型聲明類型安全、IDE 提示好無法在運(yùn)行時(shí)校驗(yàn)和提供默認(rèn)值運(yùn)行時(shí)聲明支持校驗(yàn)、默認(rèn)值缺乏 TS 類型推斷可以結(jié)合使用最佳實(shí)踐同時(shí)使用兩者發(fā)揮各自優(yōu)勢用 TS 類型提供靜態(tài)類型用運(yùn)行時(shí)聲明提供校驗(yàn)和默認(rèn)值。script setup langts interface Props { size?: small | medium | large count?: number } // 運(yùn)行時(shí)聲明 TS 類型 const props withDefaults(definePropsProps(), { size: medium, count: 0 }) /script4.5 單向數(shù)據(jù)流原則Props 是只讀的子組件不能直接修改父組件傳遞的 propscript setup // ? 錯(cuò)誤直接修改 prop const props defineProps{ count: number }() function increment() { props.count // ? 會(huì)觸發(fā)警告 } /script修改 Props 的正確方式①子組件觸發(fā)事件父組件修改推薦!-- 子組件 -- script setup const props defineProps{ count: number }() const emit defineEmits{ (e: update:count, value: number): void }() function increment() { emit(update:count, props.count 1) } /script②使用計(jì)算屬性轉(zhuǎn)換為本地?cái)?shù)據(jù)script setup import { computed } from vue const props defineProps{ initialValue: number }() const localCount computed({ get: () props.initialValue, set: (val) emit(update:initialValue, val) }) /script4.6 Props 的命名約定在模板中使用camelCase駝峰或kebab-case短橫線均可Vue 會(huì)自動(dòng)轉(zhuǎn)換!-- 父組件兩種寫法都支持 -- Child :userNamename :user-ageage /在 JavaScript 中始終使用camelCasedefineProps{userName:stringuserAge:number}()五、組件實(shí)例與 props 的關(guān)系5.1 每個(gè)組件擁有獨(dú)立的狀態(tài)當(dāng)你多次使用同一個(gè)組件時(shí)每個(gè)實(shí)例都有獨(dú)立的數(shù)據(jù)作用域!-- 父組件 -- template Counter / Counter / Counter / /template每個(gè)Counter實(shí)例都有自己獨(dú)立的count狀態(tài)互不影響。5.2 傳遞非字符串類型傳遞非字符串類型時(shí)必須使用v-bindtemplate !-- 字符串 -- Child name張三 / !-- 數(shù)字必須用 v-bind -- Child :age25 / !-- 布爾值 -- Child :is-activetrue / !-- 數(shù)組 -- Child :hobbies[閱讀, 編程] / !-- 對(duì)象 -- Child :user{ name: 李四, age: 30 } / !-- 方法很少用 -- Child :callbackhandleCallback / /template5.3 動(dòng)態(tài)傳遞多個(gè) props對(duì)象展開如果有一個(gè)包含多個(gè)屬性的對(duì)象可以使用v-bind的對(duì)象展開一次性傳遞script setup const user { name: 王五, age: 28, email: wangwuexample.com } /script template !-- 等價(jià)于傳遞 :nameuser.name :ageuser.age :emailuser.email -- UserCard v-binduser / /template六、實(shí)戰(zhàn)構(gòu)建一個(gè)卡片組件庫我們通過一個(gè)完整的卡片組件示例綜合運(yùn)用本章所學(xué)知識(shí)!-- Card.vue - 通用卡片組件 -- script setup langts interface Props { title?: string subtitle?: string variant?: default | primary | success | warning | danger size?: small | medium | large bordered?: boolean hoverable?: boolean loading?: boolean } const props withDefaults(definePropsProps(), { title: , subtitle: , variant: default, size: medium, bordered: true, hoverable: false, loading: false }) // 計(jì)算樣式類 const variantClass computed(() card-${props.variant}) const sizeClass computed(() card-${props.size}) /script template div classcard :class[ variantClass, sizeClass, { card-bordered: bordered }, { card-hoverable: hoverable }, { card-loading: loading } ] !-- 標(biāo)題區(qū)域 -- div v-iftitle || subtitle classcard-header h3 v-iftitle classcard-title{{ title }}/h3 p v-ifsubtitle classcard-subtitle{{ subtitle }}/p /div !-- 內(nèi)容區(qū)域插槽 -- div classcard-body slot nameheader/slot slot/slot slot namefooter/slot /div !-- 加載狀態(tài)覆蓋層 -- div v-ifloading classcard-overlay span classspinner/span /div /div /template style scoped .card { background: white; border-radius: 12px; padding: 20px; transition: all 0.3s ease; position: relative; overflow: hidden; } /* 尺寸變體 */ .card-small { padding: 12px 16px; } .card-medium { padding: 20px 24px; } .card-large { padding: 28px 32px; } /* 顏色變體 */ .card-default { background: white; } .card-primary { background: #ebf8ff; border-color: #4299e1; } .card-success { background: #f0fff4; border-color: #48bb78; } .card-warning { background: #fffff0; border-color: #ecc94b; } .card-danger { background: #fff5f5; border-color: #fc8181; } .card-bordered { border: 2px solid #e2e8f0; } .card-bordered.card-primary { border-color: #4299e1; } .card-bordered.card-success { border-color: #48bb78; } /* ... 其他變體 */ .card-hoverable:hover { transform: translateY(-4px); box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .card-header { margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e2e8f0; } .card-title { font-size: 18px; font-weight: 600; color: #1a202c; margin: 0; } .card-subtitle { font-size: 14px; color: #718096; margin: 4px 0 0 0; } .card-body { color: #2d3748; } .card-loading { opacity: 0.6; pointer-events: none; } .card-overlay { position: absolute; inset: 0; background: rgba(255,255,255,0.7); display: flex; align-items: center; justify-content: center; } .spinner { width: 32px; height: 32px; border: 4px solid #e2e8f0; border-top-color: #4299e1; border-radius: 50%; animation: spin 0.8s linear infinite; } keyframes spin { to { transform: rotate(360deg); } } /style使用卡片組件template div classapp !-- 基礎(chǔ)卡片 -- Card title歡迎使用 Vue 3 p這是一個(gè)基礎(chǔ)的卡片組件展示內(nèi)容區(qū)域。/p /Card !-- 帶變體的卡片 -- Card title成功提示 subtitle操作已成功完成 variantsuccess :borderedtrue p您的數(shù)據(jù)已保存。/p template #footer button clickhandleAction確認(rèn)/button /template /Card !-- 可懸浮的卡片 -- Card title產(chǎn)品卡片 variantprimary :hoverabletrue :loadingisLoading template #header div classproduct-image?/div /template div classproduct-info p商品名稱Vue 3 完全指南/p p價(jià)格¥99.00/p /div template #footer button加入購物車/button /template /Card /div /template script setup import Card from ./components/Card.vue import { ref } from vue const isLoading ref(false) function handleAction() { alert(已確認(rèn)) } /script七、本篇小結(jié)知識(shí)點(diǎn)核心內(nèi)容關(guān)鍵要點(diǎn)組件本質(zhì)可復(fù)用的 Vue 實(shí)例擁有獨(dú)立模板、邏輯、樣式組件樹構(gòu)成應(yīng)用單文件組件.vue文件包含script setup、template、style scopedscript setup是 Vue 3 推薦寫法局部注冊在script setup中import后直接使用優(yōu)先推薦按需加載全局注冊app.component()全局注冊適用于基礎(chǔ) UI 組件庫Props 定義defineProps{ name: type }()使用 TS 類型聲明Props 默認(rèn)值withDefaults(definePropsProps(), { ... })對(duì)象/數(shù)組需用工廠函數(shù)單向數(shù)據(jù)流子組件不能直接修改 props通過事件通知父組件修改Props 校驗(yàn)運(yùn)行時(shí)聲明支持validator等可結(jié)合 TS 類型和運(yùn)行時(shí)校驗(yàn)八、思考與練習(xí)基礎(chǔ)練習(xí)創(chuàng)建一個(gè)Avatar組件接收src、sizesmall/medium/large、alt等 props并顯示不同尺寸的頭像。實(shí)踐任務(wù)將上一節(jié)注冊表單中的部分內(nèi)容如輸入框組、表單驗(yàn)證錯(cuò)誤提示抽取為可復(fù)用的子組件使用 props 傳遞配置。陷阱排查以下代碼存在什么問題!-- 子組件 -- script setup const props defineProps{ value: string }() function clear() { props.value } /script !-- 父組件 -- template Child :valueparentData / /template深度思考為什么 Vue 要強(qiáng)制單向數(shù)據(jù)流如果允許子組件直接修改父組件的 props會(huì)帶來哪些問題下篇預(yù)告第 8 篇我們將全面深入地學(xué)習(xí)組件通信包括Props 和 Emit 的標(biāo)準(zhǔn)通信模式使用v-model在組件間雙向綁定.sync修飾符的替代方案復(fù)雜場景下的組件通信策略準(zhǔn)備好掌握組件間高效協(xié)作的能力