Vue知識(二)元件化開發

2021-05-08 23:00:02

元件化開發(上)

將一個複雜的問題,拆分成很多歌可以處理的小問題,再將其放在整體當中。

元件化也是類似的思想:

  • 如果我們將一個頁面中所有的處理邏輯全部放在一起,處理起來就會變得非常複雜,而且不利於後續的管理以及擴充套件。
  • 但如果,我們講一個頁面拆分成一個個小的功能塊,每個功能塊完成屬於自己這部分獨立的功能,那麼之後整個頁面的管理和維護就變得非常容易了。
    在這裡插入圖片描述

元件化是Vue.js中的重要思想

  • 它提供了一種抽象,讓我們可以開發出一個個獨立可複用的小元件來構造我們的應用。
  • 任何的應用都會被抽象成一顆元件樹。
    在這裡插入圖片描述

註冊元件

註冊元件的基本步驟

  • 建立元件構造器
  • 註冊元件
  • 使用元件。
    注意:註冊元件的第一個引數不能使用駝峰命名
    在這裡插入圖片描述

註冊元件步驟解析
1.Vue.extend():

  • 呼叫Vue.extend()建立的是一個元件構造器。
  • 通常在建立元件構造器時,傳入template代表我們自定義元件的模板。
  • 該模板就是在使用到元件的地方,要顯示的HTML程式碼。
  • 事實上,這種寫法在Vue2.x的檔案中幾乎已經看不到了,它會直接使用下面我們會講到的語法糖,但是在很多資料還是會提到這種方式,而且這種方式是學習後面方式的基礎。

2.Vue.component():

  • 呼叫Vue.component()是將剛才的元件構造器註冊為一個元件,並且給它起一個元件的標籤名稱。
  • 所以需要傳遞兩個引數:1、註冊元件的標籤名 2、元件構造器

3.元件必須掛載在某個Vue範例下,否則它不會生效。(見下頁)

  • 我們來看下面我使用了三次<my-cpn></my-cpn>
  • 而第三次其實並沒有生效:
    在這裡插入圖片描述

全域性元件和區域性元件
當我們通過呼叫Vue.component()註冊元件時,元件的註冊是全域性的。

  • 這意味著該元件可以在任意Vue範例下使用。

如果我們註冊的元件是掛載在某個vue的範例中, 那麼就是一個區域性元件。
在這裡插入圖片描述

父元件和子元件

子元件放在父元件下進行註冊,Child元件是在Parent元件中註冊的,它只能在Parent元件中使用,確切地說:子元件只能在父元件的template中使用。
在這裡插入圖片描述

父子元件錯誤用法:以子標籤的形式在Vue範例中使用。

<div id="app">
	<parent-component>
		<child-component></child-component>
	</parent-component>
</div>

有以下四點原因

  • 當子元件註冊到父元件的components時,Vue會編譯好父元件的模組
  • 模板的內容已經決定了父元件將要渲染的HTML(相當於父元件中已經有了子元件中的內容了)
  • <child-cpn></child-cpn>只能在父元件中被識別的(在爺爺的元件是識別不了)。
  • 類似這種用法,<child-cpn></child-cpn>是會被瀏覽器忽略的。

註冊元件語法糖

在上面註冊元件的方式,可能會有些繁瑣。

  • Vue為了簡化這個過程,提供了註冊的語法糖。
  • 主要是省去了呼叫Vue.extend()的步驟,而是可以直接使用一個物件來代替。

之前方式:

<body>
<div id="app">
<!--  3.使用元件-->
  <cpn1></cpn1>
</div>

<script src="../js/vue.js"></script>
<script>
  // 1.建立元件構造器
  const cpn1 = Vue.extend({
    template: `<div><h2>我是標題2</h2></div>`
  });

  // 2.註冊元件
  Vue.component('cpn1', cpn1)
</script>
<script>
  const app = new Vue({
    el: '#app', // 用於掛載要管理的元素
  })
</script>
</body>

語法糖註冊全域性元件和區域性元件:
全域性元件
Vue.component()的第1個引數是標籤名稱,第2個引數是一個選項物件,使用選項物件的template屬性定義元件模板。
使用這種方式,Vue在背後會自動地呼叫Vue.extend()。

<div id="app">
<!--  3.使用元件-->
  <cpn1></cpn1>
</div>

<script src="../js/vue.js"></script>
<script>
  // 一起呼叫 1.建立元件構造器 2.註冊元件
  Vue.component('cpn1', {
    template: `<div><h2>我是標題2</h2></div>`
  })
</script>
<script>
  const app = new Vue({
    el: '#app', // 用於掛載要管理的元素
  })
