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,然後再移除組件,就完美實現了淡出的效果。

最終效果如下: