組件化開發
基本概念
在最開始的時候,已經大概的聊了聊Vue是單頁面開發,用戶總是在一個頁面上進行操作,看到的不同內容也是由不同組件構成的。
通過用戶的操作,Vue將會向用戶展示某些組件,也會隱藏某些組件。
一個Vue的項目就是一個Vue的實例對象。而用戶看到的頁面則是Vue.component的實例對象。
對於一些復用性高的內容,我們也可以將它封裝成一個單獨的組件,如導航欄、搜索框、版權信息等等。
所以說組件是Vue的核心、但是本章節不會討論Vue如何實現組件的顯示、隱藏,而是聊一聊如何使用組件。

認識組件
根組件
被掛載管理的元素塊就是一個根組件。在此根組件中可以嵌套多個子組件,根組件一般來說一個就夠了。
<body>
<div id="app">
</div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el:"#app",
})
</script>
</body>
子組件
子組件即是被嵌套在根組件中的組件,子組件可復用,並且數據都是相互獨立的。
子組件可以擁有根組件所擁有的任意的屬性,如data/methods/computed/watch/filter。
需要注意一點:組件名字如果有多個單詞構成,應當寫成駝峰形式(個人推薦)。
並且在模板中進行引用時,當以-進行分割。

<body>
<div id="app">
<!-- 使用全局組件 -->
<cpn-header></cpn-header>
<cpn-header></cpn-header>
</div>
<script src="./vue.js"></script>
<script>
// 定義組件名字與模板
Vue.component("cpnHeader", {
template: "<div><span>這是一個頭部組件</span></div>",
})
const app = new Vue({
el: "#app",
})
</script>
</body>
組件聲明
全局子組件
全局子組件會自動進行注冊,任何Vue的實例都能進行使用。
使用Vue.component("name",{})對其進行聲明並注冊。
<body>
<div id="app">
<!-- 使用全局組件 -->
<cpn-header></cpn-header>
<cpn-header></cpn-header>
</div>
<script src="./vue.js"></script>
<script>
// 定義組件名字與模板
Vue.component("cpnHeader", {
template: "<div><span>這是一個頭部組件</span></div>",
})
const app = new Vue({
el: "#app",
})
</script>
</body>
局部子組件
局部子組件在Vue實例下使用components進行注冊。它僅供當前實例使用,如下所示:
<body>
<div id="app">
<cpn></cpn>
<cpn></cpn>
</div>
<script src="./vue.js"></script>
<script>
// 定義組件
const cpn = {
template:"<div><mark>HELLO,WORLD</mark></div>",
}
const app = new Vue({
el:"#app",
components:{ // 進行注冊
cpn, //es6語法
}
})
</script>
</body>
組件模板
根標簽
每一個組件對象都應該具有template屬性,它應當是一個HTML字符串。
並且,要擁有一個根標簽在下面,否則將會拋出警告信息:

當發生這樣的警告信息,你應該檢查一下你的子組件模板,並給他套上根標簽,如下所示:
// 定義組件
const cpn = {
template: `
<div>
<div>
HELLO,VUE
</div>
<div>
HELLO,WORLD
</div>
</div>
`,
}
抽離寫法
如果在定義組件時在template屬性中寫HTML代碼,是不太友好的,你可以將模板抽離出來。
- 使用script標簽,並添加type="text/x-template"的屬性
- 使用template標簽
如下所示,使用<template>標簽配合id屬性將其作為子組件模板:
<body>
<div id="app">
<cpn></cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<div>
HELLO,VUE
</div>
<div>
HELLO,WORLD
</div>
</div>
</template>
<script src="./vue.js"></script>
<script>
const cpn = {
template: "#cpn",
}
const app = new Vue({
el: "#app",
components: {
cpn,
}
})
</script>
</body>
錯誤示范
如果你書寫了一個DOM可識別的標簽,則是錯誤的操作,如下所示我使用了main標簽來定義組件模板,很顯然DOM認識它,就會自己先一步渲染它再交由Vue進行處理,這會導致我們的組件模板會多一次渲染。