</script>

區域性元件
在選項物件的components屬性中實現區域性註冊:

<div id="app">
  <!--  3.使用元件-->
  <cpn2></cpn2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app', // 用於掛載要管理的元素
    components: {
      'cpn2': {
        template: `<h2>我是區域性元件</h2>`
      }
    }
  })
</script>

在這裡插入圖片描述

元件模板template的抽離寫法

如果我們能將其中的HTML分離出來寫,然後掛載到對應的元件上,必然結構會變得非常清晰。
Vue提供了兩種方案來定義HTML模組內容:

  • 使用script標籤
  • 使用template標籤
    在這裡插入圖片描述

元件資料

以下做法是錯誤的:元件內部是不能存取Vue範例的。
在這裡插入圖片描述

元件中的data必須是函數
傳入Vue構造器的多數選項也可以用在 Vue.extend() 或Vue.component()中,不過有兩個特例: data 和el。

Vue.js規定:在定義元件的選項時,data和el選項必須使用函數。

  • 元件物件也有一個data屬性(也可以有methods等屬性,下面我們有用到)
  • 只是這個data屬性必須是一個函數,為了保證元件範例有各自空間物件來儲存自己的狀態,避免共用變數。(不是函數會報錯)
  • 這個函數返回一個物件,物件內部儲存著資料,函數每次執行都會建立新的物件空間
    在這裡插入圖片描述

父子元件通訊

在開發中,往往一些資料確實需要從上層傳遞到下層:

  • 比如在一個頁面中,我們從伺服器請求到了很多的資料。
  • 其中一部分資料,並非是我們整個頁面的大元件來展示的,而是需要下面的子元件進行展示。
  • 這個時候,並不會讓子元件再次傳送一個網路請求,而是直接讓大元件(父元件) 將資料傳遞給小元件(子元件)。

如何進行父子元件間的通訊呢?Vue官方提到

  • 通過props向子元件傳遞資料
  • 通過事件向父元件傳送訊息
    在這裡插入圖片描述

props基本用法
在元件中,使用選項props來宣告需要從父級接收到的資料。

props的值有兩種方式:

  • 方式一:字串陣列,陣列中的字串就是傳遞時的名稱。
  • 方式二:物件,物件可以設定傳遞時的型別,也可以設定預設值等。

父傳子
在下面的程式碼中,我直接將Vue範例當做父元件,並且其中包含子元件來簡化程式碼。
真實的開發中,Vue範例和子元件的通訊和父元件和子元件的通訊過程是一樣的。
在這裡插入圖片描述

props資料驗證
在前面,我們的props選項是使用一個陣列。
除了陣列之外,我們也可以使用物件,當需要對props進行型別等驗證時,就需要物件寫法了。

驗證都支援哪些資料型別呢?
String、Number、Boolean、Array、Object、Date、Function、Symbol

props基礎範例
下面的程式碼定義了一個子元件my-component,在Vue範例中定義了data選項。

var vm = new Vue({
	el: '#app',
	data: {
		name: 'keepfool',
		age: 28
	},
	components: {
		'my-component': {
			template: '#myComponent',
			props: ['myName', 'myAge']
		}
	}
})

為了便於理解,你可以將這個Vue範例看作my-component的父元件。
如果我們想使用父元件的資料,則必須先在子元件中定義props屬性,也就是props: ['myName', 'myAge']這行程式碼。

定義子元件的HTML模板:

<template id="myComponent">
	<table>
		<tr>
			<th colspan="2">
				子元件資料
			</th>
		</tr>
		<tr>
			<td>my name</td>
			<td>{{ myName }}</td>
		</tr>
		<tr>
			<td>my age</td>
			<td>{{ myAge }}</td>
		</tr>
	</table>
</template>

將父元件資料通過已定義好的props屬性傳遞給子元件:

<div id="app">
	<my-component v-bind:my-name="name" v-bind:my-age="age"></my-component>
</div>

注意:在子元件中定義prop時,使用了camelCase命名法。由於HTML特性不區分大小寫,camelCase的prop用於特性時,需要轉為 kebab-case(短橫線隔開)。例如,在prop中定義的myName,在用作特性時需要轉換為my-name。<my-component v-bind:my-name="name"

父元件是如何將資料傳給子元件的呢?相信看了下面這圖,也許你就能很好地理解了。
在這裡插入圖片描述
在父元件中使用子元件時,通過以下語法將資料傳遞給子元件:
<child-component v-bind:子元件prop="父元件資料屬性"></child-component>

當我們有自定義建構函式時,驗證也支援自定義的型別
在這裡插入圖片描述

