原文地址javascript
Vue.js 是一套構建用戶界面的漸進式框架。咱們可使用簡單的 API 來實現響應式的數據綁定和組合的視圖組件。html
從維護視圖到維護數據,Vue.js 讓咱們快速地開發應用。但隨着業務代碼日益龐大,組件也愈來愈多,組件邏輯耦合嚴重,使代碼維護變得十分困難。前端
同時,Vue.js 的接口和語法十分自由,實現同一功能有若干種方法。每一個人解決問題的思路不同,寫出來的代碼也就不同,缺少團隊內的規範。vue
本文旨在從組件開發的不一樣方面列舉出合理的解決方法,做爲創建組件規範的一個參考。java
組件,是一個具備必定功能,且不一樣組件間功能相對獨立的模塊。組件能夠是一個按鈕、一個輸入框、一個視頻播放器等等。git
可複用組件,高內聚、低耦合。vuex
那麼,什麼構成了組件呢。以瀏覽器的原生組件 video 爲例,分析一下組件的組成部分。api
<video src="example.mp4" width="320" height="240" onload="loadHandler" onerror="errorHandler"> Your browser does not support the video tag. </video>
實例中能看出,組件由狀態、事件和嵌套的片段組成。狀態,是組件當前的某些數據或屬性,如 video 中的 src、width 和 height。事件,是組件在特定時機觸發一些操做的行爲,如 video 在視頻資源加載成果或失敗時會觸發對應的事件來執行處理。片斷,指的是嵌套在組件標籤中的內容,該內容會在某些條件下展示出來,如在瀏覽器不支持 video 標籤時顯示提示信息。瀏覽器
在 Vue 組件中,狀態稱爲 props,事件稱爲 events,片斷稱爲 slots。組件的構成部分也能夠理解爲組件對外的接口。良好的可複用組件應當定義一個清晰的公開接口。ruby
使用 vue 對 video 組件作拓展,構造出一個支持播放列表的組件 myVideo:
<my-video :playlist="playlist" width="320" height="240" @load="loadHandler" @error="errorHandler" @playnext="nextHandler" @playprev="prevHandler"> <div slot="endpage"></div> </my-video>
myVideo 組件有着清晰的接口,接收播放列表、播放器寬高等狀態,可以觸發加載成功或失敗、播放上一個或下一個的事件,而且能自定義播放結束時的尾頁,可用於插入廣告或顯示下一個視頻信息。
在 Vue.js 中,父子組件的關係能夠總結爲 props down, events up 。父組件經過 props 向下傳遞數據給子組件,子組件經過 events 給父組件發送消息。看看它們是怎麼工做的。
組件的命名應該跟業務無關。應該依據組件的功能爲組件命名。
例如,一個展現公司部門的列表,把每一項做爲一個組件,並命名爲 DepartmentItem。這時,有一個需求要展現團隊人員列表,樣式跟剛剛的部門列表同樣。顯然,DepartmentItem 這個名字就不適合了。
所以,可複用組件在命名上應避免跟業務扯上關係,以組件的角色、功能對其命名。Item、ListItem、Cell。能夠參考 Bootstrap、ElementUI 等一些 UI 框架的命名。
可複用組件只負責 UI 上的展現和一些交互以及動畫,如何獲取數據跟它無關,所以不要在組件內部去獲取數據,以及任何與服務端打交道的操做。可複用組件只實現 UI 相關的功能。
約束好組件的職責,能讓組件更好地解耦,知道什麼功能是組件實現的,什麼功能不須要實現。
組件能夠分爲通用組件(可複用組件)和業務組件(一次性組件)。
可複用組件實現通用的功能(不會因組件使用的位置、場景而變化):
業務組件實現偏業務化的功能:
可複用組件應儘可能減小對外部條件的依賴,全部與 vuex 相關的操做都不該在可複用組件中出現。
組件應當避免對其父組件的依賴,不要經過 this.$parent 來操做父組件的示例。父組件也不要經過 this.$children 來引用子組件的示例,而是經過子組件的接口與之交互。
可複用組件除了定義一個清晰的公開接口外,還須要有命名空間。命名空間能夠避免與瀏覽器保留標籤和其餘組件的衝突。特別是當項目引用外部 UI 組件或組件遷移到其餘項目時,命名空間能夠避免不少命名衝突的問題。
<xl-button></xl-button> <xl-table></xl-table> <xl-dialog></xl-dialog> ...
業務組件也能夠有命令空間,跟通用組件區分開。這裏用 st (section) 來表明業務組件。
<st-recommend></st-recommend> <st-qq-movie></st-qq-movie> <st-sohu-series></st-sohu-series>
仍是上面那句話,可複用組件應儘可能減小對外部條件的依賴。沒有特別需求且單個組件不至於太重的的前提下,不要把一個有獨立功能的組件拆分紅若干個小組件。
<table-wrapper> <table-header slot="header" :headers="exampleHeader"></table-header> <table-body slot="body" :body-content="exampleContents"></table-body> </table-wrapper>
TableHeader 組件和 TableBody 組件依賴當前的上下文,即 TableWrapper 組件嵌套的環境下。你能夠有更好的解決辦法:
<xl-table :headers="exampleHeader" :body-content="exampleContents"></xl-table>
上下文無關原則可以下降組件使用的門檻。
定義組件接口時,儘可能不要將整個對象做爲一個 prop 傳進來。
<!-- 反例 --> <card :item="{ title: item.name, description: item.desc, poster: item.img }></card>
每一個 prop 應該是一個簡單類型的數據。這樣作有下列幾點好處:
<card
:title="item.name" :description="item.desc" :poster="item.img"> </card>
扁平化的 props 能讓咱們更直觀地理解組件的接口。
有時候,對於一個狀態,須要同時從組件內部和組件外部去改變它。
例如,模態框的顯示和隱藏,父組件能夠初始化模態框的顯示,模態框組件內部的關閉按鈕可讓其隱藏。一個好的辦法是,使用自定義事件改變父組件中的值:
<modal :show="show" @showchange="show = argument[0]"></modal>
<!-- Modal.vue --> <template> <div v-show="show"> <h3>標題</h3> <p>內容</p> <a href="javascript:;" @click="close">關閉</a> </div> </template> <script> export default { props: { show: String }, methods: { close () { this.$emit('input', false) } } } </script>
用戶點擊關閉按鈕時,Modal 組件發送一個 input 自定義事件給父組件。父組件監聽到 input 事件時,把 show 設置爲事件回調的第一個參數。
特別地,當狀態名稱爲 value
,事件名稱爲 input
時,可使用 v-model
指令語法糖:
<modal :value="show" @input="show = argument[0]"></modal>
等價於
<modal v-model="show"></model>
要讓組件的 v-model 生效,它必須:
注意:因爲每一個組件的 input 事件只能用來對一個數據進行雙向綁定,因此當存在多個須要向上同步的數據時,請不要使用 v-model,請使用多個自定義事件,並在父組件中同步新的值。
<modal
:show="show" @showchange="show = argument[0]" :content="content" @contentchange="content = argument[0]"> </model>
在開發中,有些邏輯沒法使用數據綁定,沒法避免須要對 DOM 的操做。例如,視頻的播放須要同步 Video 對象的播放操做及組件內的播放狀態。可使用自定義 watcher 來優化 DOM 的操做。
<!-- MyVideo.vue --> <template> <div> <video ref="video" src="src"></video> <a href="javascript:;" @click="togglePlay">{{ playing ? '暫停' : '播放' }}</a> </div> </template> <script> export default { props: { src: String // 播放地址 }, data () { return { playing: false // 是否正在播放 } }, watch: { // 播放狀態變化時,執行對應操做 playing (val) { let video = this.$refs.video if (val) { video.play(); } else { video.pause(); } } }, method: { // 切換播放狀態 togglePlay () { this.playing = !this.playing } } } </script>
示例中,自定義 watcher 在監聽到 playing 狀態變化時,會執行播放或暫停操做。遇到對視頻播放狀態的處理時,只須要關注 playing 狀態便可。
單組件不異太重,組件在功能獨立的前提下應該儘可能簡單,越簡單的組件可複用性越強。當你實現組件的代碼,不包括CSS,有好幾百行了(這個大小視業務而定),那麼就要考慮拆分紅更小的組件。
當組件足夠簡單時,就能夠在一個更大的業務組件中去自由組合這些組件,實現咱們的業務功能。所以,理想狀況下,組件的引用層級,只有兩級。業務組件引用通用組件。
咱們能夠獲得一個扁平化的結構。
在一個龐大的項目當中,組件間的引用關係會更復雜一些。當單頁應用有多個路由,每一個路由組件太重,須要拆分模塊時。組件結構會變成下圖這樣。
按照這個思路構建咱們的項目,最後的源代碼目錄結構(不包括構建流程文件):
│ App.vue # 頂級組件 │ client-entry.js # 前端入口文件 │ config.js # 配置文件 │ main.js # 主入口文件 │ ├─api # 接口 API ├─assets # 靜態資源 ├─components # 通用組件 ├─directives # 自定義指令 ├─mock # Mock 數據 ├─plugins # 自定義插件 ├─router # 路由配置 ├─sections # 業務組件 ├─store # Vuex Store ├─utils # 工具模塊 └─views # 路由頁面組件
在通用組件中還能夠區分容器組件、佈局組件和其餘功能性組件等。