<body>
<div id="app">
<cpn-header></cpn-header>
<cpn-header></cpn-header>
</div>
<!--子組件模板-->
<main id="cpn-header-template">
<div>
<span>這是一個頭部組件</span>
</div>
</main>
<script src="./vue.js"></script>
<script>
var cpnHeader = {
template: "#cpn-header-template",
}
const app = new Vue({
el: "#app",
components: { // Vue實例內部進行注冊
cpnHeader,
}
})
</script>
</body>
子組件的data
上面已經說過,子組件可以擁有data/methods/computed/watch/filter等對象。
但是需要注意的是子組件的data必須是一個函數,且必須返回一個Object。
這是因為每個子組件是相互獨立的,如果data是一個對象,那么獲取所有的子組件數據都會引用同一個Object。
所以子組件中的data必須是一個函數,因為函數調用時會重新申請內存,返回一個全新的Object。
<body>
<div id="app">
<cpn></cpn>
</div>
<template id="cpn">
<div>
<p>{{childrenMessage}}</p>
</div>
</template>
<script src="./vue.js"></script>
<script>
const cpn = {
template: "#cpn",
data() {
return {
childrenMessage: "子組件數據",
}
}
}
const app = new Vue({
el: "#app",
components: {
cpn,
}
})
</script>
</body>
組件通信
通信意義
組件之間的通信是十分有必要的,例如當Vue項目啟動后,父組件獲取到了一些數據,它將如何把這些數據分發到子組件上。
再比如,子組件上產生了一個新數據,它將如何把該數據交由父組件?
props
父組件向子組件傳遞數據時,則通過props進行傳遞。
接收值在父組件模板中采用 - 分割命名,在props中采用駝峰式,在子組件模板中進行使用時采用與props中同樣的命名方式
這是因為 - 分割命名方式不是Js的合法變量名,如果props中采用 - 來進行命名,子組件模板在使用時將查詢不到該變量名
具體操作步驟如下所示:

<body>
<!--根組件模板-->
<div id="app">
<father></father>
</div>
<!--子組件模板-->
<template id="son">
<div>
<!-- ④ 子組件現在已經可以正常渲染出該值了,使用接收的變量名即可-->
<p>{{childerRecv}}</p>
</div>
</template>
<!--父組件模板-->
<template id="father">
<div>
<!--② 子組件通過v-bind,將父組件中的值存儲到props中,需要注意的是再模板中應該使用 - 進行多單詞分割-->
<son :childer-recv="fatherMessage"></son>
</div>
</template>
<script src="./vue.js"></script>
<script>
const son = {
template: "#son",
// ③ 現在子組件中已經用childerRecv這個變量名接收到父組件中傳遞過來的值了,需要注意的是接收時使用駝峰式命名進行接收,否則模板中不會渲染
props: ["childerRecv",]
}
const father = {
template: "#father",
components: {
son,
},
data() {
return {
// ① 父組件的作用域內能夠接收到該值了
fatherMessage: {id: 1, name: "yunya", age: 18},
}
}
}
const app = new Vue({
el: "#app",
components: {
father,
}
})
</script>
</body>
上面這個是三層組件嵌套,可能看起來有點繞。我這里有個雙層嵌套的,看起來比較簡單:
<body>
<div id="app">
<!-- 接收: - 分割命名 -->
<cpn :recv-msg="sendMsg"></cpn>
</div>
<!-- 子組件模板 -->
<template id="cpn-template">
<!-- 使用: 與props同樣的命名方式 -->
<div><span>接收到的信息:{{recvMsg}}</span></div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
// props:使用駝峰式命名,為了子組件模板中能夠使用而不產生報錯
props: ["recvMsg",],
template: "#cpn-template",
data: function () {
return {}
}
}
const app = new Vue({
el: "#app",
data: {
sendMsg: {
id: 1,
name: "admin",
}
},
components: { // Vue實例內部進行注冊
cpn,
}
})
</script>
</body>
畫張圖,讓你更容易理解:

