——從注冊到使用的完整流程)
本篇定位組件化是 Vue 最核心的開發(fā)范式也是構(gòu)建大型應用的基礎(chǔ)。本篇將帶你從零開始理解 Vue 組件的本質(zhì)、注冊方式、Props 數(shù)據(jù)傳遞和類型校驗。讀完本篇你將能夠獨立創(chuàng)建和復用 Vue 組件并理解組件間數(shù)據(jù)流動的基本原則。一、什么是組件1.1 組件的本質(zhì)Vue 組件是可復用的 Vue 實例它擁有獨立的模板、邏輯和樣式。從本質(zhì)上講組件就是一個帶有特定行為和樣式的 UI 片段??梢赃@樣理解一個 Vue 應用就像一棵組件樹根組件App.vue是樹根其他組件是其枝葉每個組件都是一個獨立的小世界封裝了自己的 HTML、CSS 和 JavaScriptApp.vue (根組件) ├── Header.vue │ ├── Logo.vue │ └── NavMenu.vue ├── Sidebar.vue │ └── UserProfile.vue └── Footer.vue1.2 為什么要使用組件優(yōu)勢說明復用性同一個組件可以在多個地方使用避免重復造輪子可維護性每個組件職責單一易于定位和修復問題可測試性獨立組件可以單獨進行單元測試協(xié)作開發(fā)不同開發(fā)者可以并行開發(fā)不同組件減少沖突關(guān)注點分離模板、邏輯和樣式集中管理清晰明了1.3 組件的兩種編寫風格Vue 3 支持兩種組件編寫風格風格特點適用場景選項式 APIOptions API使用data、methods、computed等選項Vue 2 用戶過渡或偏好傳統(tǒng)風格組合式 APIComposition API使用setup和script setupVue 3 推薦邏輯復用更優(yōu)雅本系列教程將默認使用組合式 API 的script setup語法這是 Vue 3 的官方推薦寫法。二、單文件組件SFC的完整結(jié)構(gòu)Vue 的單文件組件Single File Component簡稱 SFC使用.vue擴展名一個文件包含三個部分script setup langts // 1. 邏輯部分JavaScript/TypeScript // 響應式數(shù)據(jù)、方法、生命周期等 /script template !-- 2. 模板部分HTML -- !-- 描述組件的 UI 結(jié)構(gòu) -- /template style scoped /* 3. 樣式部分CSS */ /* scoped 表示樣式僅作用于當前組件 */ /style2.1script setup是 Vue 3.2 引入的編譯時語法糖它極大地簡化了組件的編寫script setup langts // 導入的組件自動注冊無需手動注冊 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點擊次數(shù){{ count }}/MyButton /div /templatescript setup的核心特性頂層綁定自動暴露所有變量、函數(shù)、導入的組件都可以直接在模板中使用更簡潔的代碼不需要export default、setup()函數(shù)的返回值等更好的運行時性能編譯時優(yōu)化減少運行時代碼更好的 TypeScript 支持類型推導更準確2.2 模板模板部分使用增強的 HTML 語法支持 Vue 的指令和插值template !-- 根元素可以是多個Vue 3 支持多根節(jié)點 -- h1{{ title }}/h1 p v-ifvisible可見內(nèi)容/p button clickhandleClick點擊/button slot/slot /templateVue 3 支持多根節(jié)點Fragment不再需要包裹一個額外的父元素。2.3scoped屬性讓樣式僅在當前組件中生效style scoped /* 這個類名只在這個組件中生效 */ .title { color: blue; } /style原理Vue 會為組件中的每個元素添加一個唯一屬性如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中直接導入并使用組件自動注冊!-- Parent.vue -- script setup // 導入組件自動注冊 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 全局注冊在應用入口main.ts中注冊全局組件可以在任何子組件中使用無需再次導入// main.tsimport{createApp}fromvueimportAppfrom./App.vueimportGlobalButtonfrom./components/GlobalButton.vueconstappcreateApp(App)// 全局注冊app.component(GlobalButton,GlobalButton)app.mount(#app)然后在任意組件中直接使用template GlobalButton / /template全局注冊的優(yōu)缺點優(yōu)點缺點無需在組件中反復導入即使未使用也會被打包增加最終包體積適合基礎(chǔ)組件如按鈕、輸入框依賴關(guān)系不明確不利于維護一次注冊到處可用可能造成命名沖突最佳實踐優(yōu)先使用局部注冊僅在確實需要全局可用如 UI 基礎(chǔ)組件庫時使用全局注冊。3.3 組件命名規(guī)范場景推薦命名示例組件文件名PascalCase大駝峰MyComponent.vue模板中使用PascalCase 或 kebab-case短橫線MyComponent或my-component全局注冊名稱PascalCase 或 kebab-caseapp.component(MyButton, ...)Vue 官方推薦在模板中使用PascalCase因為它更容易與原生 HTML 元素區(qū)分。四、Props父傳子的數(shù)據(jù)傳遞Props屬性是父組件向子組件傳遞數(shù)據(jù)的主要方式。Props 遵循單向數(shù)據(jù)流原則父級 props 的更新會流向子組件反之則不行。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:綁定動態(tài)值靜態(tài)值直接寫屬性名字符串也可以不用:titleHello等價于:titleHello布爾屬性不帶值默認為trueis-active等價于:is-activetrue4.2 類型聲明與默認值在script setup中使用類型聲明定義 propsscript setup langts // 基本類型聲明 defineProps{ name: string age: number isAdmin: boolean hobbies: string[] address?: { city: string; street: string } // 可選屬性? }() /script如果需要默認值需要使用withDefaults編譯宏script setup langts interface Props { title: string count?: number items?: string[] status?: pending | success | error } // 為可選屬性設(shè)置默認值 const props withDefaults(definePropsProps(), { count: 0, items: () [默認項1, 默認項2], // 對象/數(shù)組需使用工廠函數(shù) status: pending }) /script注意withDefaults是 Vue 的編譯宏不需要導入直接在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 的校驗規(guī)則Vue 提供了豐富的 prop 校驗能力script setup langts // 使用運行時聲明而非僅類型聲明支持校驗 defineProps({ // 基礎(chǔ)類型 propA: Number, // 多種類型 propB: [String, Number], // 必填 propC: { type: String, required: true }, // 默認值 propD: { type: Number, default: 100 }, // 對象默認值 propE: { type: Object, default: () ({ name: 默認 }) }, // 自定義校驗 propF: { type: String, validator: (value: string) [small, medium, large].includes(value) } }) /scriptTypeScript 類型聲明 vs 運行時聲明方式優(yōu)點缺點TS 類型聲明類型安全、IDE 提示好無法在運行時校驗和提供默認值運行時聲明支持校驗、默認值缺乏 TS 類型推斷可以結(jié)合使用最佳實踐同時使用兩者發(fā)揮各自優(yōu)勢用 TS 類型提供靜態(tài)類型用運行時聲明提供校驗和默認值。script setup langts interface Props { size?: small | medium | large count?: number } // 運行時聲明 TS 類型 const props withDefaults(definePropsProps(), { size: medium, count: 0 }) /script4.5 單向數(shù)據(jù)流原則Props 是只讀的子組件不能直接修改父組件傳遞的 propscript setup // ? 錯誤直接修改 prop const props defineProps{ count: number }() function increment() { props.count // ? 會觸發(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②使用計算屬性轉(zhuǎn)換為本地數(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 會自動轉(zhuǎn)換!-- 父組件兩種寫法都支持 -- Child :userNamename :user-ageage /在 JavaScript 中始終使用camelCasedefineProps{userName:stringuserAge:number}()五、組件實例與 props 的關(guān)系5.1 每個組件擁有獨立的狀態(tài)當你多次使用同一個組件時每個實例都有獨立的數(shù)據(jù)作用域!-- 父組件 -- template Counter / Counter / Counter / /template每個Counter實例都有自己獨立的count狀態(tài)互不影響。5.2 傳遞非字符串類型傳遞非字符串類型時必須使用v-bindtemplate !-- 字符串 -- Child name張三 / !-- 數(shù)字必須用 v-bind -- Child :age25 / !-- 布爾值 -- Child :is-activetrue / !-- 數(shù)組 -- Child :hobbies[閱讀, 編程] / !-- 對象 -- Child :user{ name: 李四, age: 30 } / !-- 方法很少用 -- Child :callbackhandleCallback / /template5.3 動態(tài)傳遞多個 props對象展開如果有一個包含多個屬性的對象可以使用v-bind的對象展開一次性傳遞script setup const user { name: 王五, age: 28, email: wangwuexample.com } /script template !-- 等價于傳遞 :nameuser.name :ageuser.age :emailuser.email -- UserCard v-binduser / /template六、實戰(zhàn)構(gòu)建一個卡片組件庫我們通過一個完整的卡片組件示例綜合運用本章所學知識!-- 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 }) // 計算樣式類 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 } ] !-- 標題區(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這是一個基礎(chǔ)的卡片組件展示內(nèi)容區(qū)域。/p /Card !-- 帶變體的卡片 -- Card title成功提示 subtitle操作已成功完成 variantsuccess :borderedtrue p您的數(shù)據(jù)已保存。/p template #footer button clickhandleAction確認/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價格¥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(已確認) } /script七、本篇小結(jié)知識點核心內(nèi)容關(guān)鍵要點組件本質(zhì)可復用的 Vue 實例擁有獨立模板、邏輯、樣式組件樹構(gòu)成應用單文件組件.vue文件包含script setup、template、style scopedscript setup是 Vue 3 推薦寫法局部注冊在script setup中import后直接使用優(yōu)先推薦按需加載全局注冊app.component()全局注冊適用于基礎(chǔ) UI 組件庫Props 定義defineProps{ name: type }()使用 TS 類型聲明Props 默認值withDefaults(definePropsProps(), { ... })對象/數(shù)組需用工廠函數(shù)單向數(shù)據(jù)流子組件不能直接修改 props通過事件通知父組件修改Props 校驗運行時聲明支持validator等可結(jié)合 TS 類型和運行時校驗八、思考與練習基礎(chǔ)練習創(chuàng)建一個Avatar組件接收src、sizesmall/medium/large、alt等 props并顯示不同尺寸的頭像。實踐任務將上一節(jié)注冊表單中的部分內(nèi)容如輸入框組、表單驗證錯誤提示抽取為可復用的子組件使用 props 傳遞配置。陷阱排查以下代碼存在什么問題!-- 子組件 -- script setup const props defineProps{ value: string }() function clear() { props.value } /script !-- 父組件 -- template Child :valueparentData / /template深度思考為什么 Vue 要強制單向數(shù)據(jù)流如果允許子組件直接修改父組件的 props會帶來哪些問題下篇預告第 8 篇我們將全面深入地學習組件通信包括Props 和 Emit 的標準通信模式使用v-model在組件間雙向綁定.sync修飾符的替代方案復雜場景下的組件通信策略準備好掌握組件間高效協(xié)作的能力