三. Vue元件化

MPolaris發表於2020-11-28

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

相關文章