PokéAPIアプリを自作!これまでの知識と新しい挑戦
目次
はじめに
本題
👉 アプリはこちら 🔗 https://pokemon-api-seven-black.vercel.app/
コードの説明
①二段階フェッチ
②次のポケモンの読み込み
③モーダルを開くまでの流れ
④検索機能
まとめ
はじめに
どうも、もうすぐエンジニア歴6ヶ月目の者です。
早いもので、気づけばエンジニアとして半年が経とうとしています。
現場では毎日コードを見ているうちに、
少しずつ「なんとなく読める」ようになってきました。
とはいえ、今の業務は保守・運用がメイン。
自分で一から作るとなると、またまったく違う難しさがあるんだろうなとも思っています。
本題
ということで、ここからは本題です。
前回のブログでは JSONPlaceholderのAPI を利用して、
簡単な検索アプリを作りました。
今回はその応用として、検索機能を含んだ PokéAPI アプリを制作してみました!
👉 アプリはこちら
🔗 https://pokemon-api-seven-black.vercel.app/
新たに モーダル実装 にも挑戦し、
コンポーネント分割 を意識して構成してみました。
ちなみに、URLが 「seven-black」 という
とんでもなく厨二病っぽい感じになっていますが
これは Vercel の自動生成URL です…
コードの説明
①二段階フェッチ
今回いちばんハマったのは、「二段階フェッチ」 でした。
一覧エンドポイント(/pokemon?offset=&limit=)を叩けば、
そのまま v-for で描画できると思っていたのですが、
返ってくるのは各ポケモンの名前と、
その詳細データが入っているエンドポイントのURLだけ…。
つまり、
一覧を API を叩いて取得 →
その中の詳細エンドポイントをもう一度叩く
という二段構えが必要だったんです。
実装としては、まず一覧を fetch → json で取得し、
そこから results.map(...) で詳細データを Promise.all を使って並列取得。
for で回すと1件ずつ順番に処理するのに対して、Promise.all は複数のリクエストを同時に実行できる、
という仕組みを学びました。
最終的には、全てのデータが揃った段階で、
テンプレートで扱いやすい形に整形してから描画する、
という流れにしています。
async pokemonApi(offset, limit) {
try {
// ここは fetch 専用(データだけ取得)
const listUrl = `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`
const listRes = await fetch(listUrl)
if (listRes.status !== 200) {
throw new Error(listRes.status);
}
// 一度APIを叩いて、各ポケモンの詳細URL(エンドポイント一覧)を取得
const listData = await listRes.json()
// 各ポケモンの詳細データを更に叩く
const details = await Promise.all(
listData.results.map(async (item) => {
const res = await fetch(item.url)
if (res.status !== 200) {
throw new Error(res.status);
}
const detail = await res.json()
const stats = Object.fromEntries(
detail.stats.map(s => [s.stat.name, s.base_stat])
)
const types = detail.types.map(t => t.type.name)
// 取得したAPIレスポンスを、テンプレートで扱いやすいように整形
return {
id: detail.id,
name: detail.name.charAt(0).toUpperCase() + detail.name.slice(1),
image:
detail.sprites?.other?.['official-artwork']?.front_default ??
detail.sprites?.front_default ??
'',
types,
hp: stats.hp ?? 50,
atk: stats.attack ?? 50,
def: stats.defense ?? 50,
spd: stats.speed ?? 50,
spAtk: stats['special-attack'] ?? 50,
spDef: stats['special-defense'] ?? 50,
_raw: detail,
}
})
)
return details
} catch (error) {
console.log(error);
}
this.loading = false
},②次のポケモンの読み込み
さらに、次のポケモンを読み込む処理の考え方も非常に学びになりました。
先ほどの pokemonApi 関数には、引数として offset と limit を渡していますが、
これは「次のポケモンを取得するための範囲」を指定するためのものです。
まず、初回のAPI呼び出しでは以下のようにリクエストしています。
data() {
return {
offset: 0,
limit: 48,
}
},
const listUrl = `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`→ このとき、1〜48番目のポケモンが取得されます。
その後、loadMore ボタンがクリックされると、
this.offset = this.offset + this.limitという処理が走り、offset が 0 + 48 → 48 になります。
次のAPI呼び出しはこうなります
https://pokeapi.co/api/v2/pokemon?offset=48&limit=48これにより、49番目から96番目までの48体が新たに取得され、
すでにある一覧の後ろに追加されていきます。
「こうやって 次へ の仕組みは作るのか!」と、
実際に実装しながらとても感動しました。
③モーダルを開くまでの流れ
子コンポーネント(カード側)
まず、カードコンポーネント(子)でポケモンをクリックすると、
そのクリックイベントで選択されたポケモンオブジェクトが emit されます
<div
class="card"
:data-type="pokemon.types[0] || 'normal'"
@click="openModal(pokemon)"
>
<!-- ポケモンカードの中身 -->
</div>
<script>
export default {
methods: {
async openModal(p) {
this.$emit("select-pokemon", p)
}
}
}
</script>この関数は、クリックされたポケモン p(1体分のオブジェクト)を
親コンポーネントに渡す(emitする)だけのシンプルな関数です。
親コンポーネント(受け取り側)
次に、親コンポーネントで子から渡されたデータを受け取ります
<div v-else class="grid">
<pokemon-card
v-for="pokemon in search_pokemons"
:key="pokemon.id"
class="card"
:pokemon="pokemon"
@select-pokemon="openModal"
/>
</div>ここで、
子から emit された select-pokemon イベントを@select-pokemon="openModal" で受け取り、
親側の openModal() が実行されます。
親の openModal() の処理
async openModal(p) {
this.selected = p // 一覧でクリックしたポケモンの整形済みデータ
this.details = p._raw // その中にある “APIの生データ” を別で格納
this.isModalOpen = true // モーダルを開くトリガー
}ここでは、クリックしたポケモン p をthis.selected に格納し、
その中に含まれている _raw(PokéAPIの生データ)を this.details に格納します。
そして isModalOpen = true にすることで、モーダルを開くトリガーが発火します。
モーダルへのデータ受け渡し
親で保持している selected と details を
モーダルコンポーネントに props で渡します
<transition name="pop" appear>
<app-modal
:selected="selected"
:details="details"
@close-modal="closeModal"
/>
</transition>モーダル側で受け取る
モーダル(子)コンポーネントでは、
props として受け取ったデータを利用します
props: {
selected: Object,
details: Object,
}これで、
selected… 一覧で使っていた整形済みデータ(名前・タイプ・画像など)details… PokéAPIから取得した生データ(ステータス・技など)
の両方をモーダル内で自由に扱うことができます。
④検索機能
検索機能に関しては、前回の JSONPlaceholder の API を使ったときに実装した方法をそのまま応用しました。
computed: {
search_pokemons() {
let searchWord = this.search.trim()
if (searchWord === '') {
return this.pokemons
}
return this.pokemons.filter(p =>
p.name.toLowerCase().includes(searchWord.toLowerCase())
)
}
}実装のときに意識したのは、なるべく自分の記憶を頼りに書くこと。
忘れてしまった部分は、過去の記事や参考にした Qiita を見返しました。
そうやって少しずつ体に覚えさせていくような感覚で、コーディングしています。
まとめ
今回のPokéAPIアプリでは、
APIの二段階フェッチ や 次のデータの読み込み処理、
そして モーダル実装、前回学んだ検索機能の追加 など、
たくさんの機能を組み合わせて実装しました。
本当は「お気に入り機能」なども入れたかったのですが、
時間的にも実装量的にもかなり大きくなりそうだったため、
今回はここでいったん完成としています。
今はとにかく “量をこなして慣れること” を意識していて、
これからもどんどん小さなアプリを作りながら、
技術を体に染み込ませていきたいと思っています。
次の課題としては、やはり以下の2点かなと思っています。
- Vuexを使った状態管理
- ローカルストレージを利用したお気に入り保存機能
これらは実務でもよく使う技術なので、
しっかり理解を深めていきたいと思っています。
それでは、最後まで読んでいただきありがとうございました!