打造 Vue.js 可復(fù)用組件

Vue.js 是一套構(gòu)建用戶界面的漸進式框架荧关。我們可以使用簡單的 API 來實現(xiàn)響應(yīng)式的數(shù)據(jù)綁定和組合的視圖組件溉奕。

從維護視圖到維護數(shù)據(jù),Vue.js 讓我們快速地開發(fā)應(yīng)用忍啤。但隨著業(yè)務(wù)代碼日益龐大加勤,組件也越來越多,組件邏輯耦合嚴重,使代碼維護變得十分困難鳄梅。

同時叠国,Vue.js 的接口和語法十分自由咖刃,實現(xiàn)同一功能有若干種方法篱瞎。每個人解決問題的思路不一樣拂盯,寫出來的代碼也就不一樣暑竟,缺乏團隊內(nèi)的規(guī)范揽祥。

本文旨在從組件開發(fā)的不同方面列舉出合理的解決方法舟舒,作為建立組件規(guī)范的一個參考抒抬。

導(dǎo)航

  • 構(gòu)成組件
  • 組件間通信
  • 業(yè)務(wù)無關(guān)
  • 命名空間
  • 上下文無關(guān)
  • 數(shù)據(jù)扁平化
  • 使用自定義事件實現(xiàn)數(shù)據(jù)的雙向綁定
  • 使用自定義 watcher 優(yōu)化 DOM 操作
  • 項目骨架

構(gòu)成組件

組件置侍,是一個具有一定功能马篮,且不同組件間功能相對獨立的模塊沾乘。組件可以是一個按鈕、一個輸入框浑测、一個視頻播放器等等翅阵。

可復(fù)用組件,高內(nèi)聚迁央、低耦合掷匠。

那么,什么構(gòu)成了組件呢岖圈。以瀏覽器的原生組件 video 為例讹语,分析一下組件的組成部分。

<video
  src="example.mp4"
  width="320"
  height="240"
  onload="loadHandler"
  onerror="errorHandler">
  Your browser does not support the video tag.
</video>

實例中能看出蜂科,組件由狀態(tài)顽决、事件和嵌套的片斷組成。狀態(tài)导匣,是組件當(dāng)前的某些數(shù)據(jù)或?qū)傩圆挪ぃ?video 中的 src、width 和 height贡定。事件赋访,是組件在特定時機觸發(fā)一些操作的行為,如 video 在視頻資源加載成果或失敗時會觸發(fā)對應(yīng)的事件來執(zhí)行處理缓待。片段蚓耽,指的是嵌套在組件標(biāo)簽中的內(nèi)容,該內(nèi)容會在某些條件下展現(xiàn)出來命斧,如在瀏覽器不支持 video 標(biāo)簽時顯示提示信息田晚。

在 Vue 組件中,狀態(tài)稱為 props国葬,事件稱為 events贤徒,片段稱為 slots芹壕。組件的構(gòu)成部分也可以理解為組件對外的接口。良好的可復(fù)用組件應(yīng)當(dāng)定義一個清晰的公開接口接奈。

  • Props 允許外部環(huán)境傳遞數(shù)據(jù)給組件
  • Events 允許組件觸發(fā)外部環(huán)境的副作用
  • Slots 允許外部環(huán)境將額外的內(nèi)容組合在組件中踢涌。

使用 vue 對 video 組件做拓展,構(gòu)造出一個支持播放列表的組件 myVideo:

<my-video
  :playlist="playlist"
  width="320"
  height="240"
  @load="loadHandler"
  @error="errorHandler"
  @playnext="nextHandler"
  @playprev="prevHandler">
  <div slot="endpage"></div>
</my-video>

myVideo 組件有著清晰的接口序宦,接收播放列表睁壁、播放器寬高等狀態(tài),能夠觸發(fā)加載成功或失敗互捌、播放上一個或下一個的事件潘明,并且能自定義播放結(jié)束時的尾頁,可用于插入廣告或顯示下一個視頻信息秕噪。

組件間通信

在 Vue.js 中钳降,父子組件的關(guān)系可以總結(jié)為 props down, events up 。父組件通過 props 向下傳遞數(shù)據(jù)給子組件腌巾,子組件通過 events 給父組件發(fā)送消息遂填。看看它們是怎么工作的澈蝙。

業(yè)務(wù)無關(guān)

命名

組件的命名應(yīng)該跟業(yè)務(wù)無關(guān)吓坚。應(yīng)該依據(jù)組件的功能為組件命名。

例如灯荧,一個展示公司部門的列表礁击,把每一項作為一個組件,并命名為 DepartmentItem逗载。這時客税,有一個需求要展示團隊人員列表,樣式跟剛剛的部門列表一樣撕贞。顯然,DepartmentItem 這個名字就不適合了测垛。

因此捏膨,可復(fù)用組件在命名上應(yīng)避免跟業(yè)務(wù)扯上關(guān)系,以組件的角色食侮、功能對其命名号涯。Item、ListItem锯七、Cell链快。可以參考 Bootstrap眉尸、ElementUI 等一些 UI 框架的命名域蜗。

業(yè)務(wù)數(shù)據(jù)無關(guān)

可復(fù)用組件只負責(zé) UI 上的展示和一些交互以及動畫巨双,如何獲取數(shù)據(jù)跟它無關(guān),因此不要在組件內(nèi)部去獲取數(shù)據(jù)霉祸,以及任何與服務(wù)端打交道的操作筑累。可復(fù)用組件只實現(xiàn) UI 相關(guān)的功能丝蹭。

組件職責(zé)

約束好組件的職責(zé)慢宗,能讓組件更好地解耦,知道什么功能是組件實現(xiàn)的奔穿,什么功能不需要實現(xiàn)镜沽。

組件可以分為通用組件(可復(fù)用組件)和業(yè)務(wù)組件(一次性組件)。

可復(fù)用組件實現(xiàn)通用的功能(不會因組件使用的位置贱田、場景而變化):

  • UI 的展示
  • 與用戶的交互(事件)
  • 動畫效果

業(yè)務(wù)組件實現(xiàn)偏業(yè)務(wù)化的功能:

  • 獲取數(shù)據(jù)
  • 和 vuex 相關(guān)的操作
  • 埋點
  • 引用可復(fù)用組件

可復(fù)用組件應(yīng)盡量減少對外部條件的依賴缅茉,所有與 vuex 相關(guān)的操作都不應(yīng)在可復(fù)用組件中出現(xiàn)。

組件應(yīng)當(dāng)避免對其父組件的依賴湘换,不要通過 this.$parent 來操作父組件的示例宾舅。父組件也不要通過 this.$children 來引用子組件的示例,而是通過子組件的接口與之交互彩倚。

命名空間

可復(fù)用組件除了定義一個清晰的公開接口外筹我,還需要有命名空間。命名空間可以避免與瀏覽器保留標(biāo)簽和其他組件的沖突帆离。特別是當(dāng)項目引用外部 UI 組件或組件遷移到其他項目時蔬蕊,命名空間可以避免很多命名沖突的問題。

<xl-button></xl-button>
<xl-table></xl-table>
<xl-dialog></xl-dialog>
...

業(yè)務(wù)組件也可以有命令空間哥谷,跟通用組件區(qū)分開岸夯。這里用 st (section) 來代表業(yè)務(wù)組件。

<st-recommend></st-recommend>
<st-qq-movie></st-qq-movie>
<st-sohu-series></st-sohu-series>

上下文無關(guān)

還是上面那句話们妥,可復(fù)用組件應(yīng)盡量減少對外部條件的依賴猜扮。沒有特別需求且單個組件不至于過重的的前提下,不要把一個有獨立功能的組件拆分成若干個小組件监婶。

<table-wrapper>         
  <table-header slot="header" :headers="exampleHeader"></table-header>          
  <table-body slot="body" :body-content="exampleContents"></table-body>       
</table-wrapper>

TableHeader 組件和 TableBody 組件依賴當(dāng)前的上下文旅赢,即 TableWrapper 組件嵌套的環(huán)境下。你可以有更好的解決辦法:

