--- title: 如何用Vue寫一個搜尋功能? tags: 前端沒有盡頭的牆外調查, 六角 date: 20220331 image: --- # 如何用Vue寫一個搜尋功能? ### 利用 Computed + filter 簡單搜尋 透過 **Computed + filter** 雖然可以快速達成搜尋功能,但這個方式卻不能同時篩選兩個以上的關鍵字。 舉例來說,如果我今天使用 **「電影」** 這個關鍵字可以找到資料,但如改成使用 **「電 影」** 就會查無此人了qvq 此外,我們還可以利用v-model的 `lazy修飾符`,當我們輸入關鍵字時,input不會即時將內容綁定,除非我們按enter,或點選外面空白處,來避免一直重複觸發。 ```htmlembedded= <input type="search" v-model.lazy.trim="search" placeholder="輸入書籍、作者名稱"> <li v-for="item in filterProducts" :key="item.id"></li> ``` ```jsx= data(){ return{ data: [], // 取得遠端資料 keyWord: '' } }, computed: { filterProducts() { return this.data.filter(searchResult => searchResult.title.match(this.keyWord)); } } ``` ### 多關鍵字搜尋功能 1. 使用 `split` 並以空白格切割搜尋的字串,以形成一個陣列 : split(' ') 2. 利用forEach將keyWords和data的資料作比對 ```htmlembedded= <input type="search" v-model.lazy.trim="search" placeholder="輸入書籍、作者名稱"> <li v-for="item in filterProducts" :key="item.id"></li> ``` ```jsx= data(){ return{ data: [], // 取得遠端資料 keyWords: '' } }, computed: { filterProducts () { const strArr = this.keywords.split(' ') // 以空白格切分字串 const arr = [] // 篩選出搜尋結果的陣列 // 比對字串 strArr.forEach((str) => { this.data.forEach((item) => { if (item.title.includes(str) || item.author.includes(str)) { arr.push(item) } }) }) // 如果輸入兩個關鍵字就會出現重複的資料,所以需要刪除重複資料。 // 過濾出重複的元素 return [...new Set(arr)] } ``` 除了 `new Set()` 方法,我們也可以透過 `findIndex` 來去除陣列中重複的元素。 ```jsx= const filterArr = arr.filter((item, i) => { // 將 當前陣列的索引 與 findIndex() 回傳出的索引值進行比對, // 但 findIndex() 的方法 只會回傳 第一個 符合條件的陣列元素的索引 const index = arr.findIndex((book) => book.title === item.title) return i === index }) return filterArr ``` ### AutoComplete - input的自動完成效果 ![](https://i.imgur.com/Jm7xQ70.gif) 說到為什麼突然想起要做這個功能,是因為看了這篇 [網頁效能問題改善之 Debounce & Throttle](https://ithelp.ithome.com.tw/articles/10222749)。 雖然標題完全跟這個主題不合,但裡面提到了autoComplete的圖解,的確在很多搜尋的頁面都會看到這個設計,不然來實作看看如何? ```htmlembedded= <form class="autocomplete-container position-relative w-100 mb-4 mb-md-0" > <!-- 這裡是搜尋列 --> <div class="input-group w-md-50 me-2"> <input class="form-control" type="search" v-model.trim="search" @keyup="keyboardEvent" placeholder="輸入書籍、作者名稱" aria-label="Search" aria-describedby="button-addon2"> <button class="btn btn-primary" type="submit" id="button-addon2" @click="searchProducts"> <i class="fa-solid fa-magnifying-glass text-white"></i> </button> </div> <!-- 這裡是搜尋列 --> <!-- 這裡才是autoComplete的選單 --> <!-- 控制autoComplete的開闔 --> <ul class="autoComplete position-absolute box-shadow bg-white w-100 w-md-50 z-index-3" :class=" autoComplete ? '' : 'd-none'"> <!-- 控制按鈕事件的選取背景 --> <li class="searchHover p-2 w-100" v-for="(item,i) in filterProducts" :key="item.id" :class=" selectedIndex === i ?'bg-light': ''"> <router-link class="text-dark d-inline-block w-100" :to="`/product/${item.id}`">{{item.title}} </router-link> </li> </ul> <!-- 這裡才是autoComplete的選單 --> </form> ``` 我們可以在 autoComplete 選單內將 `filterProducts` 用 v-for 呈現,並且在 `li` 裡面放 `router-link` 讓我們可以直接透過點選 autoComplete 選單,連到書籍內頁~ ```jsx= data(){ return{ products:[] // 遠端資料庫 分頁資料十筆 productsAll: [] //遠端資料庫 所有資料 // 搜尋內容 search: '', // 用 autoComplete 來控制是否顯示選單 autoComplete: false, // 用 selectedIndex 來控制選單項目的選取 selectedIndex: 0 } } ``` ```jsx= methods:{ // 定義一個 searchProducts 的方法,在搜尋按鈕綁上這個事件 // 這裡是真正篩選書籍列表的地方,這裡不需要debouced的原因在於, //第一次就已載入productsAll,剩下都是依資料做篩選而已,不需要一直戳API。 searchProducts () { this.products = this.filterProducts this.autoComplete = false }, // 使用v-on的監聽按鍵事件,控制按鈕事件的選取背景 keyboardEvent (e) { // 按鈕向上 if (e.keyCode === 38) { if (this.selectedIndex > 0) { this.selectedIndex-- } // 按鈕向下 } else if (e.keyCode === 40) { this.selectedIndex++ // enter } else if (e.keyCode === 13) { this.filterProducts.forEach((item, i) => { if (this.selectedIndex === i) { // 當 selectedIndex 與 filterProducts 的 index相同,就將 search 改成選取項目的書名 this.search = item.title } }) } } } ``` ```jsx= watch:{ search () { // 如果有搜尋字詞的話,就顯示autoComplete選單 if (this.search) { this.autoComplete = true } else { this.autoComplete = false this.getProducts() } }, // 當選定選項,將 search 改成選取項目的書名後,關閉autoComplete選單 products () { if (this.products.length <= 1) { this.autoComplete = false } } }, // 搜尋功能 computed: { filterProducts () { const strArr = this.search.split(' ') // 以空白格切分字串 const arr = [] // 比對字串 strArr.forEach((str) => { this.productsAll.forEach((item) => { if (item.title.includes(str) || item.author.includes(str)) { arr.push(item) } }) }) // 如果輸入兩個關鍵字就會出現重複的資料,所以需要刪除重複資料。 // 過濾出重複的元素 return [...new Set(arr)] }, }, mounted(){ this.getProducts() // 表示已取得 this.products的資料 this.getAllProducts() // 表示已取得 this.productsAll的資料 } ``` #### 參考資料 : [[JavaScript] Array.prototype.findIndex() 檢查陣列元素](https://zwh.zone/javascript-array-prototype-findindex--e6-aa-a2-e6-9f-a5-e9-99-a3-e5-88-97-e5-85-83-e7-b4-a0/) [Vue.js 用 computed 跟 filter 做一個簡易搜尋功能](https://www.letswrite.tw/vue-search/)