談談 Vue 最基礎的 Component

Component 概念為 React 所發明,讓我們可以重複使用 HTML,Vue、Angular 也 致敬 React,採用 component ,至此 3 大 Framework 都統一採用 Component-based 架構。

Version


Vue 2.5.17

Architecture


intro000

Introduction


Vue Instance 有自己的 datamethodscomputedwatch,但 Vue Instance 只是 HTML 的代言人,讓我們在 JavaScript 控制 HTML,但若要 重複使用,就不是那麼方便,這時我們需要的是 Vue Component。

除此之外,Vue Component 也讓我們在開發時實踐 Divide and Conquer 哲學,先將需求切成小小 component,然後一一擊破,最後再將 component 組合起來,如此 component 也更加 單一職責,更 容易維護重複使用

Global Component


之前我們只會 MVVM 的 Hello World,若改用 Component-based 的寫法呢 ?

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Global Component</title>
</head>
<body>
<div id="app">
<hello-world></hello-world>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="HelloWorld.js"></script>
<script src="index.js"></script>
</html>

第 9 行

1
<hello-world></hello-world>

由原本的 <span>Hello World</span>,變成自訂的 hello-world tag。

我們會為自己的的 Vue Component 定義自己的 HTML tag

index.js

1
2
3
new Vue({
el: '#app'
});

建立 Vue Instance。

HelloWorld.js

1
2
3
Vue.component('HelloWorld', {
template: '<span>Hello World</span>',
});

定義 HelloWorld component。

使用 Vue Vue.component() 定義 Vue Component。

  • 第 1 個參數:string,傳入自訂的 HTML tag 名稱
  • 第 2 個參數:object,類似傳入 Vue Instance 的 constructor 參數

Vue Component 的使用,有幾點要注意:

  • Vue 規定 Vue Component 一定要定義在 Vue Instance 之前,否則 Life Cycle 在 compile HTML 階段,會不知道 Vue Component 所自訂的 HTML tag
  • 自訂的 HTML tag 名稱,無論使用 camelCase,CamelCase,最後 Vue 都會改用 Kebab-case (全小寫,單字間以 - 隔開),這是 W3C 所建議,且必須是 2 個單字,避免用一個單字與 HTML 預設 tag 重複
  • 關於 Component (HTML tag) 與 JavaScript 檔案命名方式,Vue 官方的 Style Guide :
    • CamelCaseHelloWorldHelloWorld.jsHelloWorld.vue
    • kebab-casehello-worldhello-world.jshello-world.vue
    • Vue CLI 使用 CamelCase

Local Component


使用 Vue.component() 所宣告的是 Global Component,也就是每個 Vue Instance 都可使用,若你想定義只有某個 Vue Instance 能使用的 component,則要使用 Local Component。

inex.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Local Component</title>
</head>
<body>
<div id="app">
<hello-world></hello-world>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="local_component.js"></script>
</html>

HTML 部分一樣不變使用 <hello-world></hello-world>

index.js

1
2
3
4
5
6
7
8
new Vue({
el: '#app',
components: {
'HelloWorld': {
template: '<span>Hello World</span>',
}
}
});

在傳入 Vue Instance 的 constructor 參數內,加上 components Property,為 object。

以自訂的 HTML tag hello-world 為 key,將 Vue.component() 第二個參數的 object 為 value。

MVVM vs. Component


目前 Vue Component 的 data 顯示都是寫死的,我們知道 MVVM 的精髓就是 Data Binding,要如何將 MVVM 與 Component-based 兩種架構合而為一呢 ?

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Counter Component</title>
</head>
<body>
<div id="app">
<my-counter></my-counter>
<my-counter></my-counter>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="MyCounter.js"></script>
<script src="index.js"></script>
</html>

第 9 行

1
<my-counter></my-counter>

使用自訂的 <my-counter></my-counter>

index.js

1
2
3
new Vue({
el: '#app'
});

建立 Vue Instance。

MyComponent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vue.component('MyCounter', {
template: `
<div>
<span>{{ counter }}</span>
<p></p>
<button @click="add">+1</button>
</div>
`,

data() {
return {
counter: 0,
};
},
methods: {
add() {
this.counter++;
}
}
});

第 2 行

1
2
3
4
5
6
7
template: `
<div>
<span>{{ counter }}</span>
<p></p>
<button @click="add">+1</button>
</div>
`,

將 HTML Template 宣告在 template property 下。

由於 HTML Template 在實務上會很多行,用普通字串不方便,建議改用 ECMAScript 2015 的 string template,就不必再 字串相加

第 9 行

1
2
3
4
5
data() {
return {
counter: 0,
};
},

data 部分,由原本 Vue Instance 的 data property 改成 data() function。

改回傳 data object。

14 行

1
2
3
4
5
methods: {
add() {
this.counter++;
}
}

data 內的 counter 累加。

Q : 為什麼寫成 Vue Component 後,要從 data property 改成 data() function ?

1
2
3
data: {
counter: 0
},

若改成 data property 寫法,Vue 會無法執行,且出現 warning。

basic001

basic002

在正統 OOP,兩個 component 應該是兩個 instance,而 data 包在 instance 內,因此 Component 間的 data 不會互相影響,也就是 OOP 的 封裝

basic003

但 Vue 底層並不是採用 OOP 方式,而是共用同一份 component instance,只有 data 是不同份。

這也是為什麼為什麼 Vue 要你改用 data() function,而且是回傳全新對 data object。

只要寫 Vue Component,就一定要改用 data() function,不能使用 data property

使用 Vue Component 時,還有一點值得注意:

  • 不可使用 HTML self closing 語法
