Vue3 實現一個自定義toast(小彈窗)

語言: CN / TW / HK

前言:

前兩天在專案中很多場景下都需要用到一個 toast 彈窗,專案使用的是 ionic + tialwind_Cssionic 也有自帶的 toast 彈窗,雖然大部分場景下直接呼叫它提供的 api 已經能滿足需求了,但是它彈窗的高度,(也就是彈窗出現的位置)並不是高度自定義的,並且彈窗的 z-index 在我們專案中會和一些元件衝突,但是這個之前一直沒有辦法解決,所以乾脆自己手寫了一個使用方法高度類似 ionic_Toast 的元件。

這個元件也是我第一次在 vue3 下實現的,也查閱了很多網上相關的文章,也受到很多啟發靈感,所以自己吸取精華去其糟粕來完成了一版個人感覺使用起來很方便的一個版本,特來記錄一下實現的過程,希望可以幫助到遇到同樣迷惑的人。

tips:(本篇文章不會上手就教你樣式怎麼寫,程式碼怎麼寫,而是會幫你逐漸一步步理解相關額外的知識。會以“假如我是一個初學者,如果當時有人這樣告訴我的話,我就大概能聽明白”的角度去解釋。所以篇幅較長,如果想直接看元件的實現,可直接跳轉到 標題三

在這裡祝大家中秋節快樂呀~~

下面是正文:

一. 前置任務:JSX和渲染函式的概念

  1. 想要完成這個需求,你需要了解一下標題的那兩個概念。官方文件在這裡,裡面的話語太過於“專業和官方”,導致我剛開始看的時候非常迷惑,所以在這裡我會幫你去理解裡面的一些很官方的言語,讓你快速有個認知。

原地址在這裡: vue官方文件JSX和渲染函式

  1. 我們暫且還不需要去深入理解渲染機制的整個流程。所以官方下面個連結暫且不要去查閱,會讓你越來越頭暈。
    但在這裡我要說明一點,接下來講的內容都是建立在我預設你對 虛擬DOM 的概念有一定的瞭解。
  2. 緊接就寫到了 Vue 為我們提供了一個函式,來建立vnodes。在閱讀這個頁面的時候,一定注意官方在每個程式碼右上角的檔案型別。

  1. 這裡需要插個必須要了解的題外話,瞭解 React 的同學一定知道 JSX 這種寫法

Vue裡 JSX 的概念和 ReactJSX 的概念是極其相似的。 Vue 也是借鑑了React的這個思想,這裡我們重點看畫線的這句話。(不熟悉 react 的小夥伴也不要擔心,本文實現的 Toast 並沒有使用到 JSXbabel 。)

是不是覺得和剛剛 Vue 官方寫的很相似?

官方在上文也提到了 h 是什麼。如果我們把 h 換成 createVnode() ,是不是就和 React.createElement 的用法及其相似了呢?

  1. 其實不管是 Vue的h() ,還是 React.createElement() 它們最終要達到的目的只有一個:建立 虛擬DOM 。而這也對應了 VuecreateVnodeVnode 其實就是 virtual node 的意思。函式名的直接翻譯其實也就是 建立虛擬節點 。而 JSX 只是建立 虛擬dom 的語法題而已,僅此而已,並沒有什麼特別之處。

二. createVnode函式的意義

  1. 現在我們在 .Vue 檔案寫如下程式碼。

非常簡單的結構,一個id是"hanzhenfang"的 div 標籤,標籤內容是我的名字。ok,這樣寫的話,vue就會幫我們將這個結構轉換為虛擬dom。

本質上是使用了

h("div",{id:"hanzhenfang"},"韓振方")

h() 可以有多個引數,

這段程式碼是在 <template> 標籤內寫的,它底層其實還是使用了 h() 函式去實現的。說白了就是, React 選用 JSX 來作為渲染 虛擬dom 函式的語法糖。而 <template> 標籤是 Vue 採用的渲染 虛擬dom 的語法糖。

從而可以引出官方的標準解釋:

  1. 你可能會疑問了,既然模板可以實現這樣的功能,那我直接寫模版不就完事了嗎?還需要寫什麼 h() 函式呢?因為有的場景確實是模板做不到的。這也就是我為什麼會寫這篇文章的原因,因為這個toast需求使用的場景很多很多,我總不能在每個地方都引入一個元件通過 v-if 來控制它的顯示和關閉吧?非常繁瑣和麻煩。

三. 編寫Toast元件(不使用tsx)

  1. 首先建立一個 toast.vue 檔案寫出大致樣式。由於是使用 tailwindCss ,所以樣式書寫的方式可能和傳統的在 style 標籤寫樣式不太一樣,但是原理是一樣的,不用擔心。實現如下:

  1. toast 在絕大場景下都是居中的,並且脫離文件流,位於整個頁面的最頂部,所以我這裡採用了傳統的絕對定位的方案。

  1. 絕對定位最簡單居中方案不過與設定 topleft50% 。但是我們不要忽略了一點,偏移的時候,我們還要減去自身寬度和高度的一半,才可以做到完全居中。但是處於複用性考慮,我們的 toast 的寬度是不能設定固定寬度的,具體的寬度是由當時文字的大小決定的。

  1. 這時候我們需要用到 offsetWidth

1.額外技能補充 offsetWidth

  1. 先讓我們看看MDN如何解釋的。

  1. 具體什麼意思呢?我們隨手寫一個很簡單的 template

我們現在不加任何 Css 屬性,來檢視一下offsetWidth是什麼值。

不要把除錯工具只當成 console.log 的地方,一定利用好這個工具。我們選擇剛剛寫的元素,點選除錯工具選項欄的 Properties 標籤。

可以看到 offsetWidth 目前是20(注意,這是一個十進位制的 number 型別,並不帶單位,並且是一個只讀屬性,無法直接更改。)

20是怎麼來的呢?其實它就是我們設定的字型大小。

來驗證一下猜想,我們設定一下字型大小為15px

  1. 現在給這個div加上10px寬度的 border

彆著急看 offsetWidth 的值,我們根據它的定義猜想一下。

應該等於 15px 的字型大小+ 10px ?

確定嗎,別忘了border是上下左右都為 10px ,所以根據猜想

`offsetWidht=15px + 10px + 10px

  1. 如果這個 div 我們自己設定寬度呢?我們來給它設定100px的寬度。

我們會發現 offsetWidth 的值變成了 100

但是我明明有還有 10pxborder 啊!哪裡去了?

你是否忘了我們現在大部分情況下使用的都是 box-size:border-box 呢?設定的寬度是包含 border 的。

驗證一下,我們改變一下 box-size 的型別。

可以看出來, offsetWidth 就變成了 100px+10px+10px

Tips:不過在本文中不會設定定寬去限制

四. Toast居中的思路

1.現在我們可以不設定 Toast 的寬度,並且拿到根據文字數量不同所變化的寬度。由於這個屬性是元件掛載完畢以後才有的屬性,那麼我們可以在 onMounted 裡拿到。首先需要拿到元素本身,這裡採用打 ref 的方式。

具體變數和程式碼如下:不過多贅述

  1. 然後我們需要通過一個計算屬性動態的計算出該元件的樣式;

ok,這樣就實現了居中的效果。並且不管我們如何改變內容都沒關係。居中效果是動態計算獲得的,並不是一開始就寫死的。

五. Toast三個出現位置的思路

有些場景下 Toast 出現在底部並不是特別合適,所以我們還要考慮出現位置的問題。這裡簡單設計另外兩個,一箇中間,一個偏頂部。實現起來也比較簡單。我們讓它 toastWrapperStyle 計算 top 的偏移量也是一個動態計算的就可以了。

我們就可以在後面給這個元件傳遞引數來控制具體的位置在哪裡。

這個目前的程式碼還動態的展示效果,我們慢慢在後面體現。

tips:下面的章節是本文全篇重點

六*. h()函式的使用

  1. 我們建立一個 toastCreator.ts 的檔案,便於函式式呼叫展示 Toast 元件。

  1. 準備如下內容,這裡需要用到前面所提到的 h() ,還有 render()render 你暫且可以理解為給你返回一個真實的DOM。因為 h() 是生成虛擬dom的,但是我們最終展示到頁面的是 真實dom ,我們之前不用在 <template> 標籤內不用執行 render 是因為 Vue 幫你呼叫了 render() 。但是我們在這裡相當於手動實現一個 Vue 的渲染過程,所以我們也同時需要用到這個函式。

  1. 同時把同文件夾下剛剛寫好的 Toast.vue 引入。 class 相關的知識不是本文的重點,不瞭解的需要自行去查閱相關知識,這點很重要。
  2. 增加了一個 duration 選項,也就是持續時間,效果為 Toast ,在頁面出現多少秒後自動消失。
  3. 然後我們需要編寫一個這個類本身的方法,名為 present() (這裡借鑑了 ionicToast 元件的呼叫名稱。)
    這一步是重點,也是比較難理解的一個點。
    1.首先我們需要自己建立一個 Vnode ,經過翻閱官網。(注意我們著重看 JS 的檔案)。

2.我們得知,原來 h() 函式可以直接接收一個元件模板作為引數。那麼我們可以這樣寫:

這樣變數 myToast 就是一個 Vnode 了。

3.然後再呼叫引入的 render() 函式,我們自然而言的會想到這樣寫。

但是好像不太對勁啊,怎麼報錯了呢?看一下報錯資訊,原來 render() 函式需要接收兩個引數。第一個引數是 Vnode ,第二個引數是套在 Vnode 的真實dom。

讓我們建立並且加上一層外殼 containner 。其實就是一個普通的 div 標籤。如下:

在這裡要強調一下,你建立的虛擬節點必須包裹一層 真實的DOM 作為容器。

這樣正好對應,為什麼 Vue 或者 React 組專案的根目錄都會有一個 .html 檔案,並且還有一個 根div標籤的原因。 因為 render() 函式需要。(想更深入瞭解原理的可以翻看原始碼,不再過多贅述。)

七. 如何傳遞props?

  1. Vnode真實DOM 都有了,那我們如何傳遞 props 呢?

2.這裡不賣官子,先給結果,我們需要改造一下我們剛剛寫的 h() 函式。

這樣即可將呼叫建構函式時傳遞的引數轉換為該元件的 props

由於篇幅限制,具體細節大家仔細閱讀為上面貼出來的 Vue 官方文件。

八. 掛載真實DOM到頁面上

都是基礎的知識,不過多解釋了,程式碼如下:

九. 持續時間的控制

  1. 編寫 dismiss() 方法。非常簡單,直接移除這個節點即可。

  1. 持續時間如何控制呢?

    和時間掛鉤,自然而然可以想到 setTimout() 。實現原理其實也是非常簡單。在特定的時間,呼叫 dismiss() 方法即可

十. Toast自定義元件的使用方法

  1. 忘了寫 message 屬性了,我們補充一下

  1. 然後在 App.vue 檔案引入

隨手寫一個 <button> 標籤。

效果如下:

3.測試一下 position:top

十一. 增加淡出的效果

我們專案不需要淡入,所以我就沒做,不過和淡出是如出一轍,在這裡我講一下大致思路。

只需在 dismiss() 函式執行前,增加一個透明效果,這裡使用的是增加類名,只不過這個動畫名稱是搭配 tailWind 來完成的,你也可以使用別的方法,我的方法並不是最好的,原理思路是一致的

  1. tailwind.config.js 下設定全域性動畫樣式。

這裡需要注意!!你的動畫持續時間要和dismiss()函式的setTimeout引數一致,不然會出現意想不到的效果。動畫結束了,結果元件又出現啦。

2.再呼叫 document.doby.removeChild 方法之前,讓我們的元件在 0.5s 內透明度變為0,然後再移除元件,就完美實現了淡出的效果。

最終效果如下: