1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue.jsとQiita APIでリアルタイム検索機能を実装する

1
Posted at

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"

ただし、今回のコードではwatchdebounceによる自動検索も実装しています。

そのため、次の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通信回数の削減
  • 通信中、検索結果なし、エラー発生時の状態表示

特に重要なのは、watchdebounceの組み合わせです。

watchによって入力内容の変更を検知し、debounceによって入力が終わったタイミングだけAPIを呼び出すことで、不要な通信を減らしながら使いやすい検索機能を実装できます。

Vue.jsでは、APIから取得したデータを変数へ代入するだけで画面へ自動反映されます。

jQueryのように検索結果のHTMLを手動で組み立てる必要がなく、データの状態を中心に画面を管理できることがVue.jsの大きなメリットだと感じました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?