原文:intro-to-vue-2-components-props-slots
譯者:nzbin
這是關於 JavaScript 框架 Vue.js 五個教程的第二部分。在這一部分,我們將學習組件,Props 以及 Slots。這個系列教程並不是一個完整的用戶手冊,而是通過基礎知識讓你快速了解 Vuejs 以及它的用途。
系列文章:
- 渲染, 指令, 事件
- 組件, Props, Slots (你在這!)
- Vue-cli
- Vuex
- 動畫
組件和傳遞數據
如果你熟悉 React 或者 Angular2,組件思想和傳遞狀態對你並不陌生。如果不是, 讓我們先了解一些主要概念。
大小網站通常由不同的部分組成,並且抽象成更小的部分更容易布局、重用、並使得我們的代碼更清晰。為了避免在冗長的多層次的頁面中搜尋標簽,我們可以這樣構建組件:
<header></header> <aside> <sidebar-item v-for="item in items"></sidebar-item> </aside> <main> <blogpost v-for="post in posts"></blogpost> </main> <footer></footer>
這是一個簡單的例子,但是你可以看到這種組合方式在開始構建網站結構時的用途。如果你要維護這些代碼,你可以很容易的了解程序的結構並且找到每一部分。
Vue 有多種創建組件的方式。讓我們從易到難,而復雜的例子就是一個普通的 Vue 程序。
app.$mount('#app'); var app = new Vue({ el: 'hello', template: '<h1>Hello World!</h1>' });
代碼正常運行,但用處不大,因為它只能使用一次,我們還沒有向不同的組件傳遞信息。從父組件向子組件傳遞數據的方式稱為 props。
下面是我能做的最簡單的例子,所以非常容易理解。記住 HTML 中的 :text
是 Vue 綁定的縮寫。我們在指令部分的最后提到過。綁定可以用於所有方面,但是在這個實例中,這樣做的好處是不需要把狀態放在 mustache 模板中, 比如 {{ message }}
。
在下面的代碼中,Vue.component
是組件, new Vue
稱為實例。一個程序中可以有多個實例。通常情況下,我們會有一個實例和多個組件,因為實例是主要應用程序。
Vue.component('child', { props: ['text'], template: `<div>{{ text }}<div>` }); new Vue({ el: "#app", data() { return { message: 'hello mr. magoo' } } });
<div id="app"> <child :text="message"></child> </div>
See the Pen simple props by Sarah Drasner (@sdras) on CodePen.
現在我們可以在程序中隨意使用這個組件:
<div id="app"> <child :text="message"></child> <child :text="message"></child> </div>
See the Pen simple props by Sarah Drasner (@sdras) on CodePen.
我們也可以向 props 中添加驗證,這和 React 中的 PropTypes
類似。這個功能很好,因為它是自描述的,並且如果與我們的期望值不同會返回錯誤,但只有在開發模式中才顯示 :
Vue.component('child', { props: { text: { type: String, required: true } }, template: `<div>{{ text }}<div>` });
在下面的例子中,我在開發模式中加載 Vue ,並且故意在 prop 驗證中輸入一個非法類型。你可以看到控制台報錯。(這非常有幫助,你可以使用 Vue 的開發工具發現錯誤).
Vue.component('child', { props: { text: { type: Boolean, required: true } }, template: `<div>{{ text }}<div>` });
See the Pen simple props with validation by Sarah Drasner (@sdras) on CodePen.
對象應該作為一個工廠函數返回,你也可以傳遞一個自定義驗證函數,這非常有用,因為可以檢查不符合業務、輸入或者其它邏輯的數值。對於如何使用每一種類型,有一篇寫的很好的 指南.
沒有必要在在 props 中給子組件傳遞數據,也可以使用狀態或靜態值:
Vue.component('child', { props: { count: { type: Number, required: true } }, template: `<div class="num">{{ count }}</div>` }) new Vue({ el: '#app', data() { return { count: 0 } }, methods: { increment() { this.count++; }, decrement() { this.count--; } } })
<div id="app"> <h3> <button @click="increment">+</button> Adjust the state <button @click="decrement">-</button> </h3> <h2>This is the app state: <span class="num">{{ count }}</span></h2> <hr> <h4><child count="1"></child></h4> <p>This is a child counter that is using a static integer as props</p> <hr> <h4><child :count="count"></child></h4> <p>This is the same child counter and it is using the state as props</p> </div>
See the Pen child component using and not using props by Sarah Drasner (@sdras) on CodePen.
區別在於你是否傳遞了一個屬性並綁定它:
沒有使用狀態
<child count="1"></child>
vs
使用狀態
<child :count="count"></child>
到現在為止,我們已經用字符串在子組件中創建了內容,如果使用 babel 的話,你可以在所有瀏覽器中使用 ES6 (我強烈建議),可以使用 模板字面量 來避免難以閱讀的拼接字符串:
Vue.component('individual-comment', { template: `<li> {{ commentpost }} </li>`, props: ['commentpost'] }); new Vue({ el: '#app', data: { newComment: '', comments: [ 'Looks great Julianne!', 'I love the sea', 'Where are you at?' ] }, methods: { addComment: function () { this.comments.push(this.newComment) this.newComment = '' } } });
<ul> <li is="individual-comment" v-for="comment in comments" v-bind:commentpost="comment" ></li> </ul> <input v-model="newComment" v-on:keyup.enter="addComment" placeholder="Add a comment" >
See the Pen rjwJdY by Sarah Drasner (@sdras) on CodePen.
雖然有些作用,但是字符串中的內容仍然有限制。最后,在這個評論列表中,我們希望有照片和作者的名字,你可能已經猜到過多的信息會非常擁擠。而字符串中沒有語法高亮效果。
考慮到所有這些事情,我們將創建一個模板。我們會用特殊的 script 標簽包裹常規的HTML,然后使用 id 引用它來創建一個組件。當文本和元素很多的時候,這種方式更清晰:
<!-- This is the Individual Comment Component --> <script type="text/x-template" id="comment-template"> <li> <img class="post-img" :src="commentpost.authorImg" /> <small>{{ commentpost.author }}</small> <p class="post-comment">"{{ commentpost.text }}"</p> </li> </script>
Vue.component('individual-comment', { template: '#comment-template', props: ['commentpost'] });
See the Pen Photo App post with Vue.js by Sarah Drasner (@sdras) on CodePen.
Slots
這樣好多了。但是如果兩個組件的內容或者樣式略有不同時會怎樣?我們可能會通過 props 將所有不同的內容及樣式傳遞到組件,每次切換所有的東西,或者我們可以復制組件並創建不同的版本。但是如果可以重用組件,並用相同的數據或功能填充它們,那就太好了。這就是 slots 的有用之處。
假如我們有一個程序實例,使用相同的組件 <app-child>
兩次。在每個子組件內部,我們需要一些相同的內容以及不同的內容。對於要保持一致的內容,我們使用一個標准的 p 標簽,而對於要切換的內容,我們放在空的 <slot></slot>
標簽中。
<script type="text/x-template" id="childarea"> <div class="child"> <slot></slot> <p>It's a veritable slot machine!<br> Ha ha aw</p> </div> </script>
然后在程序實例中,我們可以在在 <app-child>
組件標簽中傳遞內容,它會自動填充到 slots 中:
<div id="app"> <h2>We can use slots to populate content</h2> <app-child> <h3>This is slot number one</h3> </app-child> <app-child> <h3>This is slot number two</h3> <small>I can put more info in, too!</small> </app-child> </div>
See the Pen Slots Example by Sarah Drasner (@sdras) on CodePen.
slots 中也可以有默認內容。如果要在 slot 中寫內容,而不是寫 <slot></slot>
,你可以這樣填充:
<slot>I am some default text</slot>
如果你沒有在 slot 中填充其它內容,就會顯示默認文本,這是非常有用的!鼓掌吧。
你也可以使用具名 slot 。如果一個組件中有兩個 slot, 可以通過添加 name 屬性區分它們 <slot name="headerinfo"></slot>
,並且通過特定的名稱訪問 slot <h1 slot="headerinfo">I will populate the headerinfo slot!</h1>
。這非常有用。如果有多個命名的 slot 而有一個沒有命名,Vue 命名的內容填充到命名的 slot 中,而剩余的內容將填充到未命名的 slots 中。
請看以下示例:
子組件模板
<div id="post"> <main> <slot name="header"></slot> <slot></slot> </main> </div>
父組件模板
<app-post> <h1 slot="header">This is the main title</h1> <p>I will go in the unnamed slot!</p> </app-post>
渲染結果
<main> <h1>This is the main title</h1> <p>I will go in the unnamed slot!</p> </main>
就我個人而言,如果我一次使用多個 slot,我會將所有的都命名,這對於其他的維護人員來說非常清晰,但 Vue 提供的這個靈活的 API 也很好。
Slot 示例
另外,我們給不同的組件設置特殊的樣式,並保持所有的內容相同,因此可以迅速和容易地改變了外觀。在下面的葡萄酒標簽制造商中,其中一個按鈕將根據用戶的選擇切換組件和顏色,酒瓶背景、標簽和文本將全部切換,同時保持內容不變。
const app = new Vue({ ... components: { 'appBlack': { template: '#black' } } });
主要的 Vue App HTML:
<component :is="selected"> ... <path class="label" d="M12,295.9s56.5,5,137.6,0V409S78.1,423.6,12,409Z" transform="translate(-12 -13.8)" :style="{ fill: labelColor }"/> ... </component> <h4>Color</h4> <button @click="selected ='appBlack', labelColor = '#000000'">Black Label</button> <button @click="selected ='appWhite', labelColor = '#ffffff'">White Label</button> <input type="color" v-model="labelColor" defaultValue="#ff0000">
白色組件的 HTML:
<script type="text/x-template" id="white"> <div class="white"> <slot></slot> </div> </script>
(這個演示案例非常大,所以最好在一個單獨的窗口或者標簽頁瀏覽)
See the Pen Vue Wine Label Maker by Sarah Drasner (@sdras) on CodePen.
現在,我們已經將所有的 SVG 圖片數據放置在程序中,但是實際上它放置在每個組件的 <slot>
中。我們可以根據使用情況切換不同的內容或樣式,這是一個非常好的功能。你可以看到,通過創建一個更改組件的“selected”值的按鈕,允許用戶自己決定使用哪個組件。
現在所有內容都在一個 slot 中,但是我們也可以使用多個 slot,並通過命名區分它們:
<!-- main vue app instance --> <app-comment> <p slot="comment">{{ comment.text }}</p> </app-comment> <!-- individual component --> <script type="text/x-template" id="comment-template"> <div> <slot name="comment"></slot> </div> </script>
我們可以通過引用的相同的 slot 很容易地在不同的組件之間切換,但是如果希望能夠來回切換還要保持每個組件的獨立狀態會怎樣?目前,當我們切換黑白標簽的時候,模板切換了但內容保持不變。但也許有一種情況,我們希望黑色標簽和白色標簽是完全不同的。你可以把它們包在稱為 <keep-alive></keep-alive>
的特殊組件中,這樣切換的時候會保持獨立的狀態。
檢查上面例子的異常——創建一個黑色標簽,然后一個不同的白色標簽,並在它們之間切換。你會看到,每個狀態都被保存下來,並且彼此不同:
<keep-alive> <component :is="selected"> ... </component> </keep-alive>
See the Pen Vue Wine Label Maker- with keep-alive by Sarah Drasner (@sdras) on CodePen.
我喜歡這個 API 的功能。
這很好,但為了簡單起見,我們總是把所有內容放置在一個或兩個文件中。當建立網站時,如果將單獨的組件放在不同的文件中,並在需要的時候導入進來,這樣的組織性更強。實際上在真實的 Vue 開發中通常是這么做的,我們將在下一部分介紹。接下來談論 Vue-cli、構建過程以及 vuex 狀態管理!