<xl-table :headers="exampleHeader" :body-content="exampleContents"></xl-table>

上下文無關(guān)原則能夠降低組件使用的門檻惑惶。

數(shù)據(jù)扁平化

定義組件接口時煮盼,盡量不要將整個對象作為一個 prop 傳進來。

<!-- 反例 -->
<card :item="{ title: item.name, description: item.desc, poster: item.img }></card>

每個 prop 應(yīng)該是一個簡單類型的數(shù)據(jù)带污。這樣做有下列幾點好處:

  • 組件接口清晰
  • props 校驗方便
  • 當(dāng)服務(wù)端返回的對象中的 key 名稱與組件接口不一樣時僵控,不需要重新構(gòu)造一個對象
<card
  :title="item.name"
  :description="item.desc"
  :poster="item.img">
</card>

扁平化的 props 能讓我們更直觀地理解組件的接口。

使用自定義事件實現(xiàn)數(shù)據(jù)的雙向綁定

有時候鱼冀,對于一個狀態(tài)报破,需要同時從組件內(nèi)部和組件外部去改變它悠就。

例如,模態(tài)框的顯示和隱藏泛烙,父組件可以初始化模態(tài)框的顯示理卑,模態(tài)框組件內(nèi)部的關(guān)閉按鈕可以讓其隱藏。一個好的辦法是蔽氨,使用自定義事件改變父組件中的值:

<modal :show="show" @showchange="show = argument[0]"></modal>
<!-- Modal.vue -->

<template>
  <div v-show="show">
    <h3>標(biāo)題</h3>
    <p>內(nèi)容</p>
    <a href="javascript:;" @click="close">關(guān)閉</a>
  </div>
</template>

<script>
  export default {
    props: {
      show: String
    },
    methods: {
      close () {
        this.$emit('input', false)
      }
    }
  }
</script>

用戶點擊關(guān)閉按鈕時藐唠,Modal 組件發(fā)送一個 input 自定義事件給父組件。父組件監(jiān)聽到 input 事件時鹉究,把 show 設(shè)置為事件回調(diào)的第一個參數(shù)宇立。

特別地,當(dāng)狀態(tài)名稱為 value自赔,事件名稱為 input 時妈嘹,可以使用 v-model 指令語法糖:

<modal :value="show" @input="show = argument[0]"></modal>

等價于

<modal v-model="show"></model>

要讓組件的 v-model 生效,它必須:

  • 接受一個 value 屬性
  • 在有新的 value 時觸發(fā) input 事件

注意:由于每個組件的 input 事件只能用來對一個數(shù)據(jù)進行雙向綁定绍妨,所以當(dāng)存在多個需要向上同步的數(shù)據(jù)時润脸,請不要使用 v-model,請使用多個自定義事件他去,并在父組件中同步新的值毙驯。

<modal
  :show="show" @showchange="show = argument[0]"
  :content="content" @contentchange="content = argument[0]">
</model>

使用自定義 watcher 優(yōu)化 DOM 操作

在開發(fā)中,有些邏輯無法使用數(shù)據(jù)綁定灾测,無法避免需要對 DOM 的操作爆价。例如,視頻的播放需要同步 Video 對象的播放操作及組件內(nèi)的播放狀態(tài)媳搪∶危可以使用自定義 watcher 來優(yōu)化 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: {
      // 播放狀態(tài)變化時秦爆,執(zhí)行對應(yīng)操作
      playing (val) {
        let video = this.$refs.video
        if (val) {
          video.play();
        } else {
          video.pause();
        }
      }
    },
    method: {
      // 切換播放狀態(tài)
      togglePlay () {
        this.playing = !this.playing
      }
    }
  }
</script>

示例中序愚,自定義 watcher 在監(jiān)聽到 playing 狀態(tài)變化時,會執(zhí)行播放或暫停操作等限。遇到對視頻播放狀態(tài)的處理時展运,只需要關(guān)注 playing 狀態(tài)即可。

項目骨架

