2‐1 元件系統的特性 - daniel-qa/Vue GitHub Wiki
2-1 元件系統的特性 #
元件系統 (components system) 是 Vue.js 另一個重要的概念與核心功能。
元件 (Component) 是 Vue 最主要也是最強大的特性之一,它提供了 HTML DOM 元素的擴充性, 也可將部分模板、程式碼封裝起來以便開發者維護以及重複使用。
但是當專案的架構越來越大,人們開始把「關注點」從表現層移到了架構層面, 思考如何將功能、邏輯抽象化,將封裝好的 UI 模組、功能重複使用,就如同樂高積木一般。
<div id="app">
<header-component> ... </header-component>
<menu-component> ... </menu-component>
<main-component> ... </main-component>
<footer-component> ... </footer-component>
</div>
每一個被封裝後的元件單元,都含有自己的模板、樣式,與行為邏輯,並且可以被重複使用。 而在元件之中又可以含有元件,這樣由一個個元件單元組合而成的「元件樹」,就是 Vue.js 元件系統的概念。
當我們開始要把網頁轉換成模組區塊來管理的時候,首先面臨的問題,元件該怎麼拆?從何拆起?
要是範圍切得太大,元件過於龐大,切得太細則元件數量太多。再者,元件之間要是彼此耦合程度高,反而不 容易維護,還不如不拆。那麼,接下來就來談談幾種常見的元件分類方法。
常見的元件類型,大致可以分作這幾種類型:
以負責呈現 UI 為主的類型,我們很單純地把資料傳遞進去,然後DOM 就根據我們丟進去的資料生成出來。 這種元件的好處是可以提升UI的重複使用性。
這類型的元件主要負責與資料層的 service 溝通,包含了與 server端、資料來源做溝通的邏輯,然後再將資料 傳遞給前面所說的展示型元件。
像是大家所熟知的 elementUI、bootstrap 的UI library 都屬於此種類型。這種類型的元件通常會包含許多的互動邏輯在裡面,但也與展示型元件同樣強調重複使用。像是表單、燈箱等各種互動元素都算在這類型。
這類型的元件本身不渲染任何内容,主要負責將元件內容作為某種應用的延伸,或是某種機制的封裝。像是我 們未來會提及的 等都屬於此類型。
#元件的宣告與註冊
Vue.js 的元件就是個可以被重複使用的實體。如同我們在前一章介紹過的,每一個元件都可以有自己的 data computed methods,甚至是生命週期的Hooks function。如同JavaScript 的變數可分為全域變數 與區域變數,元件的宣告同樣可以分為全域元件與區域元件。 在過去 Vue 2.x 的時候,全域元件可以透過 Vue.component() 來註冊,第一個參數是元件的名稱,第二個則是 它的屬性(Options):
// for Vue 2.x,把全域元件註冊在 Vue 上
// 子元件內多數屬性與之前介紹的用法完全一樣。
Vue.component('my-component', {
template: <div>Hello Vue!</div>",
data () {
},
//...略
props: {
// ...略
},
computed: {
},
//...略
methods: {
});
},
//...略
//...以及其他選項、各種lifecycle hooks
//新增一個「根實體」,並掛載於 #app 之上
const vm = new Vue({}).$mount('#app');
3.0 開始,我們用 vue.crateApp(), 來創建一個實體
/// for Vue 3.x
const app = Vue.createApp({});
// 將過去的 Vue.component 改為 app.component
// 將元件註冊在 app 身上
app.component('my-component', {
template: <div>Hello Vue 3.x!</div>",
// 內部其餘選項與過去幾乎一樣
data () {
},
//...略
props: {
},
//...略
computed: {
},
//...略
methods: {
//...略
}
});
// 新增一個「根實體」,並掛載於 #app 之上
app.mount('#app');
這時我們就可以透過 app.component()
將新建立的元件註冊在 app
這個根實體 (或稱根元件,root instance) 裡頭,
除此之外的其餘選項與過去幾乎一樣。
<div id="app">
<h3>Root Instance</h3>
<!-- 使用自訂元件 my-component <my-component></my-component> <my-component></my-component>
</div>
另外,除了全域元件從過去 Vue 2.x 的 Vue.components(...)
到了 Vue 3.0 改為 app.components(...)
之外,
同樣改變寫法的還有:
另外,除了全域元件從過去 Vue 2.x 的 Vue.components(...)
到了 Vue 3.0 改為 app.components(...)
之外,
同樣改變寫法的還有:
// Vue 2.x
Vue.use(/**/);
Vue.mixin(/**/);
Vue.component(/**/);
Vue.directive(/**/);
const app = new Vue({
//略
});
app.$mount('#app');
// Vue 3.x
const app = Vue.createApp({
//略
});
app.use(/**/);
app.mixin(/*略*/);
app.component(/**/);
app.directive(/*略*/);
app.mount('#app');
Vue 3.0 這樣的改變,最主要是為了不把所有的特性與行為全部綁在 Vue
上,
將全域的概念,從原本的整個 Vue 移到「根實體」,這樣即使在同個頁面上同時宣告了多個根實體,也不會因此而互相污染。
另外,除了全域元件從過去 Vue 2.x 的 Vue.components(...)
到了 Vue 3.0 改為 app.components(...)
之外,
同樣改變寫法的還有:
而區域型的元件則是在上層元件的 components
屬性 (記得加上 s
) 裡面定義:
<div id="app">
<h3>Root Instance</h3>
<!-- 在根實體使用自訂元件 -->
<my-component></my-component>
</div>
// for Vue 3.0
// 新增一個「根實體」,並在components 選項內註冊子元件 my-component:
const app = Vue.createApp({
components: {
'my-component': {
// 子元件的 options
template: <div>Hello Vue!</div>",
}
}
});
app.mount('#app');
若環境支援 ES Module 也可透過 import 的方式將外部檔案引入子元件:
// for Vue 3.0
import myComponent from './components/my-component.js';
const app = Vue.createApp({
components: {
myComponent
}
});
除了以上所述,區域元件與全域元件的其他部分,在使用上幾乎沒有差別。
唯一要注意的是,全域型元件註冊後即可在這個應用程式裡的所有位置使用 (app
掛載的範圍內) ,
而區域型元件就只有在這個經由 components: { ... }
屬性宣告後的元件才能使用。
前面提到,我們可以將某個元件以 .vue
檔案的方式包裝起來,再透過 import
的方式將這個檔案引入作為子元件,
這個 .vue
檔案通常我們將它稱為 Vue 的單一元件檔 (Single File Components, 以下稱 SFC)。
這個 SFC 通常會包含三個部分,分別是作為 HTML
模板的 <template>
、用來定義元件結構與邏輯的 <script>
以及 CSS 樣式的 <style>
標籤:
<template>
<div class="app">
<div>Hello Vue!</div>
</div>
</template>
<script>
export default {
name: 'myComponent
};
</script>
<style scoped>
.app {
color: red;
}
</style>
這裡需要小心的是,由於 SFC 的 .vue
檔案並非網頁標準,
在使用時通常需透過 vue-loader
或 @vue/compiler-sfc
搭配 webpack 等工具先將 SFC 編譯成瀏覽器看得懂的 JavaScript 程式碼後才能夠被執行。
完成了元件的宣告與註冊後,既然元件的目標是要重複使用,接著我們來看如何將網頁片段封裝到元件的模板裡頭。
首先第一種方式是透過大家都很熟悉的 template
屬性:
app.component('media-block', {
template:'
<div class="media">
<img class="mr-3" src="..." alt="Generic placeholder image">
<div class="media-body">
<h5 class="mt-0">Media heading</h5>
<div>
Cras sit amet nibh libero,
in gravida nulla. Nulla vel metus scelerisque ante sollicitudin.
</div>
</div>
</div>'
});
像這樣,我們將整個 <div class="media"> ... </div>
區塊封裝成一個名叫 media-block
的元件,
在使用的時候,只要先將它引入 (無論是全域或區域) 後,在模板對應的位置插入 <media-block></media-block>
的標籤就可以了。
然而,隨著專案規模的擴增,我們的 HTML 模板結構可能會變得越來越大,光是用 template
屬性直接掛上 HTML 字串時,可能你的程式架構就會變得不是那麼好閱讀、管理。
這時候,我們可以把整個 HTML 模板區塊透過 <script id="xxx" type="text/x-template"> ... </script>
這樣的方式來封裝我們的 HTML 模板,這種方式通常被稱為「X-Templates
」:
<script type="text/x-template" id="media-block">
<div class="media">
<img class="mr-3" src="..." alt="Generic placeholder image">
<div class="media-body">
<h5 class="mt-0">Media heading</h5>
<div>
Cras sit amet nibh libero,
in gravida nulla. Nulla vel metus scelerisque ante sollicitudin.
</div>
</div>
</div>
</script>
像這樣,我們將實際的 HTML 字串放置在 script
標籤內,並且加上 type="text/x-template"
的屬性,
而子元件註冊的時候,我們就在原本的 template
屬性加上對應的 selector:
app.component('media-block', {
template: '#media-block' });
執行結果與直接放在 template
屬性是一樣的。
不過這樣寫法也會有缺點,當根節點大於一個的時候,使用 this.$el 屬性時會無法取得正確的元件 DOM, 不過別擔心,我們可以透過 $refs 來取得實際在網頁上的 DOM 節點
<div id="app">
<h3 ref="title">Root Instance</h3>
</div>
// Vue 3.x
const vm Vue.createApp({
mounted () {
// "Root Instance"
console.log(this.$refs.title.innerText);
}
}).mount('#app');
ref="title":為 <h3> 元素创建一个引用,允许你在 Vue 组件的实例中通过 this.$refs.title 访问它。
前面我們曾說過,每一個被封裝後的元件單元,都含有自己的模板、樣式,與行為邏輯,當然它們內部的狀態也是一樣。
在前一章介紹 Vue 實體時,我們的 data
屬性總是以一個物件的形式來表示。
但是在子元件的 data
屬性,則必須是以函數回傳物件的方式來表示:
// 錯誤
app.component('my-component', {
data: {
count: 0
}
});
// 正確,子元件的 data 必須是以 function 的形式回傳物件
app.component('my-component', {
data () {
return {
count: 0
};
}
});
為什麼有這樣的規定呢?
這是由於 JavaScript 的物件型別是以「傳址」(Pass by reference) 的方式來進行資料傳遞,
若是沒有透過 function
來回傳另一個新物件,則這些子元件的 data
就會共用同一個狀態: