Vue3 如何實現一個全局搜索框

Vue3 如何實現一個全局搜索框

前言:自從學習 vue 以來,就對 vue 官網全局的 command + K 調出全局關鍵詞搜索這個功能心心念念。恰好最近項目也是需要實現一個全局搜索的功能,也正好可以正大光明的帶薪學習這個功能的思路。網上的教程水平參差不齊,而恰好之前的項目中我有做過一個類似於全局彈出麪包屑的功能,於是舉一反三寫出了一個我們項目需要的全局搜索框,特來分享一下自己的思路。

注意:本文不會馬上教你如何編寫代碼,而是作爲一個引路人,一步一步引導你去理解這個組件的設計思路。會以 “假如我是一個初學者,如果我在學習這個知識的時候,別人能這樣告訴我,那麼我也可以很快的去理解” 的角度去講解 ,授人以魚不如授人以漁。希望你在閱讀本文的時候可以拓展思路,舉一反三。

一. 文件準備

前期你需要準備三個文件,來完成這個全局搜索框

  1. SearchBar.ts 文件

  2. SearchBar.vue 文件

  3. useSearch.ts 文件

二. 搜索框的樣式

樣式問題不是本文的重點,你可以花費五分鐘在 SearchBar.vue 文件內速寫一個非常簡易的正方形 div 包裹着一個 input 標籤即可快速進行下面的學習。

但是首先我們需要理清思路,這個組件是會出現在我們頁面的最頂部的,所以它組件內部需要用到絕對佈局。我們去 SearchBar.vue 去設置一個樣式給最外層的 div,這裏其它樣式的寫法使用的是 Uno CSS,沒用過的小夥伴也不需要擔心,它只是單純的樣式,和本文中心內容不牽扯。(CSS 寫成計算屬性在這個場景也毫無特殊意義,只是單純設計時考慮多了)

三. 渲染函數 hrender 函數 (重點)

  1. 打開之前準備的 SearchBar.ts 文件,從 vue 裏引入這兩個函數,並且把在上一步寫好的簡陋版搜索框(SearchBar.vue)引入到這個文件內。

  2. 看過我之前文章 Vue3 實現一個 Toast[2] 的讀者可能會比較熟悉一點點,但是在那一篇文章內由於我也是初次接觸這兩個函數,所以當時總結的也不是特別精確,所以重新捋清思路,這裏再講解一下。

  3. 首先我們從官網的介紹,先看一下這個函數的定義。
    可以看出,這個函數第一個參數是必填的,可以是一個 stringComponent,這篇文章重點討論參數爲 Component 的情況。重點是這個函數的返回值,是一個 VNode,這個你一定不陌生,Virtual Node ,看本篇文章的讀者可能對虛擬 dom 的原理可能不是那麼清楚,但是我相信你們一定知道它的基本機制。Vue 其實是先渲染 虛擬 dom --> 然後 轉換成真實 dom

  4. 先別急着寫代碼,我想你可能更清楚這樣的寫法,比如我們前面在 SearchBar.vue 文件內寫的簡單的彈出框。 整個組件的樣式都是在 Vue 提供的 組件內寫的,但是你要知道,Vue 在底層還是通過調用 h() 來完成虛擬 dom 的構建。而 僅僅只是 Vue 爲了讓你用熟悉的原生 html 開發而爲你提供的語法糖🍬而已。(嗯,你可以這樣理解)

  5. 那麼我們可以根據上面 h() 函數的介紹,它接收的第一參數可以是 Component ,那我們這個 SearchBar.vue 不就是組件嗎?那如果我不想使用 去展示這個組件的話,我是否可以這樣寫呢?h(SearchBar.vue)。沒錯,是的,你就是可以這樣寫。別忘了 h 的返回值就是我們想拿到的 Vnode ,所以按照正確的寫法是這樣的。

