本文章是我最近在公司的一場內部分享的內容。我有個習慣就是每次分享都會先將要分享的內容寫成文章。所以這個文集也是用來放這些文章的,順便也當圖床用。
1. 認識Vue.js
Vue.js(讀音 /vju?/,類似于view)是一套構建用戶界面的漸進式框架。
如果你有react或者Angular開發經驗,你肯定不會對Vue.js感到太過陌生。Vue.js是踩在Angular和React肩膀上的后來者,它充分吸收了二者的優點,是MVVM框架的集大成者。我們只需要花10分鐘寫一點代碼,就能大概窺見Vue的本質。
1.1 數據綁定
所有的MVVM框架要解決的第一件事都是數據綁定。首先要將Model的變化渲染到View中,當有用戶輸入還需要把用戶的修改反映到Model中。所謂的MVVM就是這么來的。
<!DOCTYPE html>
<html>
<head>
<title>Hello Vue</title>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{ message }}
</div>
</body>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue'
}
})
</script>
</html>
在瀏覽器打開這個HTML文件后,可以看到頁面上顯示了“Hello Vue”字樣。我們在控制臺輸入app.message = 'hello world'
并回車后,發現頁面上的消息也變成了“Hello World”。你會發現這一切都是響應式的!Vue在背后為我們搞定了數據到視圖的綁定,然而這一切并沒有什么黑魔法,這背后的原理是Object.defineProperty
和對象的存取器屬性。

這是Vue官網的一張圖,高度概括了響應式數據綁定的原理。使用Object.defineProperty
將data
中的所有屬性都轉為存取器屬性,然后在首次渲染過程中把屬性的依賴關系記錄下來并為這個Vue實例添加觀察者。當數據變化時,setter
會通知觀察者數據變動,最后由觀察者觸發render
函數進行再次渲染。
理解了這個,就不難理解Vue中視圖到數據的綁定了:
<!DOCTYPE html>
<html>
<head>
<title>Hello Vue</title>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>Welcom {{ name }}!</p>
<input type="text" placeholder="Enter your name" v-model="name">
</div>
</body>
<script>
var app = new Vue({
el: '#app',
data: {
name: ''
}
})
</script>
</html>
1.2 條件、循環與事件
Vue中可以很方便地進行條件渲染、循環渲染和事件綁定。我們將通過一個列表的例子來體驗:
<!DOCTYPE html>
<html>
<head>
<title>Hello Vue</title>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
}
body {
padding: 20px;
}
.students {
margin: 0;
padding: 0 0 20px 0;
list-style: none;
}
.students > li {
padding: 20px;
border-bottom: 1px solid #ddd;
}
</style>
</head>
<body>
<div id="app">
<ul class="students">
<li v-for="student in students">
<h3 class="name">
{{student.name}}
<span>{{student.age}}</span>
</h3>
<p v-if="Number(student.age) > 18">{{student.profile}}</p>
<button v-on:click="sayHi(student.name)">Say hi</button>
</li>
</ul>
</div>
</body>
<script>
var students = [
{
name: 'Susan',
age: 17,
profile: 'Hi there I\'m a dog! Wang Wang!'
},
{
name: 'Amanda',
age: 21,
profile: 'Kneel Down, Human! Miao~'
},
{
name: 'Lench',
age: 25,
profile: 'боевой народ!!'
}
]
new Vue({
el: '#app',
data: {
students
},
methods: {
sayHi (name) {
window.alert('Hi '+ name)
}
}
})
</script>
</html>
1.3 組件系統
我們今天的重點是Vue的組件系統。在Vue中定義和使用一個組件非常簡單:
<!DOCTYPE html>
<html>
<head>
<title>Hello Vue</title>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<my-component-a></my-component-a>
<my-component-b></my-component-b>
</div>
</body>
<script>
// register a global component
Vue.component('my-component-a', {
template: `<div>custom component A!</div>`
})
var app = new Vue({
el: '#app',
data: {},
components: {
'my-component-b': { // register a local component
template: '<div>custom component B!</div>'
}
}
})
console.log(myComponentA, app)
</script>
</html>
我們在這里分別注冊了一個全局組件和一個局部組件。所謂全局組件就是一旦注冊,所有的Vue實例中都可任意使用而不需要再單獨聲明;局部組件則是只有當前Vue實例可以使用該組件。
另外,既然是組件系統,肯定會有生命周期。在Vue中組件實質上就是Vue實例,Vue實例的生命周期就是組件的生命周期:

