Vue.jsとQiita APIでリアルタイム検索機能を実装する
はじめに
Vue.jsでは、入力フォームの値を監視し、ユーザーが入力したキーワードに応じてAPI通信を行うリアルタイム検索機能を実装できます。
今回は、Vue.js、Axios、Lodash、Qiita APIを使用して、入力したキーワードに関連するQiitaの記事を検索する機能を作成します。
実装する機能は次のとおりです。
- 入力内容を
v-modelで取得する -
watchでキーワードの変更を監視する - AxiosでQiita APIへリクエストを送信する
- 検索結果を
v-forで一覧表示する - Lodashの
debounceでAPIの連続実行を防ぐ - 通信中やエラー発生時にメッセージを表示する
完成イメージ
検索フォームに「JavaScript」や「Vue.js」などのキーワードを入力すると、該当するQiitaの記事が一覧表示されます。
検索ボタンを押さなくても、入力を止めてから1秒後に自動的に検索が実行されます。
HTML
<div id="app">
<p>
<input
type="text"
v-model="keyword"
placeholder="検索キーワードを入力"
>
</p>
<button @click="getAnswer">検索</button>
<p>{{ message }}</p>
<ul v-if="items">
<li v-for="item in items" :key="item.id">
<a :href="item.url" target="_blank" rel="noopener noreferrer">
{{ item.title }}
</a>
<span>
LGTM数: {{ item.likes_count }}
</span>
</li>
</ul>
</div>
JavaScript
const app = Vue.createApp({
data: () => ({
items: null,
keyword: '',
message: ''
}),
watch: {
keyword: function (newKeyword, oldKeyword) {
console.log('watching keyword', newKeyword, oldKeyword)
this.message = '入力が終わるまで待機しています...'
this.debounceGetAnswer()
}
},
mounted: function () {
this.debounceGetAnswer = _.debounce(this.getAnswer, 1000)
},
methods: {
getAnswer: function () {
if (this.keyword.trim() === '') {
console.log('検索キーワードが空です')
this.items = null
this.message = ''
return
}
this.message = '読み込み中...'
const params = {
page: 1,
per_page: 20,
query: this.keyword
}
axios
.get('https://qiita.com/api/v2/items', { params })
.then((response) => {
console.log(response)
this.items = response.data
this.message = ''
})
.catch((error) => {
console.error(error)
this.items = null
this.message = `エラーが発生しました: ${error.message}`
})
}
}
})
app.mount('#app')
処理の流れ
今回のリアルタイム検索は、次の順番で動作します。
ユーザーが文字を入力する
↓
keywordの値が変更される
↓
watchが変更を検知する
↓
debounceGetAnswerが実行される
↓
入力が1秒間止まる
↓
getAnswerが実行される
↓
Qiita APIへリクエストを送る
↓
取得した記事を画面に表示する
v-modelで入力値を取得する
<input type="text" v-model="keyword">
v-modelは、フォームの入力値とVue.jsのデータを連携させる機能です。
今回のコードでは、入力欄に文字を入力すると、次のkeywordへ自動的に値が反映されます。
data: () => ({
keyword: ''
})
例えば、ユーザーが入力欄へ「Vue.js」と入力すると、keywordの値も次のようになります。
keyword: 'Vue.js'
HTML要素から入力値を直接取得する必要がないため、jQueryよりもシンプルに状態を管理できます。
watchでキーワードの変更を監視する
watch: {
keyword: function (newKeyword, oldKeyword) {
console.log('watching keyword', newKeyword, oldKeyword)
this.message = '入力が終わるまで待機しています...'
this.debounceGetAnswer()
}
}
watchは、指定したデータの変更を監視する機能です。
ここでは、keywordの値が変更されるたびに処理が実行されます。
コールバック関数では、変更後と変更前の値を受け取れます。
newKeyword
oldKeyword
例えば「Vue」から「Vue.js」に変更された場合、次のような値になります。
newKeyword: Vue.js
oldKeyword: Vue
watchは、値が変更されたタイミングでAPI通信やバリデーションなどを実行したい場合に利用できます。
debounceでAPIの連続実行を防ぐ
入力内容をwatchで監視するだけでは、文字を入力するたびにAPI通信が発生します。
例えば「JavaScript」と入力すると、次のように何度も処理が実行される可能性があります。
J
Ja
Jav
Java
JavaS
JavaSc
JavaScr
JavaScri
JavaScrip
JavaScript
このままでは不要なAPI通信が増えてしまいます。
そこで、Lodashのdebounceを使用します。
mounted: function () {
this.debounceGetAnswer = _.debounce(this.getAnswer, 1000)
}
debounceは、処理の実行を一定時間待機させる機能です。
今回の設定では、ユーザーが入力を止めてから1秒後にgetAnswerが実行されます。
_.debounce(this.getAnswer, 1000)
1000は1,000ミリ秒、つまり1秒を意味します。
これにより、1文字入力するたびにAPI通信するのではなく、入力が終わったと判断されたタイミングで検索できます。
mountedでdebounce処理を準備する
mounted: function () {
this.debounceGetAnswer = _.debounce(this.getAnswer, 1000)
}
mountedは、Vue.jsのコンポーネントがHTMLへ反映されたあとに実行されるライフサイクルフックです。
今回のコードでは、画面が準備されたタイミングでdebounceGetAnswerを作成しています。
その後、watch内から次のように呼び出しています。
this.debounceGetAnswer()
空文字の場合は検索しない
if (this.keyword.trim() === '') {
this.items = null
this.message = ''
return
}
キーワードが空の場合は、API通信を実行せずに処理を終了します。
trim()を使用することで、スペースだけが入力された場合も空文字として扱えます。
this.keyword.trim()
例えば、次の入力はいずれも検索対象外になります。
''
' '
検索キーワードが空になった場合は、以前の検索結果も削除します。
this.items = null
AxiosでQiita APIへリクエストする
const params = {
page: 1,
per_page: 20,
query: this.keyword
}
APIへ渡す検索条件をparamsとして定義しています。
それぞれの役割は次のとおりです。
| プロパティ | 内容 |
|---|---|
page |
取得するページ番号 |
per_page |
1回に取得する記事数 |
query |
検索キーワード |
AxiosのgetメソッドでQiita APIへリクエストします。
axios.get('https://qiita.com/api/v2/items', { params })
実際には、次のような検索条件がURLへ付加されます。
?page=1&per_page=20&query=Vue.js
APIの取得結果を保存する
.then((response) => {
this.items = response.data
this.message = ''
})
API通信が成功すると、取得結果がresponseへ渡されます。
Qiitaの記事データは、次のresponse.dataに格納されています。
response.data
取得したデータをitemsへ代入します。
this.items = response.data
Vue.jsではitemsが変更されると、それに連動して画面も自動的に更新されます。
これがVue.jsのリアクティブな仕組みです。
v-forで検索結果を一覧表示する
<li v-for="item in items" :key="item.id">
<a :href="item.url" target="_blank" rel="noopener noreferrer">
{{ item.title }}
</a>
<span>
LGTM数: {{ item.likes_count }}
</span>
</li>
v-forは、配列のデータを繰り返し表示するためのディレクティブです。
v-for="item in items"
itemsに格納された記事を1件ずつitemとして取り出しています。
今回表示している記事情報は次のとおりです。
item.title
item.url
item.likes_count
| プロパティ | 内容 |
|---|---|
item.title |
記事タイトル |
item.url |
記事のURL |
item.likes_count |
LGTM数 |
:keyを設定する理由
:key="item.id"
:keyは、Vue.jsが一覧内の各要素を識別するために使用します。
記事ごとに異なるidを指定することで、Vue.jsが効率よく画面を更新できるようになります。
v-forを使用するときは、基本的に一意な値を:keyへ指定します。
v-ifで検索結果がある場合だけ表示する
<ul v-if="items">
v-ifは、条件によってHTML要素を表示・非表示にするディレクティブです。
今回のコードでは、itemsにデータが入っている場合だけ一覧を表示します。
初期値はnullなので、最初は検索結果が表示されません。
items: null
API通信が成功して配列が代入されると、一覧が表示されます。
通信状態をメッセージで表示する
<p>{{ message }}</p>
messageには、現在の処理状態を設定します。
入力中は次のメッセージを表示します。
this.message = '入力が終わるまで待機しています...'
API通信中は次のメッセージを表示します。
this.message = '読み込み中...'
通信が成功した場合は、メッセージを空にします。
this.message = ''
通信に失敗した場合は、エラー内容を表示します。
this.message = `エラーが発生しました: ${error.message}`
このように処理状態を画面へ表示することで、ユーザーは検索が実行されているのか、失敗したのかを判断できます。
thisを変数へ代入する方法について
元のコードでは、次のようにVueインスタンスを変数へ保存しています。
const vm = this
その後、通常の関数内からvmを利用しています。
.then(function (response) {
vm.items = response.data
})
通常のfunctionを使用すると、関数内のthisがVueインスタンスを指さなくなるためです。
一方、アロー関数を使用すると、外側のthisをそのまま利用できます。
.then((response) => {
this.items = response.data
})
そのため、今回の修正版ではconst vm = thisを使用せず、アロー関数で記述しています。
Enterキーでも検索する場合
検索ボタンだけでなく、Enterキーを押したときにも検索する場合は、次のように記述できます。
<input
type="text"
v-model="keyword"
@keyup.enter="getAnswer"
>
@keyup.enterは、Enterキーが離されたタイミングで処理を実行します。
@keyup.enter="getAnswer"
ただし、今回のコードではwatchとdebounceによる自動検索も実装しています。
そのため、次の3種類の検索方法を組み合わせられます。
- 入力停止後に自動検索
- Enterキーで検索
- 検索ボタンで検索
検索結果が0件の場合の改善
API通信が成功しても、検索結果が0件の場合があります。
その場合は、次のようにメッセージを表示できます。
.then((response) => {
this.items = response.data
if (this.items.length === 0) {
this.message = '該当する記事が見つかりませんでした'
return
}
this.message = ''
})
改善後のJavaScript全体
const app = Vue.createApp({
data: () => ({
items: null,
keyword: '',
message: ''
}),
watch: {
keyword: function () {
if (this.keyword.trim() === '') {
this.items = null
this.message = ''
return
}
this.message = '入力が終わるまで待機しています...'
this.debounceGetAnswer()
}
},
mounted: function () {
this.debounceGetAnswer = _.debounce(this.getAnswer, 1000)
},
methods: {
getAnswer: function () {
const keyword = this.keyword.trim()
if (keyword === '') {
this.items = null
this.message = ''
return
}
this.message = '読み込み中...'
const params = {
page: 1,
per_page: 20,
query: keyword
}
axios
.get('https://qiita.com/api/v2/items', { params })
.then((response) => {
this.items = response.data
if (this.items.length === 0) {
this.message = '該当する記事が見つかりませんでした'
return
}
this.message = ''
})
.catch((error) => {
console.error(error)
this.items = null
this.message = `エラーが発生しました: ${error.message}`
})
}
}
})
app.mount('#app')
まとめ
今回は、Vue.jsとQiita APIを使用してリアルタイム検索機能を実装しました。
実装を通して、次の機能について学習できました。
-
v-modelによる入力値とデータの連携 -
watchによるデータ変更の監視 - Axiosを使用したAPI通信
-
v-forによる検索結果の一覧表示 -
v-ifによる表示制御 - Lodashの
debounceによるAPI通信回数の削減 - 通信中、検索結果なし、エラー発生時の状態表示
特に重要なのは、watchとdebounceの組み合わせです。
watchによって入力内容の変更を検知し、debounceによって入力が終わったタイミングだけAPIを呼び出すことで、不要な通信を減らしながら使いやすい検索機能を実装できます。
Vue.jsでは、APIから取得したデータを変数へ代入するだけで画面へ自動反映されます。
jQueryのように検索結果のHTMLを手動で組み立てる必要がなく、データの状態を中心に画面を管理できることがVue.jsの大きなメリットだと感じました。