三. Vue組件化

1. 認識組件化

1.1 什麼是組件化

人面對複雜問題的處理方式

任何一個人處理資訊的邏輯能力都是有限的,所以當面對一個非常複雜的問題時我們不太可能一次性搞定一大堆的內容。

但是我們人有一種天生的能力就是將問題進行拆解。如果將一個複雜的問題拆分成很多個可以處理的小問題再將其放在整體當中,你會發現大的問題也會迎刃而解。

組件化也是類似的思想

如果我們將一個頁面中所有的處理邏輯全部放在一起,處理起來就會變得非常複雜,而且不利於後續的管理以及擴展。

但如果我們將一個頁面拆分成一個個小的功能塊,每個功能塊完成屬於自己這部分獨立的功能,那麼之後整個頁面的管理和維護就變得非常容易了。

image-20201124214310726

1.2 Vue組件化思想

組件化是Vue.js中的重要思想

它提供了一種抽象,讓我們可以開發出一個個獨立可復用的小組件來構造我們的應用,任何的應用都會被抽象成一顆組件樹

image-20201124214641019

組件化思想的應用

有了組件化的思想,我們在之後的開發中就要充分的利用它

儘可能的將頁面拆分成一個個小的、可復用的組件

這樣讓我們的程式碼更加方便組織和管理,並且擴展性也更強

2. 註冊組件

2.1 註冊組件的基本步驟

組件的使用分成三個步驟

  • 創建組件構造器

  • 註冊組件

  • 使用組件

image-20201124215013411

三個步驟的含義

  • Vue.extend()

    • 調用 Vue.extend() 創建的是一個組件構造器
    • 通常在創建組件構造器時,傳入template代表我們自定義組件的模板
    • 該模板就是在使用到組件的地方要顯示的HTML程式碼
    • 事實上,這種寫法在 Vue2.x 的文檔中幾乎已經看不到了,它會直接使用下面如 2.4 形式的語法糖,但是在很多資料還是會提到這種方式,而且這種方式是學習後面方式的基礎
  • Vue.component()

    • 調用 Vue.component() 是將剛才的組件構造器註冊為一個組件並且給它起一個組件的標籤名稱
    • 所以需要傳遞兩個參數:① 註冊組件的標籤名 ② 組件構造器
  • 在Vue實例的作用範圍內使用組件

    • 組件必須掛載在某個Vue實例下,否則它不會生效
    • 下面我使用了三次<my-cpn></my-cpn> ,而第三次其實並沒有生效

image-20201124215628434

2.2 組件的作用域

全局組件

當我們通過調用 Vue.component() 註冊組件時,組件的註冊是全局的這意味著該組件可以在任意Vue示例下使用

局部組件

如果我們註冊的組件是掛載在某個實例中, 那麼就是一個局部組件

image-20201124221306083

2.3 父子組件

在前面我們看到了組件樹

組件和組件之間存在層級關係

而其中一種非常重要的關係就是父子組件的關係

我們來看通過程式碼如何組成的這種層級關係

image-20201124221953846

父子組件錯誤用法:以子標籤的形式在Vue實例中使用

因為當子組件註冊到父組件的components時,Vue會編譯好父組件的模組