prop的繫結型別
單向繫結
既然父元件將資料傳遞給了子元件,那麼如果子元件修改了資料,對父元件是否會有所影響呢?
我們將子元件模板和頁面HTML稍作更改:

var vm = new Vue({
	el: '#app',
	data: {
		name: 'keepfool',
		age: 28
	},
	components: {
		'my-component': {
			template: '#myComponent',
			props: ['myName', 'myAge']
		}
	}
})
<div id="app">

	<table>
		<tr>
			<th colspan="3">父元件資料</td>
		</tr>
		<tr>
			<td>name</td>
			<td>{{ name }}</td>
			<td><input type="text" v-model="name" /></td>
		</tr>
		<tr>
			<td>age</td>
			<td>{{ age }}</td>
			<td><input type="text" v-model="age" /></td>
		</tr>
	</table>

	<my-component v-bind:my-name="name" v-bind:my-age="age"></my-component>
</div>

<template id="myComponent">
	<table>
		<tr>
			<th colspan="3">子元件資料</td>
		</tr>
		<tr>
			<td>my name</td>
			<td>{{ myName }}</td>
			<td><input type="text" v-model="myName" /></td>
		</tr>
		<tr>
			<td>my age</td>
			<td>{{ myAge }}</td>
			<td><input type="text" v-model="myAge" /></td>
		</tr>
	</table>
</template>

修改了父元件的資料,同時影響了子元件。

prop預設是單向繫結:當父元件的屬性變化時,將傳導給子元件,但是反過來不會。這是為了防止子元件無意修改了父元件的狀態

在這裡插入圖片描述
雙向繫結
可以使用.sync顯式地指定雙向繫結,這使得子元件的資料修改會回傳給父元件。
<my-component v-bind:my-name.sync="name" v-bind:my-age.sync="age"></my-component>

單次繫結
可以使用.once顯式地指定單次繫結,單次繫結在建立之後不會同步之後的變化,這意味著即使父元件修改了資料,也不會傳導給子元件。
<my-component v-bind:my-name.once="name" v-bind:my-age.once="age"></my-component>

範例

<!DOCTYPE html>
<html>

	<head>
		<meta charset="UTF-8">
		<title></title>
		<link rel="stylesheet" href="styles/demo.css" />
	</head>

	<body>
		<div id="app">
			<div id="searchBar">
				Search <input type="text" v-model="searchQuery" />
			</div>
			<simple-grid :data="gridData" :columns="gridColumns" :filter-key="searchQuery">
			</simple-grid>
		</div>

		<template id="grid-template">
			<table>
				<thead>
					<tr>
						<th v-for="col in columns">
							{{ col | capitalize}}
						</th>
					</tr>
				</thead>
				<tbody>
					<tr v-for="entry in data | filterBy filterKey">
						<td v-for="col in columns">
							{{entry[col]}}
						</td>
					</tr>
				</tbody>
			</table>
		</template>

	</body>
	<script src="js/vue.js"></script>
	<script>
		Vue.component('simple-grid', {
			template: '#grid-template',
			props: {
				data: Array,
				columns: Array,
				filterKey: String
			}
		})

		var demo = new Vue({
			el: '#app',
			data: {
				searchQuery: '',
				gridColumns: ['name', 'age', 'sex'],
				gridData: [{
					name: 'Jack',
					age: 30,
					sex: 'Male'
				}, {
					name: 'Bill',
					age: 26,
					sex: 'Male'
				}, {
					name: 'Tracy',
					age: 22,
					sex: 'Female'
				}, {
					name: 'Chris',
					age: 36,
					sex: 'Male'
				}]
			}
		})
	</script>
</html>

1.prop驗證

props: {
	data: Array,
	columns: Array,
	filterKey: String
}

這段程式碼錶示:父元件傳遞過來的data和columns必須是Array型別,filterKey必須是字串型別。

2.filterBy過濾器
可以根據指定的字串過濾資料

元件化開發(下)

概述

在子元件中定義props,可以讓父元件的資料傳遞下來,這就好比子元件告訴父元件:「嘿,老哥,我開通了一個驛站,你把東西放到驛站我就可以拿到了。」

今天我們將著重介紹slot和父子元件之間的存取和通訊,slot是一個非常有用的東西,它相當於一個內容插槽,它是我們重用元件的基礎。Vue的事件系統獨立於原生的DOM事件,它用於元件之間的通訊。

本文的主要內容如下:

  • 元件的編譯作用域
  • 在元件template中使用<slot>標籤作為內容插槽
  • 使用$children, $refs, $parent實現父子元件之間的範例存取
  • 在子元件中,使用$dispatch向父元件派發事件;在父元件中,使用$broadcast向子元件傳播事件
  • 結合這些基礎知識,我們一步一步實現一個CURD的範例

