本篇文章給大家?guī)砹岁P(guān)于Vue3的相關(guān)知識,其中主要介紹了Vue3是怎么實(shí)現(xiàn)一個全局搜索框,我會分享一下自己完整實(shí)現(xiàn)的思路,下面一起來看一下吧,希望對需要的朋友有所幫助。
Vue3 如何實(shí)現(xiàn)一個全局搜索框
前言:自從學(xué)習(xí) vue 以來,就對 vue 官網(wǎng)全局的 command + K 調(diào)出全局關(guān)鍵詞搜索這個功能心心念念。恰好最近項(xiàng)目也是需要實(shí)現(xiàn)一個全局搜索的功能,也正好可以正大光明的帶薪學(xué)習(xí)這個功能的思路。網(wǎng)上的教程水平參差不齊,而恰好之前的項(xiàng)目中我有做過一個類似于全局彈出面包屑的功能,于是舉一反三寫出了一個我們項(xiàng)目需要的全局搜索框,特來分享一下自己的思路。
注意:本文不會馬上教你如何編寫代碼,而是作為一個引路人,一步一步引導(dǎo)你去理解這個組件的設(shè)計思路。會以 “假如我是一個初學(xué)者,如果我在學(xué)習(xí)這個知識的時候,別人能這樣告訴我,那么我也可以很快的去理解” 的角度去講解 ,授人以魚不如授人以漁。希望你在閱讀本文的時候可以拓展思路,舉一反三。
一. 文件準(zhǔn)備
前期你需要準(zhǔn)備三個文件,來完成這個全局搜索框
-
SearchBar.ts 文件
-
SearchBar.vue 文件
-
useSearch.ts 文件
二. 搜索框的樣式
樣式問題不是本文的重點(diǎn),你可以花費(fèi)五分鐘在 SearchBar.vue 文件內(nèi)速寫一個非常簡易的正方形 div 包裹著一個 input 標(biāo)簽即可快速進(jìn)行下面的學(xué)習(xí)。
但是首先我們需要理清思路,這個組件是會出現(xiàn)在我們頁面的最頂部的,所以它組件內(nèi)部需要用到絕對布局。我們?nèi)?SearchBar.vue
去設(shè)置一個樣式給最外層的 div
,這里其它樣式的寫法使用的是 Uno CSS
,沒用過的小伙伴也不需要擔(dān)心,它只是單純的樣式,和本文中心內(nèi)容不牽扯。(CSS寫成計算屬性在這個場景也毫無特殊意義,只是單純設(shè)計時考慮多了)
三. 渲染函數(shù) h 和 render 函數(shù)(重點(diǎn))
-
打開之前準(zhǔn)備的 SearchBar.ts 文件,從 vue 里引入這兩個函數(shù),并且把在上一步寫好的簡陋版搜索框(SearchBar.vue)引入到這個文件內(nèi)。
-
看過我之前文章 Vue3實(shí)現(xiàn)一個 Toast 的讀者可能會比較熟悉一點(diǎn)點(diǎn),但是在那一篇文章內(nèi)由于我也是初次接觸這兩個函數(shù),所以當(dāng)時總結(jié)的也不是特別精確,所以重新捋清思路,這里再講解一下。
-
首先我們從官網(wǎng)的介紹,先看一下這個函數(shù)的定義。
可以看出,這個函數(shù)第一個參數(shù)是必填的,可以是一個string
和Component
,這篇文章重點(diǎn)討論參數(shù)為Component
的情況。重點(diǎn)是這個函數(shù)的返回值,是一個VNode
,這個你一定不陌生,Virtual Node ,看本篇文章的讀者可能對虛擬 dom 的原理可能不是那么清楚,但是我相信你們一定知道它的基本機(jī)制。Vue 其實(shí)是先渲染 虛擬 dom –>然后 轉(zhuǎn)換成真實(shí) dom。 -
先別急著寫代碼,我想你可能更清楚這樣的寫法,比如我們前面在 SearchBar.vue 文件內(nèi)寫的簡單的彈出框。
整個組件的樣式都是在 Vue 提供的 <template> 組件內(nèi)寫的,但是你要知道,Vue 在底層還是通過調(diào)用 h() 來完成虛擬 dom 的構(gòu)建。而 <template> 僅僅只是 Vue 為了讓你用熟悉的原生 html 開發(fā)而為你提供的語法糖?而已。(嗯,你可以這樣理解)
-
那么我們可以根據(jù)上面 h() 函數(shù)的介紹,它接收的第一參數(shù)可以是
Component
,那我們這個SearchBar.vue
不就是組件嗎?那如果我不想使用 <template> 去展示這個組件的話,我是否可以這樣寫呢?h(SearchBar.vue)
。沒錯,是的,你就是可以這樣寫。別忘了h
的返回值就是我們想拿到的 Vnode ,所以按照正確的寫法是這樣的。
三. 編寫 SearchBarMaker 構(gòu)造函數(shù)和 present 方法
-
讓我們回到
SearchBar.ts
文件。 -
首先思考,這個搜索框一定有一個出現(xiàn)的函數(shù),和一個消失的函數(shù)?,ok,起名字,一個 present,一個 dismiss 。
-
接下來我需要創(chuàng)建出一個 VNode ,然后想辦法處理成真實(shí) dom。經(jīng)過上面的學(xué)習(xí),第一步馬上就可以想到下面的寫法。
-
下面這位更是重量級,
render()
函數(shù)。虛擬 dom 有了,真實(shí)dom 該如何拿到呢? Vue 為我們提供了這樣一個函數(shù),這里我們需要重點(diǎn)去看這個函數(shù)的類型是值,是一個RootRenderFunction
類型的。 -
這里我們轉(zhuǎn)變一下思路,我們看一下 render 函數(shù)的第二個參數(shù)是 一個 container:HostElement ,然后讓我們打開我們 main.ts 文件,我們跳進(jìn)
mount
的定義部分,發(fā)現(xiàn)神奇的地方了嗎,我們雖然不知道 HostElement 的類型是什么,但是你知道你
mount
函數(shù)內(nèi)填的參數(shù)是什么了嗎?(忘掉的轉(zhuǎn)頭自覺復(fù)習(xí)官網(wǎng)哈。)
沒錯,就是全局唯一的一個真實(shí) dom,一個樸實(shí)無華的id叫app
的 div 元素。由于篇幅限制,在這里你可以先暫時簡單的理解,render 函數(shù)會將你的虛擬 dom 包裝成一個真實(shí) dom 元素,但是你需要給它一個真實(shí)的 外殼dom 來告訴它將虛擬 dom 渲染到哪個位置。
-
ok,拿到一個包裝后的虛擬 dom ,接下來就是告訴瀏覽器在哪里渲染這個元素。這里我們需要思考?,既然是全局都可以彈出的,并且需要在所有組件之上彈出。
那么最簡單的方法就是讓它出現(xiàn)在 body的第一個元素,那么它一定會和我們網(wǎng)頁所有的組件同級別(tips:通常我們所有的頁面構(gòu)成都會寫在 body內(nèi) 的一個 div 內(nèi)。什么?你問我為什么?請打開你的 index.html 看一下,你是否忘記了我們的 App.vue 是掛在這個真實(shí)的,id為 app 的元素內(nèi)的)
那其實(shí)我們的操作的思路就是非常簡單的,當(dāng)我按下全局搜索按鈕,那么你就在
<div id="app">
的元素之前插入我的組件即可。 -
ok,到這里我們已經(jīng)可以看到基本效果了,我們來測試一下。讓我們在
App.vue
組件內(nèi)隨便寫一個按鈕,然后調(diào)用SearchBarCreator
實(shí)例身上的present
方法。(maker 感覺不是那么合理,之后我們將SearchBarMaker
變更為SeachBarCreator
的叫法,僅僅是名字變了而已,邏輯什么的根本沒變哦。?)效果如下:
-
到這里 searchBar 已經(jīng)可以呈現(xiàn)在頁面上了,但是我們還不知道怎樣讓它消失,其實(shí)也非常簡單,我們只需要在合適的時機(jī)移除這個 dom 元素即可。
在這里我們需要知道一點(diǎn),我們需要將
searchBar
提升到當(dāng)前文件的全局,不能僅只在open
中去new
了。ok,我們測試一下
四. 優(yōu)化 SearchBarCreator 構(gòu)造函數(shù)的代碼邏輯
寫到這里的時候,你可能發(fā)現(xiàn)了一個小問題,當(dāng)我一直去按搜索按鈕的時候,它會出現(xiàn)多個搜索框,但是我們希望的是它在全局只能出現(xiàn)一個搜索框。換個角度思考,也就是同一時間,這個被我們 new
出來的 SeachBar
實(shí)例只能出現(xiàn)一個。思考一下?,我加一個變量,isShowing 是否正在被展示
,如果正在被展示的話,那么用戶再次調(diào)用 present
的時候,我就去調(diào)用實(shí)例自身的 dismiss
方法讓它消失,是否可行呢?測試一下:
OK,看來完美解決當(dāng)前的問題了。
五. 編寫全局唯一的調(diào)用實(shí)例
-
在上面的這種情況下,我們已經(jīng)可以在
App.vue
文件內(nèi)去new
一個實(shí)例來調(diào)用這個搜索框了。但是我們加入現(xiàn)在需要在XXX.vue
文件內(nèi)調(diào)用這個搜索框呢?我難道還需要重新去引入,然后重新new
嗎?nonono,某位大佬說過,程序員都是很懶的,不可能寫這種低級的重復(fù)代碼的。那么該如何實(shí)現(xiàn)呢 -
打開我們之前準(zhǔn)備的 useSearch.ts 文件,我們把之前在
App.vue
的全局生成的這個 SearchBar 實(shí)例轉(zhuǎn)換思路,使它在全局的一個 ts 文件內(nèi)生成一個,然后把這個實(shí)例自身的一些方法封裝成函數(shù),暴露給外部。那么我就可以在全局任意一個地方去調(diào)用這個實(shí)例身上的這兩個方法。 -
讓我們在
App.vue
去試一下。
這是我們之前的App.vue
文件的調(diào)用方法。我們改造一下它。
我們再次測試一下功能有沒有什么問題
如此一來就方便很多了,我們可以在任意位置去調(diào)用這個“唯一的搜索框”
六. 添加全局的快捷鍵 Command + K
-
再此之前,我們需要理解一個概念,注意我們的
main.ts
文件,我們是把誰掛在了全局的那一個id='app'
的真實(shí) dom 下的?
沒錯,就是前面我們提到的 App.vue 組件。 -
那么假如我在這個
App.vue
組件掛載的時候,給全局 window 對象身上添加一個鍵盤事件,是不是就可以了呢?怎么添加呢?其實(shí)非常非常簡單,要用到見組合按鍵,我們就需要使用到 “keydown”,具體為什么不是 “keypress” ,讀者可以自行查閱這兩者的區(qū)別,不屬于本文的主要探討內(nèi)容。 -
這時候,我們先來按一下
command
看看打印的內(nèi)容是什么。這里重點(diǎn)的內(nèi)容是該鍵盤事件身上的metaKey
屬性。在這里我們還可以推算出按下 “ctrl” 的事件為
-
keydown 事件支持多個按鍵同時按下。當(dāng)我們同時按下 “command” 和 “K” 鍵,會發(fā)生什么呢?
但是我們發(fā)現(xiàn)好像并沒有
K:true
這個屬性呀,那我們怎么去判斷呢?別著急接著往下看。 -
我們可以看到鍵盤事件 event 身上有個 key 屬性,它的值恰好是字符串類型的 “k”,
這里我直接公布寫法,js 允許我們這樣判斷是否同時按下兩個按鍵。
-
我們測試一下,我們?nèi)グ?App.vue 文件內(nèi)的這兩個按鈕給去掉
然后再打印一下我們按下
command
和k
的時候。測試一下:
七. 添加出現(xiàn)的動畫
-
在上面我們可以看到,這樣突然的出現(xiàn)好像有一絲絲的突兀。我希望這個搜索框在出現(xiàn)的時候,可以有那么一絲絲的平移效果,(類似于下面的效果)該如何做呢??
-
我這里介紹一種較為簡單的思路,我們在
App.vue
文件的 style 內(nèi)預(yù)設(shè)一個 Css 動畫,并起好名字。叫做 "searchInput" -
然后回到我們
searBar.vue
的組件去,給我們這個組件最外層的起一個好聽的名字,我這里就叫做searchBarWrapper
。 -
然后回到我們的
SearchBar.ts
文件內(nèi),也就是放我們 SeachBarCreator 構(gòu)造函數(shù)的那個文件內(nèi)。(tips:不是 useSearch.ts 哦) 我這里解釋一下思路,在調(diào)用 render 函數(shù)后,這個組件其實(shí)已經(jīng)渲染成為一個真實(shí)的 dom 元素,只不過我們還沒給它指定渲染的位置。既然是真實(shí)的 dom ,那么我們就可以通過document.getElementById這個方法(querySelector同理,一個意思)
拿到這個SearchBar.vue組件
,接下來我只需要在調(diào)用document.body.insertBefore
方法前,給它添加上剛剛我們在App.vue
里預(yù)設(shè)好的類名,searchInput
,就完美達(dá)成我們想要的效果了。 -
注意:style ,這個點(diǎn)僅僅是類名選擇器,不要忘記了基礎(chǔ)知識。
-
測試一下效果:
八. 自動聚焦
在彈出框的 input 框?qū)崿F(xiàn)自動聚焦相比于之前講的就非常簡單了,我在這里一筆帶過了。只需要在 nextTick 中調(diào)用 input 本身的 focus 方法即可。
總結(jié):
之所以不喜歡使用真代碼去寫文章而大量使用截圖的原因是:我自己在搜索到自己想要的文章后,也會喜歡直接看有沒有最后的成品代碼,然后直接復(fù)制就拿過去用了,而往往忽略了自己動手去實(shí)現(xiàn)一遍才是真正理解了的過程。
所以我寫代碼的時候,盡量不寫特別復(fù)雜的邏輯,而寫一些很簡單的幾行代碼去實(shí)現(xiàn)某一個功能。是因?yàn)槲蚁M銈冋嬲龓胱约旱乃伎?,和一步步體會這個實(shí)現(xiàn)過程,從而舉一反三。
如果你認(rèn)真看了該文章,你也許會明白現(xiàn)在很多組件庫的底層實(shí)現(xiàn)原理其實(shí)就是這樣的,比如全局彈出的dialog ,modal 框等等。我們要去理解組件庫組件實(shí)現(xiàn)的思路,而不是一味的復(fù)制粘貼。
這個搜索框有很多可以更加優(yōu)化的地方,你們可以帶入自己的思考去想一想。比如
1.如何保存搜索歷史?
2.如何實(shí)現(xiàn)實(shí)時的給出搜索聯(lián)想
與君共勉才是我的初衷…
源碼
這里貼出核心代碼 SearchBar.ts
文件的源碼,希望讀者可以僅作為參考使用,希望不要直接復(fù)制粘貼。
import { h, render } from "vue" import SearchBar from "./SearchBar.vue" class SearchBarCreator { container: HTMLElement appElement: HTMLElement | null showing: boolean _dismiss: () => void constructor() { this.container = document.createElement("div") this.showing = false this.appElement = document.body.querySelector("#app") this.present.bind(this) this.dismiss.bind(this) this._dismiss = this.dismiss.bind(this) } present() { if (this.showing) { this.dismiss() } else { const SearchBar = h(h(SearchBar)) render(SearchBar, this.container) const searchBarWrapperDOM = this.container.querySelector("#searchBarWrapper") searchBarWrapperDOM?.classList.add("animate-searchInputAnimation") document.body.insertBefore(this.container, document.body.firstChild) this.showing = true this.appElement?.addEventListener("click", this._dismiss) } } dismiss() { if (this.showing && this.container) { render(null, this.container) document.body.removeChild(this.container) this.showing = false this.appElement?.removeEventListener("click", this._dismiss) } else { console.log("不需要關(guān)閉") } } }
推薦學(xué)習(xí):《vue.js視頻教程》