該模板的內容已經決定了父組件將要渲染的HTML(相當於父組件中已經有了子組件中的內容了

<child-cpn></child-cpn> 是只能在父組件中被識別的

類似這種用法,<child-cpn></child-cpn>是會被瀏覽器忽略的

2.4 註冊組件語法糖

在上面註冊組件的方式,可能會有些繁瑣

Vue為了簡化這個過程,提供了註冊的語法糖

主要是省去了調用 Vue.extend() 的步驟,而是可以直接使用一個對象來代替

語法糖註冊全局組件和局部組件如下

image-20201124222436062

3. 組件其他補充

3.1 模板的分離寫法

剛才我們通過語法糖簡化了Vue組件的註冊過程,另外還有一個地方的寫法比較麻煩,就是template模組寫法

如果我們能將其中的HTML分離出來寫,然後掛載到對應的組件上,必然結構會變得非常清晰

Vue提供了兩種方案來定義HTML模組內容:

  • 使用 <script> 標籤
  • 使用 <template> 標籤

image-20201124222815433

3.2 組件可以Vue的實例數據嗎?

組件是一個單獨功能模組的封裝

這個模組有屬於自己的HTML模板,也應該有屬性自己的數據data

組件中的數據是保存在哪裡呢?頂層的Vue實例中嗎?

  • 如下測試發現不能並不能訪問,而且即使可以訪問,如果將所有的數據都放在Vue實例中Vue實例就會變的非常臃腫
  • 結論:組件並不能直接訪問Vue實例中的data,Vue組件應該有自己保存數據的地方

image-20201124223920656

3.3 組件數據的存放

組件自己的數據存放在哪裡呢?

組件對象也有一個data屬性(當然也可以有methods等屬性,下面我們有用到)

只是這個data屬性必須是一個函數

而且這個函數返回一個對象,對象內部保存著數據

image-20201124223817597

為什麼data在組件中必須是一個函數呢?

首先,如果不是一個函數Vue直接就會報錯

其次,原因是在於Vue讓每個組件對象都返回一個新的對象。因為如果是同一個對象的,組件在多次使用後會相互影響

image-20201124224212074

4. 父子組件通訊

4.1 父子組件通訊理解

之前我們提到了子組件是不能引用父組件或者Vue實例的數據的

但是,在開發中往往一些數據確實需要從上層傳遞到下層:

  • 比如在一個頁面中我們從伺服器請求到了很多的數據

  • 其中一部分數據並非是我們整個頁面的大組件來展示的,而是需要下面的子組件進行展示

  • 這個時候並不會讓子組件再次發送一個網路請求,而是直接讓 大組件(父組件) 將數據傳遞給 小組件(子組件)

  • 如何進行父子組件間的通訊呢?Vue官方提到

    • 通過 props 向子組件傳遞數據
    • 通過 事件 向父組件發送消息

image-20201124225224780

在下面的程式碼中,我直接將Vue實例當做父組件並且其中包含子組件來簡化程式碼

真實的開發中,Vue實例和子組件的通訊和父組件和子組件的通訊過程是一樣的

4.2 父組件向子組件傳遞數據 – props

基本用法

在組件中,使用選項props來聲明需要從父級接收到的數據

props的值有兩種方式

  • 字元串數組,數組中的字元串就是傳遞時的名稱
  • 對象,對象可以設置傳遞時的類型,也可以設置默認值等

我們先來看一個最簡單的props傳遞

image-20201124225501375

props數據驗證

在前面我們的props選項是使用一個數組

除了數組之外我們也可以使用對象,當需要對props進行類型等驗證時就需要對象寫法了

驗證支援如下數據類型

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

image-20201124225932450

當我們有自定義構造函數時,驗證也支援自定義的類型

image-20201124225823411

4.4 子組件向父組件傳遞數據或事件 – $emit()

自定義事件

props用於父組件向子組件傳遞數據,還有一種比較常見的是子組件傳遞數據或事件到父組件中我們應該如何處理呢?這個時候我們需要使用自定義事件來完成

什麼時候需要自定義事件?

當子組件需要向父組件傳遞數據時,就要用到自定義事件了

我們之前學習的v-on不僅僅可以用於監聽DOM事件,也可以用於組件間的自定義事件

自定義事件的流程

在子組件中,通過$emit()來觸發事件

在父組件中,通過v-on來監聽子組件事件

我們來看一個簡單的例子:

  • 我們之前做過一個兩個按鈕 +1-1 ,點擊後修改 counter
  • 我們整個操作的過程還是在子組件中完成,但是之後的展示交給父組件
  • 這樣我們就需要將子組件中的 counter,傳給父組件的某個屬性比如 total

image-20201124230536738

4.5 父子組件的直接訪問方式 – $children或$refs  /  $parent

理解

有時候我們需要父組件直接訪問子組件,子組件直接訪問父組件,或者是子組件訪問根組件

  • 父組件訪問子組件:使用$children$refs

  • 子組件訪問父組件:使用$parent

$children

this.$children是一個數組類型,它包含所有子組件對象

我們這裡通過一個遍歷,取出所有子組件的message狀態

image-20201124231839032

$refs

$children的缺陷

通過 $children 訪問子組件時,是一個數組類型,訪問其中的子組件必須通過索引值

但是當子組件過多,我們需要拿到其中一個時往往不能確定它的索引值,甚至還可能會發生變化

有時候我們想明確獲取其中一個特定的組件,這個時候就可以使用$refs

$refs的使用

$refsref指令 通常是一起使用的

首先我們通過 ref 給某一個子組件綁定一個特定的ID

其次通過 this.$refs.ID 就可以訪問到該組件了

image-20201124232844530

$parent

如果我們想在子組件中直接訪問父組件,可以通過$parent

注意

儘管在Vue開發中,我們允許通過$parent來訪問父組件,但是在真實開發中盡量不要這樣做

子組件應該盡量避免直接訪問父組件的數據,因為這樣耦合度太高了
如果我們將子組件放在另外一個組件之內,很可能該父組件沒有對應的屬性,往往會引起問題

另外更不好做的是通過$parent直接修改父組件的狀態,那麼父組件中的狀態將變得飄忽不定,很不利於我的調試和維護

image-20201124233250766

5. 非父子組件通訊

5.1 理解

剛才我們討論的都是父子組件間的通訊,那如果是非父子關係呢?

非父子組件關係包括多個層級的組件,也包括兄弟組件的關係

Vue1.x的時候,可以通過 $dispatch$broadcast完成,但是在 Vue2.x都被取消了

  • $dispatch用於向上級派發事件

  • $broadcast用於向下級廣播事件

Vue2.x 中,有一種方案是通過中央事件匯流排,也就是一個中介來完成

  • 但是這種方案和直接使用 Vuex 的狀態管理方案還是遜色很多
  • 並且 Vuex 提供了更多好用的功能,所以這裡我們暫且不討論這種方案,後續我們專門學習 Vuex 的狀態管理
5.2 中央事件匯流排
5.3 Vuex狀態管理(後面專門講)

6. 插槽slot

6.1 編譯作用域

在真正學習插槽之前我們需要先理解一個概念:編譯作用域

官方對於編譯的作用域解析比較簡單,我們自己來通過一個例子來理解這個概念

我們來考慮下面的程式碼是否最終是可以渲染出來的:

  • <my-cpn v-show="isShow"></my-cpn>中,我們使用了 isShow屬性

  • isShow屬性包含在組件中,也包含在Vue實例中

答案:最終可以渲染出來,也就是使用的是Vue實例的屬性。為什麼呢?

  • 官方給出了一條準則:父組件模板的所有東西都會在父級作用域內編譯;子組件模板的所有東西都會在子級作用域內編譯
  • 而我們在使用 <my-cpn v-show="isShow"></my-cpn> 的時候,整個組件的使用過程是相當於在父組件中出現的
  • 那麼他的作用域就是父組件,使用的屬性也是屬於父組件的屬性
  • 因此 isShow使用的是Vue實例中的屬性,而不是子組件的屬性

image-20201124234313056

6.2 為什麼使用slot

slot翻譯為插槽

在生活中很多地方都有插槽,電腦的USB插槽,插板當中的電源插槽

插槽的目的是讓我們原來的設備具備更多的擴展性

比如電腦的USB我們可以插入U盤、硬碟、手機、音響、鍵盤、滑鼠等

組件的插槽

組件的插槽也是為了讓我們封裝的組件更加具有擴展性

讓使用者可以決定組件內部的一些內容到底展示什麼

例子:移動網站中的導航欄

  • 移動開發中,幾乎每個頁面都有導航欄

  • 導航欄我們必然會封裝成一個插件,比如nav-bar組件

  • 一旦有了這個組件,我們就可以在多個頁面中復用了

  • 但是,每個頁面的導航是一樣的嗎?No,我以京東M站為例

image-20201124234612254

6.3 如何在封裝組件時正確使用slot

如何去封裝京東M站導航欄這類的組件呢?

它們也很多區別,但是也有很多共性

如果我們每一個單獨去封裝一個組件顯然不合適:比如每個頁面都返回,這部分內容我們就要重複去封裝

但是如果我們封裝成一個好像也不合理:有些左側是菜單,有些是返回,有些中間是搜索,有些是文字等

如何封裝合適呢?抽取共性,保留不同

  • 最好的封裝方式就是將共性抽取到組件中,將不同暴露為插槽

  • 一旦我們預留了插槽,就可以讓使用者根據自己的需求,決定插槽中插入什麼內容

  • 是搜索框,還是文字,還是菜單。由調用者自己來決定

  • 這就是為什麼我們要學習組件中的插槽slot的原因

6.4 slot基本使用

了解了為什麼用slot,我們再來談談如何使用slot?

  • 在子組件中,使用特殊的元素 <slot> 就可以為子組件開啟一個插槽。

  • 該插槽插入什麼內容取決於父組件如何使用。

我們通過一個簡單的例子,來給子組件定義一個插槽

  • <slot> 中的內容表示,如果沒有在該組件中插入任何其他內容,就默認顯示該內容
  • 有了這個插槽後,父組件如何使用呢?

image-20201128135942587

6.5 具名插槽slot

當子組件的功能複雜時,子組件的插槽可能並非是一個

比如我們封裝一個導航欄的子組件,可能就需要三個插槽,分別代表左邊、中間、右邊。、

那麼,外面在給插槽插入內容時,如何區分插入的是哪一個呢?

這個時候,我們就需要給插槽起一個名字

image-20201128135942587

如何使用具名插槽呢?

非常簡單,只要給slot元素一個name屬性即可

<slot name='myslot'></slot>

我們來給出一個案例:

這裡我們先不對導航組件做非常複雜的封裝,先了解具名插槽的用法。

image-20201128140254483

6.6 作用域插槽

作用域插槽是slot一個比較難理解的點,而且官方文檔說的又有點不清晰。

這裡,我們用一句話對其做一個總結,然後我們在後續的案例中來體會:父組件替換插槽的標籤,但是內容由子組件來提供。

我們先提一個需求

子組件中包括一組數據,比如:pLanguages: [‘JavaScript’, ‘Python’, ‘Swift’, ‘Go’, ‘C++’]

需要在多個介面進行展示:

  • 某些介面是以水平方向一一展示的,
  • 某些介面是以列表形式展示的,
  • 某些介面直接展示一個數組

內容在子組件,希望父組件告訴我們如何展示,怎麼辦呢

  • 利用slot作用域插槽就可以了

我們來看看子組件的定義:

image-20201128140613312

在父組件使用我們的子組件時,從子組件中拿到數據

我們通過 <template slot-scope="slotProps"> 獲取到 slotProps 屬性

在通過 slotProps.data 就可以獲取到剛才我們傳入的data了

image-20201128140707214