編譯作用域

儘管使用元件就像使用一般的HTML元素一樣,但它畢竟不是標準的HTML元素,為了讓瀏覽器能夠識別它,元件會被解析為標準的HTML片段,然後將元件的標籤替換為該HTML片段。

<div id="app">
	<my-component>
	</my-component>
</div>

<template id="myComponent">
	<div>
		<h2>{{ msg }}</h2>
		<button v-on:click="showMsg">Show Message</button>
	</div>
</template>
<script src="js/vue.js"></script>
<script>
	new Vue({
		el: '#app',
		components: {
			'my-component': {
				template: '#myComponent',
				data: function() {
					return {
						msg: 'This is a component!'
					}
				},
				methods: {
					showMsg: function() {
						alert(this.msg)
					}
				}
			}
		}
	})

這段程式碼定義了一個my-component元件,<my-component><my-component>不是標準的HTML元素,瀏覽器是不理解這個元素的。
那麼Vue是如何讓瀏覽器理解<my-component><my-component>標籤的呢?
在這裡插入圖片描述
在建立一個Vue範例時,除了將它掛載到某個HTML元素下,還要編譯元件,將元件轉換為HTML片段。
除此之外,Vue範例還會識別其所掛載的元素下的<my-component>標籤,然後將<my-component>標籤替換為HTML片段。

元件在使用前,經過編譯已經被轉換為HTML片段了,元件是有一個作用域的,那麼元件的作用域是什麼呢?
你可以將它理解為元件模板包含的HTML片段,元件模板內容之外就不是元件的作用域了。
例如,my-component元件的作用域只是下面這個小片段。
在這裡插入圖片描述

父元件模板的內容在父元件作用域內編譯;子元件模板的內容在子元件作用域內編譯

通俗地講,在子元件中定義的資料,只能用在子元件的模板。在父元件中定義的資料,只能用在父元件的模板。如果父元件的資料要在子元件中使用,則需要子元件定義props。

使用Slot

為了讓元件可以組合,我們需要一種方式來混合父元件的內容與子元件自己的模板。這個處理稱為內容分發,Vue.js 實現了一個內容分發 API,使用特殊的 <slot> 元素作為原始內容的插槽。

<slot>元素是一個內容插槽。

單個Slot
下面的程式碼在定義my-component元件的模板時,指定了一個<slot></slot>元素。

<div id="app">
	<my-component>
		<h1>Hello Vue.js!</h1>
	</my-component>

	<my-component>
	</my-component>
</div>
<template id="myComponent">
	<div class="content">
		<h2>This is a component!</h2>
		<slot>如果沒有分發內容,則顯示slot中的內容</slot>
		<p>Say something...</p>
	</div>
</template>
<script src="js/vue.js"></script>
<script>
	Vue.component('my-component', {
		template: '#myComponent'
	})

	new Vue({
		el: '#app'
	})
</script>

在這裡插入圖片描述

第一個<my-component>標籤有一段分發內容<h1>Hello Vue.js!</h1>,渲染元件時顯示了這段內容。
在這裡插入圖片描述
第二個<my-component>標籤則沒有,渲染元件時則顯示了slot標籤中的內容

指定名稱的slot

上面這個範例是一個匿名slot,它只能表示一個插槽。如果需要多個內容插槽,則可以為slot元素指定name屬性。

多個slot一起使用時,會非常有用。例如,對話方塊是HTML常用的一種互動方式。
在不同的運用場景下,對話方塊的頭部、主體內容、底部可能是不一樣的。
在這裡插入圖片描述
這時,使用不同名稱的slot就能輕易解決這個問題了。

<template id="dialog-template">
	<div class="dialogs">
		<div class="dialog" v-bind:class="{ 'dialog-active': show }">
			<div class="dialog-content">
				<div class="close rotate">
					<span class="iconfont icon-close" @click="close"></span>
				</div>
				<slot name="header"></slot>
				<slot name="body"></slot>
				<slot name="footer"></slot>
			</div>
		</div>
		<div class="dialog-overlay"></div>
	</div>
</template>

<script src="js/vue.js"></script>
<script>
	Vue.component('modal-dialog', {
		template: '#dialog-template',
		props: ['show'],
		methods: {
			close: function() {
				this.show = false
			}
		}
	})

	new Vue({
		el: '#app',
		data: {
			show: false
		},
		methods: {
			openDialog: function() {
				this.show = true
			},
			closeDialog: function() {
				this.show = false
			}
		}
	})
</script>