三. 編寫 SearchBarMaker 構造函數和 present 方法

  1. 讓我們回到 SearchBar.ts 文件。

  2. 首先思考,這個搜索框一定有一個出現的函數,和一個消失的函數🤔,ok,起名字,一個 present,一個 dismiss

  3. 接下來我需要創建出一個 VNode ,然後想辦法處理成真實 dom。經過上面的學習,第一步馬上就可以想到下面的寫法。

  4. 下面這位更是重量級,render() 函數。虛擬 dom 有了,真實 dom 該如何拿到呢?Vue 爲我們提供了這樣一個函數,這裏我們需要重點去看這個函數的類型是值,是一個 RootRenderFunction 類型的。

  5. 這裏我們轉變一下思路,我們看一下 render 函數的第二個參數是 一個 container:HostElement ,然後讓我們打開我們 main.ts 文件,我們跳進 mount的定義部分, 發現神奇的地方了嗎,我們雖然不知道 HostElement 的類型是什麼,但是你知道你 mount 函數內填的參數是什麼了嗎?(忘掉的轉頭自覺複習官網哈。)
    沒錯,就是全局唯一的一個真實 dom,一個樸實無華的 id 叫 appdiv 元素。 由於篇幅限制,在這裏你可以先暫時簡單的理解,render 函數會將你的虛擬 dom 包裝成一個真實 dom 元素,但是你需要給它一個真實的 外殼 dom 來告訴它將虛擬 dom 渲染到哪個位置。

  6. ok,拿到一個包裝後的虛擬 dom ,接下來就是告訴瀏覽器在哪裏渲染這個元素。這裏我們需要思考🤔,既然是全局都可以彈出的,並且需要在所有組件之上彈出。 那麼最簡單的方法就是讓它出現在 body 的第一個元素,那麼它一定會和我們網頁所有的組件同級別(tips:通常我們所有的頁面構成都會寫在 body 內 的一個 div 內。什麼?你問我爲什麼?請打開你的 index.html 看一下,你是否忘記了我們的 App.vue 是掛在這個真實的,id 爲 app 的元素內的) 那其實我們的操作的思路就是非常簡單的,當我按下全局搜索按鈕,那麼你就在 <div> 的元素之前插入我的組件即可。

  7. ok,到這裏我們已經可以看到基本效果了,我們來測試一下。讓我們在 App.vue 組件內隨便寫一個按鈕,然後調用 SearchBarCreator 實例身上的 present 方法。(maker 感覺不是那麼合理,之後我們將 SearchBarMaker 變更爲 SeachBarCreator 的叫法,僅僅是名字變了而已,邏輯什麼的根本沒變哦。🍦) 效果如下:

  8. 到這裏 searchBar 已經可以呈現在頁面上了,但是我們還不知道怎樣讓它消失,其實也非常簡單,我們只需要在合適的時機移除這個 dom 元素即可。 在這裏我們需要知道一點,我們需要將 searchBar 提升到當前文件的全局,不能僅只在 open 中去 new 了。
    ok,我們測試一下

四. 優化 SearchBarCreator 構造函數的代碼邏輯

寫到這裏的時候,你可能發現了一個小問題,當我一直去按搜索按鈕的時候,它會出現多個搜索框,但是我們希望的是它在全局只能出現一個搜索框。換個角度思考,也就是同一時間,這個被我們 new 出來的 SeachBar 實例只能出現一個。思考一下🤔,我加一個變量,isShowing 是否正在被展示 ,如果正在被展示的話,那麼用戶再次調用 present 的時候,我就去調用實例自身的 dismiss 方法讓它消失,是否可行呢?
測試一下:
OK,看來完美解決當前的問題了。

五. 編寫全局唯一的調用實例

  1. 在上面的這種情況下,我們已經可以在 App.vue 文件內去 new 一個實例來調用這個搜索框了。但是我們加入現在需要在 XXX.vue 文件內調用這個搜索框呢?我難道還需要重新去引入,然後重新 new 嗎?nonono,某位大佬說過,程序員都是很懶的,不可能寫這種低級的重複代碼的。那麼該如何實現呢

  2. 打開我們之前準備的 useSearch.ts 文件,我們把之前在 App.vue 的全局生成的這個 SearchBar 實例轉換思路,使它在全局的一個 ts 文件內生成一個,然後把這個實例自身的一些方法封裝成函數,暴露給外部。那麼我就可以在全局任意一個地方去調用這個實例身上的這兩個方法。

  3. 讓我們在 App.vue 去試一下。
    這是我們之前的 App.vue 文件的調用方法。 我們改造一下它。 我們再次測試一下功能有沒有什麼問題
    如此一來就方便很多了,我們可以在任意位置去調用這個 “唯一的搜索框”

六. 添加全局的快捷鍵 Command + K

  1. 再此之前,我們需要理解一個概念,注意我們的 main.ts 文件,我們是把誰掛在了全局的那一個 id='app' 的真實 dom 下的?
    沒錯,就是前面我們提到的 App.vue 組件。

  2. 那麼假如我在這個 App.vue 組件掛載的時候,給全局 window 對象身上添加一個鍵盤事件,是不是就可以了呢?怎麼添加呢?其實非常非常簡單,要用到見組合按鍵,我們就需要使用到 “keydown”,具體爲什麼不是 “keypress” ,讀者可以自行查閱這兩者的區別,不屬於本文的主要探討內容。

  3. 這時候,我們先來按一下 command 看看打印的內容是什麼。這裏重點的內容是該鍵盤事件身上的metaKey 屬性。 在這裏我們還可以推算出按下 “ctrl” 的事件爲

  4. keydown 事件支持多個按鍵同時按下。當我們同時按下 “command” 和 “K” 鍵,會發生什麼呢? 但是我們發現好像並沒有 K:true 這個屬性呀,那我們怎麼去判斷呢?彆着急接着往下看。

  5. 我們可以看到鍵盤事件 event 身上有個 key 屬性,它的值恰好是字符串類型的 “k”

這裏我直接公佈寫法,js 允許我們這樣判斷是否同時按下兩個按鍵。

  1. 我們測試一下,我們去吧 App.vue 文件內的這兩個按鈕給去掉 然後再打印一下我們按下 commandk 的時候。 測試一下:

七. 添加出現的動畫

  1. 在上面我們可以看到,這樣突然的出現好像有一絲絲的突兀。我希望這個搜索框在出現的時候,可以有那麼一絲絲的平移效果,(類似於下面的效果)該如何做呢?🤔

  2. 我這裏介紹一種較爲簡單的思路,我們在 App.vue 文件的 style 內預設一個 Css 動畫,並起好名字。叫做 "searchInput"

  3. 然後回到我們 searBar.vue 的組件去,給我們這個組件最外層的起一個好聽的名字,我這裏就叫做 searchBarWrapper

  4. 然後回到我們的 SearchBar.ts 文件內,也就是放我們 SeachBarCreator 構造函數的那個文件內。(tips:不是 useSearch.ts 哦) 我這裏解釋一下思路,在調用 render 函數後,這個組件其實已經渲染成爲一個真實的 dom 元素,只不過我們還沒給它指定渲染的位置。既然是真實的 dom ,那麼我們就可以通過 document.getElementById這個方法(querySelector同理,一個意思)拿到這個SearchBar.vue組件 ,接下來我只需要在調用 document.body.insertBefore 方法前,給它添加上剛剛我們在 App.vue 裏預設好的類名,searchInput ,就完美達成我們想要的效果了。

  5. 注意:style ,這個點僅僅是類名選擇器,不要忘記了基礎知識。

  6. 測試一下效果:

八. 自動聚焦

在彈出框的 input 框實現自動聚焦相比於之前講的就非常簡單了,我在這裏一筆帶過了。只需要在 nextTick 中調用 input 本身的 focus 方法即可。

總結:

之所以不喜歡使用真代碼去寫文章而大量使用截圖的原因是:我自己在搜索到自己想要的文章後,也會喜歡直接看有沒有最後的成品代碼,然後直接複製就拿過去用了,而往往忽略了自己動手去實現一遍纔是真正理解了的過程。

所以我寫代碼的時候,儘量不寫特別複雜的邏輯,而寫一些很簡單的幾行代碼去實現某一個功能。是因爲我希望你們真正帶入自己的思考,和一步步體會這個實現過程,從而舉一反三。

如果你認真看了該文章,你也許會明白現在很多組件庫的底層實現原理其實就是這樣的,比如全局彈出的 dialogmodal 框等等。我們要去理解組件庫組件實現的思路,而不是一味的複製粘貼。

這個搜索框有很多可以更加優化的地方,你們可以帶入自己的思考去想一想。比如

1. 如何保存搜索歷史?
2. 如何實現實時的給出搜索聯想

與君共勉纔是我的初衷...

關於本文

作者:韓振方

https://juejin.cn/post/7170345858938961957

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sdnJKK_ntEvVpvQbpO4o7Q