1
2
3
4
<div id="app">
<my-counter/>
<my-counter/>
</div>

這種寫法,Vue 不會出錯,但只有一個 component 能動。

1
2
3
4
<div id="app">
<my-counter></my-counter>
<my-counter></my-counter>
</div>

要這樣寫,Vue 才能正常執行。

Component vs. DOM Parser


有時候在使用 Vue Component 時,會發現無法如預期顯示在 Browser 裏。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM Parse Error</title>
</head>
<body>
<div id="app">
<select>
<my-option></my-option>
</select>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="index.js"></script>
<script src="MyOption.js"></script>
</html>

第 9 行

1
2
3
<select>
<my-option></my-option>
</select>

<select></select> 內使用自訂的 <my-option></my-option> Vue Component。

index.js

1
2
3
new Vue({
el: '#app'
});

建立 Vue Instance。

MyOption.js

1
2
3
Vue.component('MyOption', {
template: '<option>Vue</option>',
});

自訂的 MyOption 只包含 <option>Vue</option> 部分。

basic004

  1. Chrome 無法正常顯示
  2. <select></select> 以下沒有任何 <option></option>

這牽涉到各 Browser 的 DOM Parser 如何解析 HTML。

以 Chrome 而言,它認為 <select></select>只應該<option></option>,其他的 HTML tag 都為非法,因此忽略不使用。

DOM Parser 會因 Browser 而異

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM Parse OK</title>
</head>
<body>
<div id="app">
<my-select></my-select>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="MySelect.js"></script>
<script src="index.js"></script>
</html>

由本來的 <my-option></my-option> 改成 <my-select></my-select>

index.js

1
2
3
new Vue({
el: '#app'
});

建立 Vue Instance。

MySelect.js

1
2
3
4
5
6
7
Vue.component('MySelect', {
template: `
<select>
<option>Vue</option>
</select>
`,

});

<select> 一起包進 Vue Component,如此 Chrome 就無法干涉 <option>

Dynamic Component


先定義好 Vue Component,然後動態切換 component。

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic Component</title>
</head>
<body>
<div id="app">
<button @click="selectLesson">Lessons</button>
<button @click="selectApply">Apply</button>
<p></p>
<component :is="content"></component>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="MyLessons.js"></script>
<script src="MyApply.js"></script>
<script src="index.js"></script>
</html>

12 行

1
<component :is="content"></component>

使用 Vue 擴充的 <component></component>,綁定其 is,當 content 指定什麼 Vue Component 時,<component></component> 就會動態切換該 Vue Component。

並沒有在 HTML 內事先使用特定 Component tag,只使用 <component></component> 保留其動態彈性

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Vue({
el: '#app',
data: {
content: 'my-lessons'
},
methods: {
selectLesson() {
this.content = 'my-lessons';
},
selectApply() {
this.content = 'my-apply';
}
}
});

第 3 行

1
2
3
data: {
content: 'my-lessons'
},

data 內定義 content model,其中預設值 my-lessons 為 component 名稱。

第 6 行

1
2
3
4
5
6
7
8
methods: {
selectLesson() {
this.content = 'my-lessons';
},
selectApply() {
this.content = 'my-apply';
}
}

由 method 動態改變 content,MVVM 會再動態改變 <component></component>:is,達到動態組件的需求。

MyLessions.js

1
2
3
4
5
6
7
8
9
Vue.component('MyLessons', {
template: `
<ul>
<li>React</li>
<li>Angular</li>
<li>Vue</li>
</ul>
`,

});

定義了 MyLessions component。

MyApply.js

1
2
3
4
5
6
7
8
9
Vue.component('MyApply', {
template: `
<form>
<textarea></textarea>
<p></p>
<button>Submit</button>
</form>
`,

});

定義了 MyApply component。

但這兩個 Vue Component 都沒在 HTML 內被使用,將由 JavaScript 動態指定

Keep-Alive


由於 <component></component> 類似 v-if,其 Dynamic Component 是藉由 刪除 DOM element,並建立新的 DOM element 的方式,所以原本的 user 輸入的資料,也會一併被刪除。

若要保留原本 user 輸入的資料,就必須搭配 <keep-alive><keep-alive>

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Keep Alive</title>
</head>
<body>
<div id="app">
<button @click="selectLesson">Lessons</button>
<button @click="selectApply">Apply</button>
<p></p>
<keep-alive>
<component :is="content"></component>
</keep-alive>
</div>
</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="MyLessons.js"></script>
<script src="MyApply.js"></script>
<script src="index.js"></script>
</html>

12 行

1
2
3
<keep-alive>
<component :is="content"></component>
</keep-alive>

<component></component> 外部加上 <keep-alive></keep-alive>,則 Dynamic Component 內的資料將獲得保留。

JavaScript 的寫法不用改變。

Vue 底層會將 user 的輸入保留,然後切換 component 時,除了建立新的 component 外,還會將 user 原本所輸入的資料也 重新 填回新建立的 component,讓動態切換 component 更方便

Conclusion


  • Vue 提供了 Vue Component,讓我們將 HTML 會重複的部分可以使用 component 包起來,方便閱讀,也更方便維護
  • MVVM 可以 Component-based 完美結合,但 data property 必須改用 data function
  • Component 有時候會違背 Browser 的 DOM Parser,此時必須改變寫法繞過 Browser
  • Dynamic Component 讓我們可以根據商業邏輯自行切換 component
  • 透過神奇的 <keep-alive></keep-alive>,user 原本的輸入將保留在 component 內

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


Vue, Component Basics
Vue, Style Guide

2018-10-25