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 元件系統的概念。

元件的分類與切分

當我們開始要把網頁轉換成模組區塊來管理的時候,首先面臨的問題,元件該怎麼拆?從何拆起?

要是範圍切得太大,元件過於龐大,切得太細則元件數量太多。再者,元件之間要是彼此耦合程度高,反而不 容易維護,還不如不拆。那麼,接下來就來談談幾種常見的元件分類方法。

常見的元件類型,大致可以分作這幾種類型:

展示型元件(Presentation)

以負責呈現 UI 為主的類型,我們很單純地把資料傳遞進去,然後DOM 就根據我們丟進去的資料生成出來。 這種元件的好處是可以提升UI的重複使用性。

容器型元件(Container)

這類型的元件主要負責與資料層的 service 溝通,包含了與 server端、資料來源做溝通的邏輯,然後再將資料 傳遞給前面所說的展示型元件

互動型元件(Interactive)

像是大家所熟知的 elementUI、bootstrap 的UI library 都屬於此種類型。這種類型的元件通常會包含許多的互動邏輯在裡面,但也與展示型元件同樣強調重複使用。像是表單、燈箱等各種互動元素都算在這類型。

功能型元件(Functions)

這類型的元件本身不渲染任何内容,主要負責將元件內容作為某種應用的延伸,或是某種機制的封裝。像是我 們未來會提及的 等都屬於此類型。

元件的宣告與註冊

#元件的宣告與註冊

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: { ... } 屬性宣告後的元件才能使用。

單一元件檔-single-file-components-sfc

前面提到,我們可以將某個元件以 .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 屬性封裝

首先第一種方式是透過大家都很熟悉的 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> 的標籤就可以了。

透過-x-template-封裝模板

然而,隨著專案規模的擴增,我們的 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 访问它。

子元件的-data-必須是函數

前面我們曾說過,每一個被封裝後的元件單元,都含有自己的模板、樣式,與行為邏輯,當然它們內部的狀態也是一樣。

在前一章介紹 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 就會共用同一個狀態:

⚠️ **GitHub.com Fallback** ⚠️