props數據驗證
在上述例子中,父組件從后端獲取的數據傳遞到子組件時沒由進行任何驗證就直接渲染了,這可能導致子組件渲染錯誤。
所以在子組件接收父組件數據時進行驗證是十分必要的流程。
我們可以發現,上述例子的props接收是一個array,如果要使用驗證,則接收要用Object,如下示例:
| 驗證項目 | 描述 |
|---|---|
| type | 一個Array,允許的類型 |
| required | 一個Boolen,是否必須傳遞 |
| default | 任意類型 |
| validator | 一個Function,返回需要驗證的數據字段 |
<body>
<div id="app">
<cpn :recv-msg="sendMsg"></cpn>
</div>
<!-- 模板 -->
<template id="cpn-template">
<div><span>接收到的信息:{{recvMsg}}</span></div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
props: { // props是一個Object
recvMsg: { // 參數驗證是一個Object
// 允許的類型
type: [Object, Array],
// 是否是必須傳遞
required: true,
// 如果沒有傳遞的默認值
default() {
return "默認值";
},
// 驗證,當驗證失敗后,會在調試台顯示錯誤
validator(v) {
// v就是父組件傳遞過來的數據
return v.id;
},
},
},
template: "#cpn-template",
data: function () {
return {}
}
}
const app = new Vue({
el: "#app",
data: {
sendMsg: {
// id: 1,
name: "admin",
}
},
components: { // Vue實例內部進行注冊
cpn,
}
})
</script>
</body>
由於父組件中傳遞的數據不具有id字段,所以控制台拋出異常,但是不會影響正常渲染:

$emit
當子組件中發生某一個事件,我們可以使用父組件對其進行處理。
使用$emit進行自定義事件,由父組件進行處理,示例如下:
我們使用子組件定義了一個加法的運算,但是結果卻是在父組件中顯示,需要子組件將計算結果發送給父組件。
如果自定義事件的單詞有多個,則在Js中采用駝峰形式,在html中采用 - 分割形式

<body>
<div id="app">
<!-- 父組件監聽add事件,並且交由fatherAdd進行處理-->
<cpn @add="fatherAdd"></cpn>
結果:{{showResult}}
<!-- 結果顯示在父組件 但是計算確是在子組件 -->
</div>
<!-- 子組件模板 -->
<template id="cpn-template">
<div>
<input type="text" v-model.number="n1"> + <input type="text" v-model.number="n2">
<button @click="sum">計算</button>
</div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
template: "#cpn-template",
data() {
return {
n1: 0,
n2: 0,
}
},
methods: {
sum() {
let sonResult = this.n1 + this.n2;
// 自定義了一個add事件,由父組件進行監聽。並且傳遞了一個值
this.$emit("add", sonResult);
}
}
}
const app = new Vue({
el: "#app",
components: { // Vue實例內部進行注冊
cpn,
},
data: {
showResult: 0,
},
methods: {
fatherAdd(v) {
this.showResult = v;
}
}
})
</script>
</body>
還是畫一張圖梳理一下流程,這其實都是固定的用法:

.sync
如果子組件的數據來自於父組件,當子組件中的數據發生改變時我們也想讓父組件中的數據發生同樣的改變。
則可以使用.sync修飾符(盡量少用,會破壞單一性),如下所示:

<body>
<div id="app">
<span>父組件的值:{{num}}</span>
<cpn :son-num.sync="num"></cpn>
</div>
<!-- 子組件模板 -->
<template id="cpn-template">
<div>
<span>子組件的值:{{sonNum}}</span>
<p><input type="text" @keyup="changeValue" v-model="newValue" placeholder="輸入新的值"></p>
</div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
props: ["sonNum",],
template: "#cpn-template",
data: function () {
return {
newValue: this.sonNum,
}
},
methods: {
changeValue() {
this.$emit("update:sonNum", this.newValue)
}
}
}
const app = new Vue({
el: "#app",
data: {
num: 100,
},
components: { // Vue實例內部進行注冊
cpn,
},
})
</script>
</body>
流程圖如下:

組件訪問
$children
有的時候我們想直接通過父組件拿到子組件這個對象,調用其下面的某一個方法,可以使用$children屬性完成操作。
如下所示,父組件想調用子組件中的show()方法:
一個父組件可能有多個子組件,所以該屬性$children是一個Array

<body>
<div id="app">
<button @click="btn">父組件調用子組件方法</button>
<cpn></cpn>
</div>
<!-- 子組件模板 -->
<template id="cpn-template">
<div>
<span>{{msg}}</span>
</div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
template: "#cpn-template",
data: function () {
return {
msg: "子組件show未調用",
}
},
methods: {
show() {
this.msg = "子組件show已調用";
}
}
}
const app = new Vue({
el: "#app",
components: { // Vue實例內部進行注冊
cpn,
},
methods: {
btn() {
// 取出第0個子組件,進行調用其下方法
this.$children[0].show();
}
}
})
</script>
</body>
$refs
上述的訪問方法並不常用,因為父組件非常依賴索引值來訪問子組件。
使用$refs來訪問子組件就方便的多,我們需要給子組件取一個名字,再用父組件進行調用,這個是非常常用的手段。

