VUE2中文文檔:深入組件(上)
組件註冊
組件名稱
命名方案
可以通過兩種可選方式,定義組件名稱:
串聯式命名(kebab-case)
Pascal 式命名(PascalCase)使用串聯式命名(kebab-case)定義一個組件,在引用其自定義元素時,你必須也使用串聯式命名(kebab-case),例如 <my-component-name>
。
使用 Pascal 式命名(PascalCase)定義一個組件,在引用其自定義元素時,兩種方式都可以使用。也就是說 <my-component-name>
和 <MyComponentName>
都是可以接受的。然而要註意,直接在 DOM 中(即,非字符串模板)引用自定義元素,串聯式命名(kebab-case)是唯一有效的命名方式。
全局註冊方式(global registration)
Vue.component(‘my-component-name‘, { // ... options ... })
這些組件都是全局註冊。也就是說,全局註冊的組件,可以在之後(通過 new Vue
)創建的 Vue 根實例的模板中引用。
這甚至可以應用於所有子組件,這意味著,這三個組件還可以在每個其他組件內部使用
局部註冊方式(local registration)
如果你使用一個類似 webpack 的模塊構建系統,全局註冊所有組件,意味著就算你不引用某個組件,它仍然會打包到最終的構建 bundle 中。這會增加 JavaScript 的體積,讓用戶下載多余的代碼。
在下面這些示例中,可以將你的組件定義為純 JavaScript 對象:
var ComponentA = { /* ... */ } var ComponentB = { /* ... */ } var ComponentC = { /* ... */ }
然後,在 components
選項中,定義你需要用到的組件:
new Vue({ el: ‘#app‘ components: { ‘component-a‘: ComponentA, ‘component-b‘: ComponentB } })
對於 components
對象的每個屬性,對象的 key 是自定義元素的名稱,而 value 包含著組件的選項對象。
註意,局部註冊的組件在子組件中無法訪問。
如果使用 ES2015 模塊(例如,通過 Babel 和 webpack 進行轉譯),則看起來可能類似這樣:
import ComponentA from ‘./ComponentA.vue‘ export default { components: { ComponentA }, // ... }
在 ES2015+ 中,在一個對象放置類似 ComponentA
這樣的變量名稱,其實是 ComponentA: ComponentA
的簡寫形式,
模塊系統
在模塊系統中的局部註冊方式
你可能正在使用一個模塊系統(例如,通過 Babel 和 webpack 進行轉譯)。在這種場景中,我們推薦你創建一個 components
目錄,每個組件中都定義在文件中。
然後,在局部註冊這些組件之前,你需要預先導入每個需要用到的組件。例如,在假想的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from ‘./ComponentA‘ import ComponentC from ‘./ComponentC‘ export default { components: { ComponentA, ComponentC }, // ... }
自動化全局註冊基本組件
多相對通用的組件。我們有時將這些組件歸為 基礎組件,並且往往在其他組件中頻繁使用這類組件。
結果就是,許多組件可能會列出一個很長的基礎組件清單,然後在 components 選項中進行逐個引用。
幸運的是,如果你正在使用 webpack(或者內置 webpack 的 Vue CLI 3+),你就可以只通過 require.context
來全局註冊這些常用基礎組件。在你的應用程序入口文件(例如 src/main.js
)中,你可能會通過全局方式導入基礎組件,下面是一些示例代碼:
import Vue from ‘vue‘ import upperFirst from ‘lodash/upperFirst‘ import camelCase from ‘lodash/camelCase‘ const requireComponent = require.context( //匹配的你想導入的目標組件們 // components 文件夾的相對路徑 ‘./components‘, // 是否查找子文件夾 false, // 用於匹配組件文件名的正則表達式 /Base[A-Z]\w+\.(vue|js)$/ ) requireComponent.keys().forEach(fileName => { //取得它們的pascal命名 // 獲取組件配置 const componentConfig = requireComponent(fileName) // 取得組件的 Pascal 式命名 const componentName = upperFirst( camelCase( // 將文件名前面的 `‘./` 和擴展名剝離 fileName.replace(/^\.\/(.*)\.\w+$/, ‘$1‘) ) ) // 以全局方式註冊組件 Vue.component( //全局註冊 componentName, // 如果組件是通過 `export default` 導出, // 則在 `.default` 中,查找組件選項, // 否則回退至模塊根對象中,查找組件選項 componentConfig.default || componentConfig ) })
props
prop 命名方案(駝峰式和串聯式)
Vue.component(‘blog-post‘, { // 在 JavaScript 中使用駝峰式(camelCase) props: [‘postTitle‘], template: ‘<h3>{{ postTitle }}</h3>‘ }) <!-- 在 HTML 中使用串聯式(kebab-case) --> <blog-post post-title="hello!"></blog-post>
再次申明,如果是在使用字符串模板的場景,則沒有這些限制。
靜態 props 和動態 props
可以通過 v-bind
給 props 分配動態值,就像這樣:
<blog-post v-bind:title="post.title"></blog-post>
我們傳遞字符串值,然而,實際上可以給一個 prop 傳遞任意類型的值。
傳遞一個 Number 類型值
傳遞一個 Boolean 類型值
傳遞一個 Array 類型值
傳遞一個 Object 類型值
<!-- object 是靜態的,這就需要我們使用 v-bind, --> <!-- 來告訴 Vue 它是以 JavaScript 表達式表現,而不是一個字符串 --> <blog-post v-bind:comments="{ id: 1, title: ‘我的 Vue 旅程‘ }"></blog-post> <!-- 將一個變量,動態地分配到屬性值上 --> <blog-post v-bind:post="post"></blog-post>
傳遞一個對象的所有屬性
如果你想要向 props 傳遞一個對象所有屬性,你可以使用不帶參數的 v-bind
(即 v-bind
來替換 v-bind:prop-name
)。
單向數據流
所有 props 都在子組件和父組件之間形成一個單向往下流動的數據綁定:當父組件中的屬性更新時,數據就會向下流動到子組件,但是反過來,子組件屬性更新時,父組件並不會感知到子組件的數據變化。這種機制可以防止子組件意外地修改了父組件的狀態,造成應用程序的數據流動變得難於理解。
此外,每次父組件更新時,子組件中所有的 props 都會更新為最新值。也就是說,你不應該試圖在子組件內部修改 prop。如果你這麽做,Vue 就會在控制臺給出警告。
誘使我們修改 prop 的原因,通常有兩種:
1.prop 用於傳遞初始值(initial value);之後子組件需要將 prop 轉為一個局部數據屬性。在這種情況中,最好定義一個局部的 data 屬性,然後將 prop 的值,作為局部屬性初始值。
props: [‘initialCounter‘], data: function () { return { counter: this.initialCounter } }
2.prop 用於傳遞一個需要轉換的未加工值(raw value)。在這種情況中,最好預先定義一個 computed 屬性,然後在其函數內部引用 prop 的值:
props: [‘size‘], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
註意,JavaScript 中的對象和數組都是通過引用(reference)傳遞的,因此,如果 prop 是一個數組或對象,則在子組件內部改變對象或數組本身,仍然會影響到父組件狀態。
prop 驗證
你需要將 props
的值定義為一個帶有驗證接收條件的對象,而不是一個由字符串構成的數組。
Vue.component(‘my-component‘, { props: { // 基本類型(base type)的檢查(`null` 表示接受所有類型) propA: Number, // 多種可能的類型 propB: [String, Number], // 必須傳遞,且 String 類型 propC: { type: String, required: true }, // Number 類型,有一個默認值 propD: { type: Number, default: 100 }, // Object 類型,有一個默認值 propE: { type: Object, // Object/Array 類型, // 默認必須返回一個工廠函數 default: function () { return { message: ‘hello‘ } } }, // 自定義驗證函數 propF: { validator: function (value) { // 值必須是這些字符串中的一個 return [‘success‘, ‘warning‘, ‘danger‘].indexOf(value) !== -1 } } } })
當 prop 驗證失敗,(如果使用的是開發構建版本,)Vue 就會在控制臺拋出警告。
註意,props 會在組件實例創建之前進行驗證,因此在 default
或 validator
這些驗證函數中,還無法訪問到實例上的屬性(像 data
, computed
這些)。
類型檢查
type
可以是以下原生構造函數之一:
- String
- Number
- Boolean
- Function
- Object
- Array
- Symbol
除了以上這些,type
還可以是一個自定義構造函數,通過 instanceof
對 props 值進行類型推斷。
function Person (firstName, lastName) { this.firstName = firstName this.lastName = lastName } //你可以這樣做類型推斷:以驗證author
prop 的值,是由new Person
創建出來的。 Vue.component(‘blog-post‘, { props: { author: Person } })
非 prop 特性(non-prop attributes)
例如,假想我們使用一個第三方 bootstrap-date-input
組件,其內部引用一個 BootStrap 插件,現在需要我們向組件內的 input
元素傳入一個 data-date-picker
特性。我們可以在組件實例上添加這個特性:
<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>
然後,data-date-picker="activated"
特性就會被自動添加到 bootstrap-date-input
組件的根元素上。
替換/合並現有的特性(replacing/merging with existing attributes)
對於大多數特性,傳給組件的值將會替換掉組件自身設置的值。例如,向組件傳入 type="text"
,將會替換掉組件自身設置的 type=”date”,這就很可能破壞組件的一些預設功能!幸運的是,class
和 style
特性會略微智能,這兩個值會被合並而非替換,而最終的值是:form-control date-picker-theme-dark
。
禁用特性繼承(disabling attribute inheritance)
如果你不希望組件根元素從父實例中繼承特性(attribute),你可以在組件選項中設置 inheritAttrs: false
來禁用特性繼承。
這對於通過子組件 $attrs
實例屬性,合並父實例的特性來說尤其有用,其中 $attrs 對象包含父實例傳入到一個組件的特性名稱和特性值,類似這樣:
{ class: ‘username-input‘, placeholder: ‘Enter your username‘ }
通過 inheritAttrs: false
和 $attrs
,你可以手動決定將特性,傳送到具體的某個元素,對於 基本組件 來說,這是符合需求的功能:
Vue.component(‘base-input‘, { inheritAttrs: false, props: [‘label‘, ‘value‘], template: ` <label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit(‘input‘, $event.target.value)" > </label> ` }) //這種方式允許你像使用原始 HTML 元素那樣去使用基本組件,而不必關心組件根元素是哪個元素: <base-input v-model="username" class="username-input" placeholder="Enter your username" ></base-input>
自定義事件
事件名稱(event names)
與 components 和 props 不同,事件名稱並不提供命名自動轉換,事件名稱永遠不會用作 JavaScript 變量或屬性名稱,所以沒有理由去使用駝峰式命名(camelCase)或帕斯卡命名(PascalCase)。DOM 模板中的 v-on
事件監聽器會自動轉換為小寫(這是因為 HTML 屬性名稱不區分大小寫)。
由於這些原因,我們建議你總是使用串聯式命名(kebab-cased)來命名事件名稱。
定制組件 v-model
在一個組件中,v-model
默認使用 value
作為 prop,以及默認使用 input
作為監聽事件,然而,對於某些類型的 input 元素(例如 checkbox 和 radio),由於這些類型的 input 元素本身具有 不同用法,可能會占用 value
特性。在這種情況下,使用組件的 model
選項可以避免沖突:
Vue.component(‘base-checkbox‘, { model: { prop: ‘checked‘, //將prop更改為checked event: ‘change‘ //將監聽事件改為change }, props: { checked: Boolean //?? }, template: ` <input type="checkbox" v-bind:checked="checked" //綁定checked作為prop v-on:change="$emit(‘change‘, $event.target.checked)" //將checked傳給監聽事件change > ` })
<base-checkbox v-model="lovingVue"></base-checkbox>
lovingVue
的值就會傳遞給 checked
prop。當 <base-checkbox>
內部觸發一個 change
事件,並且傳遞一個新值,lovingVue
屬性就會進行更新。
為組件綁定本地事件(binding native events to components)
有時候,你可能希望某個組件的根元素能夠直接監聽到組件所處位置的本地事件。在這種場景中,你可以在 v-on
上使用 .native
修飾符:
<base-input v-on:focus.native="onFocus"></base-input>
有時綁定本地事件會很有用,但是註意,如果你試圖監聽一個非常特殊的元素(例如 <input>
元素),則不是正確用法。。舉例說明,上面的 <base-input>
組件或許會進行重構,因此其根元素可能實際上是一個 <label>
元素:
<label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit(‘input‘, $event.target.value)" > </label>
為了解決這個問題,Vue 提供了一個 $listeners
屬性,它是包含組件中所有監聽器的對象
你可以將所有父組件中由 v-on="$listeners"
綁定的監聽器,通過 $listeners
屬性轉發到子組件的特定元素。
對於像 <input>
這樣的元素,你還需要實現 v-model
機制,通常會創建返回一個新的 listeners 對象的 computed 屬性,類似如下 inputListeners
:
Vue.component(‘base-input‘, { inheritAttrs: false, props: [‘label‘, ‘value‘], computed: { inputListeners: function () { var vm = this // `Object.assign` 將這些對象合並在一起,構成一個新的對象 return Object.assign({}, // 我們在父組件中添加的所有監聽器 this.$listeners, // 然後我們可以新增自定義的監聽器, // 或覆蓋掉一些監聽器的行為。 { // 這裏確保組件能夠正常運行 v-model 指令 input: function (event) { vm.$emit(‘input‘, event.target.value) } } ) } }, template: ` <label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on="inputListeners" > </label> ` })
現在,<base-input>
組件是一個毫無疑惑的容器組件(fully transparent wrapper)了,也就是說,可以像使用一個普通的 <input>
元素一樣去使用它:所有的特性和監聽器,都能夠如同普通 input 元素一樣正常運行。
.sync
修飾符
通過子組件觸發 update:my-prop-name
事件的方式,來更新父組件的狀態。例如,在一個接收 title
prop 的假想組件中,我們想要分配一個子組件的新值給父組件的意圖,可以通過通信機制來實現:
this.$emit(‘update:title‘, newTitle) //父組件可以監聽到這個事件,並且(如果需要的話)更新一個本地數據屬性。例如: <text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document>
為了簡便,我們使用 .sync
修飾符,提供了這個模式的簡寫:
<text-document v-bind:title.sync="doc.title"></text-document>
//.sync 修飾符還可以用於 v-bind,使用一個對象一次性設置多個 prop: <text-document v-bind.sync="doc"></text-document>‘ //這會傳遞 doc 對象中的每個屬性(例如 title),作為單獨的 prop,然後為每個屬性添加相應的 v-on:update 監聽器。
對一個字面量對象使用 v-bind.sync
(例如 v-bind.sync=”{ title: doc.title }”
),則不會正常運行,因為在解析像這種復雜表達式時,需要考慮太多的邊界情況。
slot
插槽內容(slot content)
//內容分發機制,可以幫助你以如下方式構成組件: <navigation-link url="/profile"> Your Profile </navigation-link>
//然後,在 <navigation-link> 模板中,可能是: <a v-bind:href="url" class="nav-link" > <slot></slot> </a>
//在組件渲染時,<slot> 元素就會被替換為 “Your Profile”。在 slot 位置,可以包含任何模板代碼,也包括 HTML: <navigation-link url="/profile"> <!-- 添加一個 Font Awesome 圖標 --> <span class="fa fa-user"></span> Your Profile </navigation-link>
//甚至,slot 位置也能包含其他組件: <navigation-link url="/profile"> <!-- 使用一個組件添加一個圖標 --> <font-awesome-icon name="user"></font-awesome-icon> Your Profile </navigation-link>
//如果 <navigation-link> 完全沒有 <slot> 元素,則 slot 位置傳遞的所有內容都會被直接丟棄。
命名插槽(named slot)
在某些場景中,需要用到多個插槽。對於這種場景,<slot>
元素有一個特殊的 name
特性,可以用於定義除默認插槽以外的多余插槽:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
為了給命名插槽提供內容,我們可以在父組件模板的 <template>
元素上使用 slot
特性
<base-layout> <template slot="header"> <h1>這裏是一個頁面標題</h1> </template> <p>main 內容的一個段落。</p> <p>main 內容的另一個段落。</p> <template slot="footer"> <p>這裏是一些聯系信息</p> </template> </base-layout>
//或者,也可以對某個普通元素,直接使用 slot 特性: <base-layout> <h1 slot="header">這裏是一個頁面標題</h1> <p>main 內容的一個段落。</p> <p>main 內容的另一個段落。</p> <p slot="footer">這裏是一些聯系信息</p> </base-layout>
還有一個未命名插槽(unnamed slot),這是默認插槽,它是用於放置所有不匹配內容的插槽位置。在以上這兩個示例中,最終渲染的 HTML 是:
<div class="container"> <header> <h1>這裏是一個頁面標題</h1> </header> <main> <p>main 內容的一個段落。</p> <p>main 內容的另一個段落。</p> </main> <footer> <p>這裏是一些聯系信息</p> </footer> </div>
默認插槽內容(default slot content)
<button type="submit"> <slot>Submit</slot> </button>
如果父組件模板中,向 slot 位置提供了內容,子組件 slot 元素的默認內容就會被替換。
編譯時的作用域(compilation scope)
父組件模板的內容,全部在父組件作用域內編譯;子組件模板的內容,全部在子組件作用域內編譯。
作用域插槽(scoped slots)
在某些場景中,需要提供一個具有「可以訪問組件內部數據的可復用插槽(reusable slot)」的組件。
//一個簡單的<todo-list>
組件
<ul> <li v-for="todo in todos" v-bind:key="todo.id" > {{ todo.text }} </li> </ul>
但是在我們應用程序的某些部分中,我們想要將 todo items 中的每一項,都渲染為不同於 todo.text
的內容。
為了實現此潛在功能,我們必須將 todo item 的內容,包裹到一個 <slot>
元素中,然後,將此 slot 內部所需的所有相關數據,都傳遞給它的上下文環境(context)
<ul> <li v-for="todo in todos" v-bind:key="todo.id" > <!-- 我們為每個 todo 提供一個 slot 元素, --> <!-- 然後,將 `todo` 對象作為 slot 元素的一個 prop 傳入。 --> <slot v-bind:todo="todo"> <!-- 這裏是回退內容(fallback content) --> {{ todo.text }} </slot> </li> </ul>
現在,在我們引用 <todo-list>
組件的位置,我們可以將 todo items 插槽內容稍作修改,定義為一個 <template>
,並且通過 slot-scope
特性訪問子組件數據:
<todo-list v-bind:todos="todos"> <!-- 將 `slotProps` 作為插槽內容所在作用域(slot scope)的引用名稱 --> <template slot-scope="slotProps"> <!-- 為 todo items 定義一個模板, --> <!-- 通過 `slotProps` 訪問每個 todo 對象。 --> <span v-if="slotProps.todo.isComplete">?</span> {{ slotProps.todo.text }} </template> </todo-list>
在 2.5.0+,slot-scope
不再局限於 <template>
元素,而是可以在任何元素或任何組件中的插槽內容上使用。
解構 slot-scope
(不理解)
slot-scope
的值,實際上可以接收任何有效的 JavaScript 表達式,可以出現在函數定義中的參數所在位置。也就是說,在支持的環境中(在 單個文件組件 或在 現代瀏覽器),可以使用 ES2015 解構 來對表達式進行解構,就像這樣:
<todo-list v-bind:todos="todos"> <template slot-scope="{ todo }"> <span v-if="todo.isComplete">?</span> {{ todo.text }} </template> </todo-list>
VUE2中文文檔:深入組件(上)