---
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/)