<body>
<div id="app">
<button @click="btn">父組件調用子組件方法</button>
<!-- 取名字 -->
<cpn ref="nbcpn"></cpn>
</div>
<!-- 子組件模板 -->
<template id="cpn-template">
<div>
<span>{{msg}}</span>
</div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
template: "#cpn-template",
data: function () {
return {
msg: "子組件show未調用",
}
},
methods: {
show() {
this.msg = "子組件show已調用";
}
}
}
const app = new Vue({
el: "#app",
components: { // Vue實例內部進行注冊
cpn,
},
methods: {
btn() {
// 根據取的名字進行調用
this.$refs.nbcpn.show();
}
}
})
</script>
</body>
$parent
如果在子組件中想拿到父組件的對象,使用$parent即可,如果存在多層嵌套,它只會拿自己上一層。
一個子組件,在一塊被掛載區域中只有一個父組件

<body>
<div id="app">
{{msg}}
<cpn></cpn>
</div>
<!-- 子組件模板 -->
<template id="cpn-template">
<div>
<button @click="btn">子組件調用父組件方法</button>
</div>
</template>
<script src="./vue.js"></script>
<script>
var cpn = {
template: "#cpn-template",
data: function () {
return {}
},
methods: {
btn() {
this.$parent.show();
}
}
}
const app = new Vue({
el: "#app",
data: {
msg: "父組件show未調用",
},
components: { // Vue實例內部進行注冊
cpn,
},
methods: {
show() {
// 取出自己的父組件,進行調用其下方法
this.msg = "父組件show已調用";
}
}
})
</script>
</body>
$root
如果存在三級或以上嵌套,可以直接使用$root來訪問根組件。與$parent使用相同,但是它是具體的一個對象,而並非Array,所以這里不再做演示。
組件的動態切換
使用:is屬性,可以讓我們動態使用不同的組件。
如下,定義了一個input框和文本域兩個組件,當我們點擊不同按鈕,它就會選擇不同的組件。

<body>
<div id="app">
<div :is="choice"></div>
<input type="radio" v-model="choice" value="inputCPN">文本框
<input type="radio" v-model="choice" value="textareaCPN">文本域
</div>
<script src="./vue.js"></script>
<script>
const inputCPN = {
template: "<div><input type='text'/></div>",
}
const textareaCPN = {
template: "<div><textarea></textarea></div>",
}
const app = new Vue({
el: "#app",
data: {
choice: "inputCPN",
},
components: {
inputCPN, textareaCPN,
},
})
</script>
</body>
組件的鈎子函數
一個Vue的項目就是由不同的組件構成,不管是局部注冊也好,全局注冊也罷,Vue官方都給你提供了一些鈎子函數供你調用,如下圖所示:

| 鈎子函數 | 描述 |
|---|---|
| beforeCreate | 創建Vue實例之前調用 |
| created | 創建Vue實例成功后調用(可以在此處發送異步請求后端數據) |
| beforeMount | 渲染DOM之前調用 |
| mounted | 渲染DOM之后調用 |
| beforeUpdate | 重新渲染之前調用(數據更新等操作時,控制DOM重新渲染) |
| updated | 重新渲染完成之后調用 |
| beforeDestroy | 銷毀之前調用 |
| destroyed | 銷毀之后調用 |
如下所示,定義這幾個鈎子函數:

<div id="app">
<p>{{msg}}</p>
</div>
<script src="./vue.js"></script>
<script>
let app = new Vue({
el: "#app",
data:{
msg:"HELLO,VUE",
},
beforeCreate() {
console.log("創建Vue實例之前...");
},
created() {
console.log("創建Vue實例成功...");
},
beforeMount() {
console.log("准備渲染DOM...");
},
mounted() {
console.log("渲染DOM完成...");
},
beforeUpdate() {
console.log("准備重新渲染DOM...");
},
updated() {
console.log("重新渲染DOM完成");
},
// 后兩個暫時體會不到
beforeDestroy() {
console.log("准備銷毀當前實例");
},
destroyed() {
console.log("當前實例銷毀完成...");
}
})
</script>
</body>