單組件不異過重精刷,組件在功能獨立的前提下應(yīng)該盡量簡單,越簡單的組件可復(fù)用性越強蔗候。當(dāng)你實現(xiàn)組件的代碼怒允,不包括CSS,有好幾百行了(這個大小視業(yè)務(wù)而定)锈遥,那么就要考慮拆分成更小的組件纫事。

當(dāng)組件足夠簡單時勘畔,就可以在一個更大的業(yè)務(wù)組件中去自由組合這些組件,實現(xiàn)我們的業(yè)務(wù)功能丽惶。因此炫七,理想情況下,組件的引用層級钾唬,只有兩級万哪。業(yè)務(wù)組件引用通用組件。

我們可以得到一個扁平化的結(jié)構(gòu)抡秆。

在一個龐大的項目當(dāng)中奕巍,組件間的引用關(guān)系會更復(fù)雜一些。當(dāng)單頁應(yīng)用有多個路由儒士,每個路由組件過重的止,需要拆分模塊時。組件結(jié)構(gòu)會變成下圖這樣着撩。

按照這個思路構(gòu)建我們的項目诅福,最后的源代碼目錄結(jié)構(gòu)(不包括構(gòu)建流程文件):

│  App.vue          # 頂級組件
│  client-entry.js  # 前端入口文件
│  config.js        # 配置文件
│  main.js          # 主入口文件
│  
├─api               # 接口 API
├─assets            # 靜態(tài)資源
├─components        # 通用組件
├─directives        # 自定義指令
├─mock              # Mock 數(shù)據(jù)
├─plugins           # 自定義插件
├─router            # 路由配置
├─sections          # 業(yè)務(wù)組件
├─store             # Vuex Store
├─utils             # 工具模塊
└─views             # 路由頁面組件

在通用組件中還可以區(qū)分容器組件、布局組件和其他功能性組件等拖叙。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末氓润,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子憋沿,更是在濱河造成了極大的恐慌旺芽,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辐啄,死亡現(xiàn)場離奇詭異采章,居然都是意外死亡,警方通過查閱死者的電腦和手機壶辜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門悯舟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人砸民,你說我怎么就攤上這事抵怎。” “怎么了岭参?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵反惕,是天一觀的道長。 經(jīng)常有香客問我演侯,道長姿染,這世上最難降的妖魔是什么秒际? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任悬赏,我火速辦了婚禮狡汉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闽颇。我一直安慰自己盾戴,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布兵多。 她就那樣靜靜地躺著尖啡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪中鼠。 梳的紋絲不亂的頭發(fā)上可婶,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音援雇,去河邊找鬼矛渴。 笑死,一個胖子當(dāng)著我的面吹牛惫搏,可吹牛的內(nèi)容都是我干的具温。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼筐赔,長吁一口氣:“原來是場噩夢啊……” “哼铣猩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茴丰,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤达皿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贿肩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峦椰,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年汰规,在試婚紗的時候發(fā)現(xiàn)自己被綠了汤功。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡溜哮,死狀恐怖滔金,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茂嗓,我是刑警寧澤餐茵,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站述吸,受9級特大地震影響忿族,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一肠阱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朴读,春花似錦屹徘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氮唯,卻和暖如春鉴吹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惩琉。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工豆励, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瞒渠。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓良蒸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伍玖。 傳聞我的和親對象是個殘疾皇子嫩痰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,262評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件窍箍、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,117評論 4 61
  • 本文章是我最近在公司的一場內(nèi)部分享的內(nèi)容串纺。我有個習(xí)慣就是每次分享都會先將要分享的內(nèi)容寫成文章。所以這個文集也是用來...
    Awey閱讀 9,452評論 4 67
  • 不知道你是否有過這樣的錯覺? 我經(jīng)常第一次走在上海某一處四岔路口晰搀,看著信號燈閃爍五辽,恍惚覺得周圍的建筑似曾相識; 又...
    哉小洛閱讀 988評論 0 1
  • 喜歡雨 它小 滋潤我 它大 淋醒我 一陣一陣外恕,一波一波 告訴我一段時間結(jié)束了 討厭雨 它小 讓我猶豫不決 它大 讓...
    烏鴉吃蛋糕閱讀 253評論 2 8