在定義modal-dialog元件的template時,我們使用了3個slot,它們的name特性分別是header、body和footer。

<modal-dialog>標籤下,分別為三個元素指定slot特性:

<div id="app">
	<modal-dialog v-bind:show.sync="show">

		<header class="dialog-header" slot="header">
			<h1 class="dialog-title">提示資訊</h1>
		</header>

		<div class="dialog-body" slot="body">
			<p>你想在對話方塊中放什麼內容都可以!</p>
			<p>你可以放一段文字,也可以放一些表單,或者是一些圖片。</p>
		</div>

		<footer class="dialog-footer" slot="footer">
			<button class="btn" @click="closeDialog">關閉</button>
		</footer>
	</modal-dialog>

	<button class="btn btn-open" @click="openDialog">開啟對話方塊</button>
</div>

對話方塊的標題內容、主體內容、底部內容,完全由我們自定義,而且這些內容就是一些簡單的HTML元素!
在這裡插入圖片描述

父子元件之間的存取

有時候我們需要父元件存取子元件,子元件存取父元件,或者是子元件存取根元件。
針對這幾種情況,Vue.js都提供了相應的API:

父元件存取子元件:使用$children或$refs
子元件存取父元件:使用$parent
子元件存取根元件:使用$root

$children範例

下面這段程式碼定義了3個元件:父元件parent-component,兩個子元件child-component1和child-component2。

在父元件中,通過this.$children可以存取子元件。
this.$children是一個陣列,它包含所有子元件的範例。

<div id="app">
	<parent-component></parent-component>
</div>

<template id="parent-component">
	<child-component1></child-component1>
	<child-component2></child-component2>
	<button v-on:click="showChildComponentData">顯示子元件的資料</button>
</template>

<template id="child-component1">
	<h2>This is child component 1</h2>
</template>

<template id="child-component2">
	<h2>This is child component 2</h2>
</template>

<script src="js/vue.js"></script>
<script>
	Vue.component('parent-component', {
		template: '#parent-component',
		components: {
			'child-component1': {
				template: '#child-component1',
				data: function() {
					return {
						msg: 'child component 111111'
					}
				}
			},
			'child-component2': {
				template: '#child-component2',
				data: function() {
					return {
						msg: 'child component 222222'
					}
				}
			}
		},
		methods: {
			showChildComponentData: function() {
				for (var i = 0; i < this.$children.length; i++) {
					alert(this.$children[i].msg)
				}
			}
		}
	})

	new Vue({
		el: '#app'
	})
</script>

在這裡插入圖片描述

$refs範例

元件個數較多時,我們難以記住各個元件的順序和位置,通過序號存取子元件不是很方便。
在子元件上使用v-ref指令,可以給子元件指定一個索引ID:

<template id="parent-component">
	<child-component1 v-ref:cc1></child-component1>
	<child-component2 v-ref:cc2></child-component2>
	<button v-on:click="showChildComponentData">顯示子元件的資料</button>
</template>

在父元件中,則通過$refs.索引ID存取子元件的範例:

showChildComponentData: function() {
	alert(this.$refs.cc1.msg);
	alert(this.$refs.cc2.msg);
}

$parent範例

下面這段程式碼定義了兩個元件:child-component和它的父元件parent-component。
在子元件中,通過this.$parent可以存取到父元件的範例。

<div id="app">
	<parent-component></parent-component>
</div>

<template id="parent-component">
	<child-component></child-component>
</template>

<template id="child-component">
	<h2>This is a child component</h2>
	<button v-on:click="showParentComponentData">顯示父元件的資料</button>
</template>

<script src="js/vue.js"></script>
<script>
	Vue.component('parent-component', {
		template: '#parent-component',
		components: {
			'child-component': {
				template: '#child-component',
				methods: {
					showParentComponentData: function() {
						alert(this.$parent.msg)
					}
				}
			}
		},
		data: function() {
			return {
				msg: 'parent component message'
			}
		}
	})
	new Vue({
		el: '#app'
	})
</script>

在這裡插入圖片描述

注意:儘管可以存取父鏈上任意的範例,不過子元件應當避免直接依賴父元件的資料,儘量顯式地使用 props 傳遞資料。另外,在子元件中修改父元件的狀態是非常糟糕的做法,因為:
1.這讓父元件與子元件緊密地耦合;
2.只看父元件,很難理解父元件的狀態。因為它可能被任意子元件修改!理想情況下,只有元件自己能修改它的狀態。

說到底,元件的API主要來源於以下三部分:

  • prop 允許外部環境傳遞資料給元件;
  • 事件 允許元件觸發外部環境的 action;
  • slot 允許外部環境插入內容到元件的檢視結構內。