在進一步了解Vue的組件系統之前,有一個概念我們需要先來統一,就是組件化。
2. 組件化
2.1 組件化的定義
將實現頁面某一部分功能的結構、樣式和邏輯封裝成為一個整體,使其高內聚,低耦合,達到分治與復用的目的。
在前端范疇,我們可以用下面的這張圖來簡單地理解組件化:
這樣看起來,組件化前端開發就像造一輛車,我們將輪子、發動機、懸掛、車身車門等等各部分組裝成一輛車,輪子、發動機就是組件,車就是最終產品。我們將頁頭、側邊欄、頁腳、內容區等等組件拼裝起來組成了我們的頁面。
2.2 組件化的意義
分而治之
在談到組件化的意義時,很多人的看法都是組件化的目的是復用,但我并不贊同這一看法。
良好地組件化以后的組件,會表現出高內聚低耦合的特征,這會給我們帶來好處:
- 組件之間不會相互影響,能有效減少出現問題時定位和解決問題的時間
- 組件化程度高的頁面,具有清晰的頁面組織和高可讀性的HTML結構代碼,組件之間的關系一目了然
- 組件化會強迫開發人員劃清各個組件的功能邊界,使得開發出的功能更加健壯
所以分而治之才是組件化的意義所在,復用只是它的副作用。同時我們還有很多其他方式都可以做到復用,這并不是組件化的專利。
2.3 組件化與模塊化
有時候我們可能會分不清組件化和模塊化的區別。
模塊化是一種處理復雜系統分解成為更好的可管理模塊的方式。它可以通過在不同組件設定不同的功能,把一個問題分解成多個小的獨立、互相作用的組件,來處理復雜、大型的軟件。[^2]
這段話出《Java應用架構設計》,似乎在后端領域,組件化和模塊化說的是同一件事。但在我的理解中,前端領域的組件化和模塊化是兩個概念。先說結論
組件化是從產品功能角度進行分割,模塊化是從代碼實現角度進行分割,模塊化是組件化的前提和基礎。
當我們將一段代碼寫成一個模塊的時候,它有可能是一個函數、一個對象或者其他什么做了一件單一事情的東西,我們將它做成模塊是因為它完成了一個單一的功能,并且這個功能很多地方都可能用得到。
而當一個組件被從產品中抽象出來,它有時候就只是一個模塊,但有時候卻有相對復雜的實現,它就可能會有多個模塊。
我們說一個日期選擇器是一個組件,但實現它的時候,我們分成了計算模塊、渲染模塊、用戶輸入響應模塊等等模塊來實現。一個單一產品功能的實現,可能是由多個模塊來實現的。這樣理解起來,其實可以說組件化是更粗粒度的模塊化,它是在產品功能上的模塊化。說到這里,其實不難理解為什么后端領域可以認為組件化與模塊化是一件事了,這一點交給大家思考。
2.4 組件化在前端工程中的位置
現在市面上的前端團隊的武功等級大概可以用下面的這張圖概括:
今天我們前端領域最先進的工程化水平,在傳統的桌面軟件開發領域中早就被用爛了,所以這都不是什么新概念。但這也是我今天要分享的原因,既然組件化早就大行其道了,那我們是不是可以探討一下在組件化過程中要面對的常見問題,以及如何優雅地運用Vue提供的組件系統進行組件化開發?
2.5 前端組件化開發的常見問題
- 組件隔離(模塊化):既然要組件化,那么第一件事就是實現組件之間的隔離,否則內聚和低耦合就無從談起。組件隔離其實就是模塊化,這里我們需要實現CSS模塊化和JS模塊化。
- 組件間通信:高內聚低耦合必然會帶來數據流動上的壁壘,所以隔離后的組件就要解決組件之間的通信處理。組件通信分為父子組件通信和非父子組件通信,這就涉及到接口設計、事件處理和狀態管理三塊內容。
- 內容分發:有時候我們希望抽象的是組件的某種行為模式或交互方式,而組件中包含的內容卻是需要使用組件時才能確定,這雖然本質上也是組件間通信,但它的方式更為直觀和方便。內容分發涉及到具名/非具名內容分發,子組件向分發內容傳遞數據等。
- 遞歸和循環引用:組件本質上也是模塊,那么肯定也需要面對模塊會面對的問題,遞歸和循環引用。
- 按需加載:既然已經組件化了,那么更進一步應該實現組件的按需加載,從而提高產品體驗
3. Vue中的組件化
Vue在組件化上針對上述問題給出了很完整的解決方案。
3.1 單文件組件系統與CSS局部作用域
之前我們已經看到了Vue中是如何注冊和使用一個組件的,然而很多時候一個組件本身的結構和邏輯都遠遠比這要多和復雜,在這種時候僅僅依靠對象實例這種形式,就會出現諸多不便,同時基本沒有什么好的辦法來實現CSS隔離。
<style lang="scss" scoped>
.my-component {
color: red;
}
</style>
<template>
<div class="my-component">
{{ message }}
</div>
</template>
<script>
export default {
data () {
return {
message: 'This is my component!'
}
}
}
</script>
Vue給我們提供了單文件組件系統,在這套系統中,我們可以使用一個.vue
后綴的文件來組織組件,這個文件內的結構像極了普通的html
文件:一個表示結構的template
標簽,一個編寫樣式的style
標簽,和一個表示邏輯的script
標簽。
在script中我們將組件輸出為一個模塊,利用ES6的Module系統來作為隔離組件的基礎。同時我想你已經注意到了style標簽中的這個scoped
屬性,它意味著當前組件的樣式是局部的,不會影響其他組件。至于如何實現的,非常簡單:
Webpack的vue-style-load
會在組件的每個元素上添加一個data-v-hash
屬性,然后在其對應的CSS選擇器上添加這個屬性作為選擇器:
這樣就將組件的樣式與其他組件隔離開來。
3.2 Vue組件通信
可以用一張圖來表示Vue組件系統中父子組件的數據流動:
使用props
向子組件傳遞數據,首先要在子組件中定義子組件能接受的props
,然后在父組件中子組件的自定義元素上將數據傳遞給它:
雖然官方并沒有這樣的說法,但我仍舊習慣將子組件的props
叫做它的接口,通過組件的接口,我們可以從外部向組件傳遞數據。但是如果組件需要向外部傳遞數據,則不能通過props
,這是Vue 2與前一代Vue的區別。Vue 2中強調“單項數據流”。跟React中提倡的“單項數據流”一樣,所謂“單向數據流”,即是數據的變動只能由外向內傳遞,而不能由內向外傳遞。組件只能將從接口傳遞進來的數據進行使用,不能對其進行修改:
export default {
props: ['message'],
mounted () {
this.message = 'local message' // Vue will warn you if you try to modify props
}
}
我們唯一能做的,就是在子組件中將props
中傳遞進來的數據賦值給子組件的本地data
變量,然后在修改了這個本地變量的時候,發送事件通知外部。父組件通過監聽子組件發送的這個事件,來決定需要做什么:
<template>
<div>
<input type="text" v-model="localMessage" v-on:change="localMessageChange">
</div>
</template>
<script>
export default {
props: ['message'],
data () {
return {
localMessage: this.message
}
}
methods: {
localMessageChange () {
this.$emit('message-change', localMessage) // notify parent component the change of message
}
}
}
</script>
另外,事件系統也能夠解決非父子組件的通信問題,我們使用一個空的Vue實例來作為中央事件總線,就像這樣:
let bus = new Vue()
bus.$on('a-custom-event', function () {
// handle the event
})
bus.$emit('a-custom-event', 'some custom event data')
講到這里就不得不提Vuex。和Redux一樣,Vuex是Vue官方提供的狀態管理方案。在很多情況下,通過props
和事件系統就基本能滿足我們的需求,但當情況復雜到一定階段(比如咱們的Cube),上述簡單的手段就會讓狀態管理變得不可控,這時應該考慮使用Vuex。
3.3 向子組件分發內容
有時候我們希望將某種“容器”功能抽象出來成為組件,這時它內部的“容納物”就不確定了。我們當然可以完全通過props
向組件傳遞大量的HTML字符串來解決問題,但那樣的寫法相信沒幾個人會喜歡。HTML是用于表示“結構”的,我們自然希望他們出現在他們該出現的位置上。
Vue提供了slot
(插槽)來解決這個問題。父組件可以通過子組件的slot
向子組件中注入HTML:
<template>
<div class="modal">
<slot></slot>
<slot name="operations"></slot>
</div>
</template>
<modal>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<div slot="operations">
<button>cancel</button>
<button>confirm</button>
</div>
</modal>
在Vue 2.1以前,子組件對于通過slot
傳遞進來的HTML是沒有太多手段去控制的,但在2.1版本后,Vue甚至還提供了一個叫做“作用域插槽”的特性,子組件現在可以向被注入的HTML中傳遞數據了!這意味著子組件得到了被注入HTML的數據控制權,它可以自定義每一項的展示行為,更可以將列表項中那些特殊項的共同行為和特征也抽象到子組件內部去,不需要額外在子組件外部進行處理了,舉個不是很恰當的例子:
<!--with out scope slot-->
<my-list>
<li v-for="item in listItem">{{ item.url || item.text }}</li>
</my-list>
<!--with scope slot-->
<my-list :items="listItem">
<template slot="item" scope="props">
<li v-for="item in listItem">{{ props.text }}</li>
</template>
</my-list>
列表組件可以將“優先顯示url”這個特性,通過作用于插槽封裝到組件內部進行處理,不再需要外部去處理了:
<!--my-list component-->
<ul>
<slot
name="item"
v-for="item in items"
:text="item.url || item.text"></slot>
</ul>
這個時候每一項展示的數據來源就不是父組件而是子組件了。到這里我們回過頭來看一看這三個特性:props
、slot
和scope slot
。
使用props
來傳遞數據,是將子組件中的結構和數據的控制權完全封閉到子組件中,父組件只管向其提供數據;如果使用了slot
來分發內容,則是將子組件中的某些結構和數據的控制權完全交給父組件,父組件要將這部分結構和數據渲染好了放到子組件指定的位置中;而scope slot
則是二者的中和,它將數據控制權交給了子組件,將結構控制權交給了父組件。
3.4 Vue組件的遞歸與循環引用
大部分模塊系統都會需要處理遞歸和循環引用這兩個問題。Vue組件系統中對這兩個問題的處理非常優雅,首先是遞歸:
<template>
<ul class="admin-menu" :class="isTopLevel ? 'top-level' : ''">
<li v-for="item in localItems">
{{ item.text }}
<admin-menu v-if="item.children && item.children.length" :menu-items="item.children"></admin-menu>
</li>
</ul>
</template>
export default {
name: 'admin-menu',
data () {
return {
localItems: this.menuItems
}
},
props: ['meneItems']
}
這是來自于Admin-UI中的組件admin-menu
中的實現,Vue中的組件只要給定了name
屬性,就能夠很自然地進行遞歸調用,只要確保遞歸有停止條件即可。所以通常遞歸會與v-if
、v-for
等配合使用。
組件引用自身為遞歸引用,AB組件互相引用則為循環引用。Vue.component()
方法內部自動處理了這種循環引用,你不僅不需要擔心這是個循環引用,你甚至可以將這個特性作為優勢進行充分利用。但當使用的是ES2015的模塊系統來引入的組件,Webpack就會報循環引用錯誤了。
為了解釋為什么會報錯,簡單的將上面兩個組件稱為 A 和 B ,模塊系統看到它需要 A ,但是首先 A 需要 B ,但是 B 需要 A, 而 A 需要 B,陷入了一個無限循環,因此不知道到底應該先解決哪個。要解決這個問題,我們需要在其中一個組件中(比如 A )告訴模塊化管理系統,“A 雖然需要 B ,但是不需要優先導入 B”
Vue的官方教程上說的非常清楚,只要讓兩個組件的導入不同時發生,就可以規避這個問題。那么事情就簡單了,我們在其中一個組件中注冊另一個組件的時候再去引入它就錯開了它們的引入時間:
// a.vue
export default {
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./b.vue')
}
}
3.5 配合Webpack實現組件按需加載
在大型應用中,我們可能需要將應用拆分為多個小模塊,按需從服務器下載。為了讓事情更簡單, Vue.js 允許將組件定義為一個工廠函數,動態地解析組件的定義。Vue.js 只在組件需要渲染時觸發工廠函數,并且把結果緩存起來,用于后面的再次渲染。
Vue.component('async-webpack-example', function (resolve) {
// 這個特殊的 require 語法告訴 webpack
// 自動將編譯后的代碼分割成不同的塊,
// 這些塊將通過 Ajax 請求自動下載。
require(['./my-async-component'], resolve)
})
3.6 vue-cli實例演示(待定)
使用Node作服務器,制作一個TODO List頁面,實現增刪改查
4. 其他
4.1 組件層級劃分
依據與業務的耦合程度,由低到高,我們可以將組件分為三個層次:UI組件,應用組件和業務組件。
UI組件主要是大部分由UI庫提供的業務無關的純UI渲染組件,三者中它的粒度最細,每個組件就完成一個UI功能;同時因為無關業務它可以在項目間具有通用性。
應用組件則是與業務有一定耦合的組件,它是基于UI組件進行的封裝或組合,粒度與UI組件類似,但帶上了一定的業務屬性,僅在本項目通用。
業務組件則是完成某個具體業務的組件,它是基于UI組件和應用組件進行的封裝或組合,粒度最粗,具有針對性的業務屬性,它不需要也不具備通用性。
反映到實現中,可以用一個例子來理解:列表組件 -> 用戶列表組件 -> 用戶管理組件。基于這種分層,從文件組織,到組件劃分,都會有一些最佳實踐。
- 適度的組件嵌套:a->b->c->d->e->f...當嵌套層級過多時會帶來另一個極端,復雜度不降反升。合適的嵌套規則應該是UI組件盡可能相互獨立,不進行嵌套;應用組件是最容易發生過度嵌套的地方,所以它們之間也應該盡可能互相獨立,即使嵌套也請不要超過1層,它們應當純粹由UI組件和業務規則組成;業務組件則僅僅應當由UI組件和應用組件組成,不應該在一個業務組件中嵌套另一個業務組件,這會讓業務邏輯顯得很奇怪
- 良好的組件命名:UI組件的名稱應當反映組件功能,應用組件的名稱應當反映業務屬性和組件功能,業務組件名稱則應當完全體現業務屬性,至于英文還是拼音...我只能說隨緣吧...
-
統一的組件接口:組件的接口命名應當表達一致的語義,類似
message
、text
、items
這樣常用的接口名稱代表的語義和功能盡可能要在項目中得到統一 - 清晰的文件組織:UI組件應當來自項目中引入的UI庫,或者項目中單獨的UI組件文件夾,應用組件應當來自單獨的應用組件文件夾,而業務組件則應當每個業務組件一個文件夾,在其中存放該業務組件相關的一切文件
最后,當我們按照上面的劃分來組織組件的時候,還會面臨一個問題,一個業務組件中,并不完全是由UI組件和應用組件組成的,很多部分其實并不具有任何通用性,那這部分應該如何處理?通常情況下我們會直接將它們寫在業務組件中,所以我們一般見到的業務組件多是自定義組件和原生HTML代碼混雜在一起的。但更優雅的解決方案,是將這部分內容也拿出來做成組件,它們就放置在業務組件自己的目錄中,一旦你這樣做,你會發現你的業務組件中不再出現大塊的原生HTML代碼,取而代之的是語義清晰結構簡明的自定義組件。組件化的首要目的是分治而不是復用,所以即使沒有復用的需求,你也應該有動力去進行組件化。
4.2 ajax是否需要置于組件內
大量的剛剛開始進行組件化的團隊成員們都會對一個問題進行爭論:ajax是否需要封裝到組件內部?
先說結論:不需要也不應該。原因很簡單:解耦。
僅考慮兩種情況:
-
一個應用組件在某個業務組件中引用了兩次:當這個應用組件內部在
created
鉤子中封裝了加載數據請求的ajax時,如果參數相同,那么該組件的請求會在同一個業務組件中被發送兩次 - 項目需要進行統一的ajax管理和優化:當組件內部存在ajax邏輯的時候,統一的ajax管理和優化會變得麻煩
還有更多的坑我沒有列出來,所以出于解耦的目的,盡可能不要將ajax邏輯封裝到組件中,組件僅關心渲染邏輯即可。
4.3 為什么選擇Vue
安利一波Vue給大家:
-
快速上手,事實上Vue沒有改變傳統的開發模式,我們在
style
中寫樣式,我們在template
中寫模板,我們在script
中寫邏輯,同時文檔極其完善,各種語言都有,所以不關你是老鳥還是新手,都能非??焖俚厣鲜諺ue進行開發 - 全姿勢解鎖,數據驅動、HTML模板與JSX三者兼得,不喜歡Vue的姿勢?沒關系,什么姿勢都可以,你可以像寫React一樣去寫Vue,也可以像寫Angula一樣去寫Vue
- 強大的項目模板,超好用的項目模板——vue-cli,比create-react-app不知道高到哪里去了
- 性能強悍,基本上Vue的渲染性能是React的差不多兩倍,至于Angular...我不說了
- 可愛的開發者,接地氣的開發者:尤雨溪活躍在知乎、github、stackoverflow等國內外各大平臺,而React和Angular則是facebook和Google團隊在維護,你很難接觸到他們
- 腦殘粉,我喜歡我喜歡我喜歡
4.4 Admin-UI:
最后,再安利一波我們出的Admin-UI庫給大家(暫未開源)。
Admin-UI是一套基于Vue,用于PC端的UI庫。就像名字那樣,這套UI庫主要用于PC端的后臺管理系統。這一類系統對樣式的定制要求比較低,相應地我們希望用于其中的UI庫能夠帶來更快速的開發體驗。與BootStrapde的大而全不一樣的是,我們對Admin-UI的預期是小而美,借此盡可能降低使用者的學習成本,加速開發。