關於 vue 全家桶的四個 「最佳實踐」

前言

在讀這篇文章以前,我想先安利你們一個東西: html

Vue.js 組件編碼規範

看到這副黑框眼鏡,你是否是想到了什麼?前端

對,就是它:Vue.js 組件編碼規範。讀過的同窗忽略,沒讀過的同窗有時間的話請花 20 分鐘認真看看,文章的內容都是在承認這篇規範的基礎上展開的。vue

另外,本文中的「最佳實踐」(注意引號),全都是一家之言,不必定對,歡迎各路大佬討論拍磚。ios

實踐一:如何分類組件

組件(component)是 vue 最核心的概念之一,可是正由於這一律念太過寬泛,咱們會在實際開發中看到各類各樣的組件,對開發和維護的同窗帶來了很大的困惑和混亂。這裏我把組件分紅四類:git

view

顧名思義,view 指的是頁面,你也能夠把它叫作 page。它的定義是:和具體的某一條路由對應,在 vue-router 配置中指定。view 是頁面的容器,是其餘組件的入口。它能夠和 vuex store 通訊,再把數據分發給普通組件。github

global component

全局組件,做爲小工具而存在。例如 toast、alert 等。他的特色是具有全局性,直接嵌套在 root 下,而不從屬於哪一個 view。global component 也和 vuex store 通訊,它單獨地使用 state 中的一個 module,這個 state 中的數據專門用來控制 gloabl component 的顯隱和展現,不和其餘業務實體用到的 state 混淆。
其餘組件想修改它,能夠直接派發相應的 mutation。而要監聽它的變化(好比一個全局的confirm,確認以後在不一樣的組件中觸發不一樣的操做),則使用全局事件總線(event bus)。vue-router

simple component

簡單組件。這種組件對應的是 vue 中最傳統的組件概念。它的交互和數據都很少,基本上就是起到一個簡單展現,拆分父組件的做用。這種組件和父組件之間經過最傳統的方式進行通信:父組件將 props 傳入它,而它經過 $emit 觸發事件到父組件。
簡單組件內部是不寫什麼業務邏輯的,它能夠說是生活不能自理,要展現什麼就等着父組件傳入,要幹什麼就 $emit 事件出去讓父組件幹,父組件夠操心的。vuex

complex component

複雜組件。這種組件的特色是,內部包含有不少交互邏輯,經常須要訪問接口。另外,展現的數據也每每比較多。以下圖。vue-cli

圖中紅框內部的就是一個複雜組件的實例。它是一個大列表的列表項,展現的數據不少,並且點擊左下角的幾個 button,還會彈出相應的彈窗,彈窗內有複雜的表單須要填寫提交···邏輯能夠說是至關複雜了。若是這時咱們還拘泥於簡單組件的那種通訊方式,衣來伸手飯來張口,啥事兒不幹,那麼:
1.全部的 props 都由父組件一一傳入,若是有十幾個乃至幾十個要展現的數據,那麼父組件 <template> 內的代碼可不得上天了?
2.全部的業務流程都要 $emit 出去要父組件處理,那麼父組件 <script> 內的代碼可不得上天了?
因此,對於這種複雜組件,咱們應該容許它有必定的「自主權」。能夠跳過父組件,自行和 vuex 通訊,獲取一下 state,派發一下 mutation 和 action,不是很開心麼。

我畫了一張圖來講明上面這四種 component 的關係,但願能幫助你們更好理解。json

在區分了這四種 component 後,咱們在編碼時就能作到內心有數,如今在寫的組件,到底屬於哪一類?每一類以特定的方式編寫和交互,邏輯上就會清晰不少。 使用 vue-cli 構建的項目中都會有一個目錄叫作 component,之前是一股腦往裏塞,如今能夠在此基礎上再設置幾個子目錄,放置不一樣類型的組件。

實踐二:如何優雅地修改 props

先來看一個栗子🌰
假設有一個模態對話框的組件。父組件爲了可以打開模態框,給模態框傳入了一個控制其顯隱的 props,命名爲 visible,type 爲 Boolean,綁定模態框外層的 v-if 指令。那麼,問題來了,若是咱們點擊了模態框內部的關閉按鈕,關閉自身,應該怎麼寫?
固然,最傳統的方式天然仍是模態框拋出事件,父組件中設置監聽,而後修改值。但這種方式無疑有很強的侵入性,無故增長了不少的代碼量。關閉按鈕在模態框內部,關閉本身是我本身的事兒,能不能不讓父組件管這些?
有同窗說了,直接在模態框內部修改 visible 啊。this.visible = false ,不行嗎?
還真不行。若是這麼幹,你會看到如下一堆報錯:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

vue 很明確地告訴你了,做爲子組件,你要安分守己,不準隨便修改老爹傳給你的 props。
那麼咱們應該怎麼辦?

方法一

咱們思考一下,若是不容許修改 props 的值,那咱們修改 porps 的······屬性如何?
事實證實,是能夠的。
咱們能夠把上面 visible 的 type 設爲 Object,模態框的顯隱決定於 visible.value。當模態框想要關閉自身時,只需 this.visible.value = false 便可。 這種方式看起來至關方便,但實際是一種投機取巧的方法。上面安利的 Vue.js 組件編碼規範中明確有一條規範,就是 props 原子化,也就是說,props 裏的字段必須是簡單的 String,Number 或 Boolean。這麼作的緣由是:

  • 使得組件 API 清晰直觀。
  • 只使用原始類型和函數做爲 props 使得組件的 API 更接近於 HTML(5) 原生元素。
  • 其它開發者更好的理解每個 prop 的含義、做用。
  • 傳遞過於複雜的對象使得咱們不可以清楚的知道哪些屬性或方法被自定義組件使用,這使得代碼難以重構和維護。

因此,咱們把 visible 改成 Object,原本就是違反規範的。

方法二

vue 中有種已經存在的機制,和現有需求很像,這就是 v-model。在表單中,每個 input,就像一個子組件。在外層經過 v-model 綁定的值能夠在 input 中回顯,而 input 自己的值也能改變。
事實上,v-model 僅僅是一個語法糖,v-model="xxx",就至關於 :value="xxx" @input="val=>xxx=val"。那麼,咱們就能夠利用 v-model 的這種特性來實現咱們的需求。咱們只須要在模態框內部拋出一個 input 事件 this.$emit('input', false),就能關閉自身了。
這種方式比較簡潔,也不違反規範,可是容易讓人困惑,覺得這裏是要進行什麼表單操做。
咱們還有沒有什麼更好的方式呢?

方法三

若是你是從大版本爲 1 時就開始接觸 vue,那你能夠知道一個修飾符,叫作.sync。若是你是從 2.0 開始接觸的,則極可能不熟悉它。這是由於,vue 在 2.0 版本時把它刪除了,不過好在, 2.3 版本以後,它又回來了
這個修飾符簡直就是爲咱們這個需求量身定製的。它自己是一個和 v-model 相似的語法糖,咱們要作的,僅僅是在組件內部須要改動值的地方,拋出一個 update 事件。this.$emit('update:foo', newValue)。既不違反規範,也足夠清晰,能夠說是最佳的解決方案了。惟一的不足之處,就是對版本有一點要求。

實踐三:如何封裝請求接口

數據是 SPA 的核心,而數據的來源都是接口。如何優雅、高效地經過接口請求數據,是開發者必需要關心的問題。在實踐中,我是這樣封裝接口的:

從高層到底層,依次說明。
第一層就是組件。
第二層則是 vuex 中的 action,咱們在組件中調用 action,基本操做。
第三層是 api。在這裏,咱們預先定義了每個接口。包括接口的 url、type、content-type,以及寫死的請求參數。在 action 中,咱們調用 api 請求接口。
第四層是 request,這是咱們請求的公共方法,做用就是對特定的 http client。 進行封裝,實現一套統一的接口請求——處理流程。
第五層則是以 axios 爲表明的各類 http client。
咱們主要進行編碼的是第三層和第四層,也就是 api 和 request。api 的編寫沒有什麼難點,主要談談 request 的代碼。這部分代碼,咱們要關心如下幾個方面。

  • loading 處理。當請求時間比較長時,要跳出全局的 loading 讓用戶知曉。
  • 錯誤處理。有兩種錯誤,第一種是 http 請求直接返回錯誤碼。第二種,雖然請求的返回值是 200,可是返回結果中提示錯誤。好比返回的 json 中 success: false。對於這兩種錯誤,咱們都要捕獲並處理。
  • api 一致性處理。http client 接受的參數是有講究的,以 axios爲例,get 請求的請求參數爲 params,而 post 請求的參數則爲 data。對於這種差別,request 這層須要將其抹平,api 層不須要在定義接口時關心這些。

下面是示例代碼,可供參考。

if (opt.method === 'post') {
    axiosOpt.data = opt.payload
  } else if (opt.method === 'get') {
    axiosOpt.params = opt.payload
  }
  if (opt.withFile) {
    Object.assign(axiosOpt, { headers: {
      'Content-Type': 'multipart/form-data'
    }})
  }

  // 全局請求的 loading,當請求 300 ms 後還沒返回,纔會出現 loading
  const timer = setTimeout(() => {
    store.dispatch('showLoading', {
      text: '加載數據中'
    })
  }, 300)

  try {
    // 開始請求
    const result = await axios(axiosOpt)
    // 若是 300 ms 還沒到,就取消定時器
    clearTimeout(timer)
    store.dispatch('closeLoading')

    if (result.status === 200 && result.statusText === 'OK') {
      if (result.data.success) {
        return result.data.results || true
      } else {
        // 請求失敗的 toast
        store.dispatch('showAlert', {
          type: 'error',
          text: `請求失敗${result.data.message ? `,信息:${result.data.message}`: ''}`
        })
        return false
      }
    } else {
      return false
    }
  } catch(e) {
    clearInterval(timer)
    // 請求失敗的 toast
    store.dispatch('closeLoading')
    store.dispatch('showAlert', {
      type: 'error',
      text: '請求失敗'
    })
    return false
  }
複製代碼

實踐四:如何決定請求數據的時機

SPA中,每個 view 中的都有不少數據是須要經過接口請求得到的,若是沒有得到,頁面中就會有不少空白。上面,咱們討論瞭如何封裝好接口請求,下一步就是決定何時請求初始化數據,即,代碼在哪裏寫的問題。實踐下來,有兩個時機是比較理想的。

beforeRouteEnter/Update

vue-router 提供了以上兩個生命週期鉤子,分別會在進入路由和路由改變時觸發。這兩個鉤子是寫的 view 中的。

router.beforeEach

vue-router還提供了一個全局性的 beforeEach 方法,任何一個路由改變時,都會被這個方法攔截,咱們能夠在這個方法中加入咱們本身的代碼,作統一處理。好比,對於全部 view 初始化請求的 action,咱們能夠以特定的名稱命名,如以 _init 做爲後綴等。在 beforeEach 方法內,咱們對當前 view 對應的 store 進行監聽,查找到其中以 _init 命名的 action 並派發。
以上兩種方式各有特色。
對於前者,優勢是數據獲取的代碼和具體的 view 是綁定在一塊兒的,咱們能夠在 view 內部就清晰地看到數據獲取的流程。缺點是,每增長一個頁面,都要在其內部寫一堆初始化代碼,增長了代碼量。 對於後者。優勢是,代碼統一且規整,使用了配置的方式,寫一次便可,不須要每次增長額外的代碼。缺點是比較隱晦,且初始化代碼和 view 自己割裂了。
對於以上兩種方式如何取捨的問題,我傾向於,大型項目用後者,小型項目用前者。

Other Tips

  • 多使用 mixing,可以在組件級別抽離公共部分,減小冗餘,極好的機制。
  • 多使用常量,這點和 vue 自己沒有關係,可是能極大地提高代碼的健壯性。
  • 連接若是是在項目內部跳轉,多使用 ,而不是去拼 a 標籤的 href。
  • 不要用 dom 操做。但若是無可奈何,好比你要得到某個 dom 的 scrollTop 屬性,用 $ref,而不是用選擇器去取。
  • 能想到的就這些,歡迎大佬們討論補充。

做者:丁香園前端團隊-㍿社長

相關文章
相關標籤/搜索