こんにちは!
LIFULLエンジニアの吉永です。
本日も最近あまり関わらなくなったのであまりキャッチアップできていなかったフロントエンド開発技術についてインプットした内容を備忘録として記載していきます。
本記事の概要
Vue.js入門 Vol.1 ~jQueryとの対比編~
Vue.js入門 Vol.2 ~jQueryとの対比編~
Vue.js入門 Vol.3 ~基礎まとめで簡易家計簿を作る編~
上記3記事の続きで、外部APIを呼び出して、その結果を画面に反映させるアプリケーションを作るまでを解説していきます!
本記事で利用させていただいた外部APIと注意事項について
tsukumijimaさんの天気予報 API(livedoor 天気互換)を利用させていただきました。
2020年夏に突然サービスが終了してしまった「livedoor 天気」というサービスの互換APIを提供してくださっています。
この記事をお読みの方で、この記事に沿って動作確認アプリケーションを開発される方は事前に必ず、上記ページの注意事項を良く確認してから本記事の内容に沿って実装をするようにしてください。
※tsukumijimaさん 便利なAPIを無料で公開していただき誠にありがとうございます。
作成するアプリケーションの仕様
- 天気予報情報を外部APIから取得して画面に表示させる。
- 天気予報を取得する都道府県、地域を選択して取得ボタンを押すと外部APIを呼び出して取得する。
- 天気予報を取得する都道府県をプルダウンメニューから選択したら地域プルダウンメニューの内容を都道府県配下の地域に置き換え、未選択状態にする。
- 取得ボタンは都道府県、地域が未選択中には押せないようにする。
- 取得ボタンは外部API呼び出し中はローディング表示を行い、取得が終わるまではボタンを押せないようにする。
- 外部API呼び出しに失敗した場合はエラーメッセージを画面に表示させる。
画面仕様
初期表示
天気予報表示時
エラー発生時
天気予報取得アプリ~完成版デモ~
See the Pen 天気予報取得アプリ~完成版デモ~ by Yuta Yoshinaga (@yuta-yoshinaga) on CodePen.
この後の解説部分では各ソースコードをパーツごとに表記しているので、ソースコードの全文を見たい方は上記codepenへのリンクから参照してください。
天気予報取得アプリ実装
入力フォーム
<h1 class="title">天気予報</h1>
<div class="columns">
<div class="column">
<label class="label">都道府県</label>
<select v-model="curPref" @change="prefChange">
<option v-for="pref in prefs">{{ pref.name }}</option>
</select>
</div>
<div class="column">
<label class="label">地域</label>
<select v-model="curCity">
<option v-for="city in citys" :value="city.id">{{ city.name }}</option>
</select>
</div>
<div class="column">
<button :class="btnClass" @click="getWeather" :disabled="canSendBtn">取得</button>
</div>
</div>
入力フォームではセレクトボックスを二つとボタンを一つ用意します。
※@clickはv-onディレクティブの省略形の書き方で、:valueはv-bindディレクティブの省略形です。
エラーメッセージ表示部分
<div v-if="hasError">
<article class="message is-danger">
<div class="message-header">
<p>Error</p>
</div>
<div class="message-body">
{{ errorMessage }}
</div>
</article>
</div>
v-ifディレクティブを使ってエラーがあった際にのみ有効になる部分です。
天気予報表示部分
<table class="table is-bordered is-striped" v-if="curWether">
<tr>
<td>予報の発表日時</td>
<td>{{ curWether.publicTimeFormatted }}</td>
</tr>
<tr>
<td>予報を発表した気象台</td>
<td>{{ curWether.publishingOffice }}</td>
</tr>
<tr>
<td>タイトル・見出し</td>
<td>{{ curWether.title }}</td>
</tr>
<tr>
<td>リクエストされたデータの地域に該当する気象庁 HP の天気予報の URL</td>
<td><a :href="curWether.link">{{ curWether.link }}</a></td>
</tr>
<tr>
<td>天気概況文</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>天気概況文の発表時刻</td>
<td>{{ curWether.description.publicTimeFormatted }}</td>
</tr>
<tr>
<td>天気概況文</td>
<td>{{ curWether.description.text }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>都道府県天気予報の予報日毎の配列</td>
<td>
<table class="table is-bordered is-striped" v-for="forecast in curWether.forecasts">
<tr>
<td>予報日</td>
<td>{{ forecast.date }}</td>
</tr>
<tr>
<td>予報日(今日・明日・明後日のいずれか)</td>
<td>{{ forecast.dateLabel }}</td>
</tr>
<tr>
<td>天気(晴れ、曇り、雨など)</td>
<td>{{ forecast.telop }}</td>
</tr>
<tr>
<td>天気詳細</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>詳細な天気情報</td>
<td>{{ forecast.detail.weather }}</td>
</tr>
<tr>
<td>風の強さ</td>
<td>{{ forecast.detail.wind }}</td>
</tr>
<tr>
<td>波の高さ(海に面している地域のみ)</td>
<td>{{ forecast.detail.wave }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>最高気温</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>摂氏 (°C)</td>
<td>{{ forecast.temperature.max.celsius }}</td>
</tr>
<tr>
<td>華氏 (°F)</td>
<td>{{ forecast.temperature.max.fahrenheit }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>最低気温</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>摂氏 (°C)</td>
<td>{{ forecast.temperature.min.celsius }}</td>
</tr>
<tr>
<td>華氏 (°F)</td>
<td>{{ forecast.temperature.min.fahrenheit }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>降水確率</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>0 時から 6 時までの降水確率</td>
<td>{{ forecast.chanceOfRain.T00_06 }}</td>
</tr>
<tr>
<td>6 時から 12 時までの降水確率</td>
<td>{{ forecast.chanceOfRain.T06_12 }}</td>
</tr>
<tr>
<td>12 時から 18 時までの降水確率</td>
<td>{{ forecast.chanceOfRain.T12_18 }}</td>
</tr>
<tr>
<td>18 時から 24 時までの降水確率</td>
<td>{{ forecast.chanceOfRain.T18_24 }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>天気アイコン</td>
<td>
<img :src="forecast.image.url" :alt="forecast.image.title" :width="forecast.image.width"
:height="forecast.image.height">
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>予報を発表した地域を定義</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>地方名</td>
<td>{{ curWether.location.area }}</td>
</tr>
<tr>
<td>都道府県名</td>
<td>{{ curWether.location.prefecture }}</td>
</tr>
<tr>
<td>一次細分区域名</td>
<td>{{ curWether.location.district }}</td>
</tr>
<tr>
<td>地域名(気象観測所名)</td>
<td>{{ curWether.location.city }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>copyright</td>
<td>
<table class="table is-bordered is-striped">
<tr>
<td>コピーライトの文言</td>
<td>{{ curWether.copyright.title }}</td>
</tr>
<tr>
<td>天気予報 API(livedoor 天気互換)の URL</td>
<td><a :href="curWether.copyright.link">{{ curWether.copyright.link }}</a></td>
</tr>
<tr>
<td>天気予報 API(livedoor 天気互換)のアイコン</td>
<td><img :src="curWether.copyright.image.url" :alt="curWether.copyright.image.title"
:width="curWether.copyright.image.width" :height="curWether.copyright.image.height">
</td>
</tr>
<tr>
<td>天気予報 API(livedoor 天気互換)で使用している気象データの配信元(気象庁)</td>
<td>
<table>
<tr>
<td>link</td>
<td><a
:href="curWether.copyright.provider[0].link">{{ curWether.copyright.provider[0].link }}</a>
</td>
</tr>
<tr>
<td>name</td>
<td>{{ curWether.copyright.provider[0].name }}</td>
</tr>
<tr>
<td>note</td>
<td>{{ curWether.copyright.provider[0].note }}</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
天気予報 API(livedoor 天気互換)のレスポンスフィールドの内容を参考に、APIから取得したデータを画面上に表示させるための部分になります。
都道府県データJS
const infoTbl = {
prefs: [
{
name: "北海道",
citys: [
{ name: "稚内", id: "011000" },
{ name: "旭川", id: "012010" },
{ name: "留萌", id: "012020" },
{ name: "網走", id: "013010" },
{ name: "北見", id: "013020" },
{ name: "紋別", id: "013030" },
{ name: "根室", id: "014010" },
{ name: "釧路", id: "014020" },
{ name: "帯広", id: "014030" },
{ name: "室蘭", id: "015010" },
{ name: "浦河", id: "015020" },
{ name: "札幌", id: "016010" },
{ name: "岩見沢", id: "016020" },
{ name: "倶知安", id: "016030" },
{ name: "函館", id: "017010" },
{ name: "江差", id: "017020" },
]
},
{
name: "青森県",
citys: [
{ name: "青森", id: "020010" },
{ name: "むつ", id: "020020" },
{ name: "八戸", id: "020030" },
]
},
{
name: "岩手県",
citys: [
{ name: "盛岡", id: "030010" },
{ name: "宮古", id: "030020" },
{ name: "大船渡", id: "030030" },
]
},
{
name: "宮城県",
citys: [
{ name: "仙台", id: "040010" },
{ name: "白石", id: "040020" },
]
},
{
name: "秋田県",
citys: [
{ name: "秋田", id: "050010" },
{ name: "横手", id: "050020" },
]
},
{
name: "山形県",
citys: [
{ name: "山形", id: "060010" },
{ name: "米沢", id: "060020" },
{ name: "酒田", id: "060030" },
{ name: "新庄", id: "060040" },
]
},
{
name: "福島県",
citys: [
{ name: "福島", id: "070010" },
{ name: "小名浜", id: "070020" },
{ name: "若松", id: "070030" },
]
},
{
name: "茨城県",
citys: [
{ name: "水戸", id: "080010" },
{ name: "土浦", id: "080020" },
]
},
{
name: "栃木県",
citys: [
{ name: "宇都宮", id: "090010" },
{ name: "大田原", id: "090020" },
]
},
{
name: "群馬県",
citys: [
{ name: "前橋", id: "100010" },
{ name: "みなかみ", id: "100020" },
]
},
{
name: "埼玉県",
citys: [
{ name: "さいたま", id: "110010" },
{ name: "熊谷", id: "110020" },
{ name: "秩父", id: "110030" },
]
},
{
name: "千葉県",
citys: [
{ name: "千葉", id: "120010" },
{ name: "銚子", id: "120020" },
{ name: "館山", id: "120030" },
]
},
{
name: "東京都",
citys: [
{ name: "東京", id: "130010" },
{ name: "大島", id: "130020" },
{ name: "八丈島", id: "130030" },
{ name: "父島", id: "130040" },
]
},
{
name: "神奈川県",
citys: [
{ name: "横浜", id: "140010" },
{ name: "小田原", id: "140020" },
]
},
{
name: "新潟県",
citys: [
{ name: "新潟", id: "150010" },
{ name: "長岡", id: "150020" },
{ name: "高田", id: "150030" },
{ name: "相川", id: "150040" },
]
},
{
name: "富山県",
citys: [
{ name: "富山", id: "160010" },
{ name: "伏木", id: "160020" },
]
},
{
name: "石川県",
citys: [
{ name: "金沢", id: "170010" },
{ name: "輪島", id: "170020" },
]
},
{
name: "福井県",
citys: [
{ name: "福井", id: "180010" },
{ name: "敦賀", id: "180020" },
]
},
{
name: "山梨県",
citys: [
{ name: "甲府", id: "190010" },
{ name: "河口湖", id: "190020" },
]
},
{
name: "長野県",
citys: [
{ name: "長野", id: "200010" },
{ name: "松本", id: "200020" },
{ name: "飯田", id: "200030" },
]
},
{
name: "岐阜県",
citys: [
{ name: "岐阜", id: "210010" },
{ name: "高山", id: "210020" },
]
},
{
name: "静岡県",
citys: [
{ name: "静岡", id: "220010" },
{ name: "網代", id: "220020" },
{ name: "三島", id: "220030" },
{ name: "浜松", id: "220040" },
]
},
{
name: "愛知県",
citys: [
{ name: "名古屋", id: "230010" },
{ name: "豊橋", id: "230020" },
]
},
{
name: "三重県",
citys: [
{ name: "津", id: "240010" },
{ name: "尾鷲", id: "240020" },
]
},
{
name: "滋賀県",
citys: [
{ name: "大津", id: "250010" },
{ name: "彦根", id: "250020" },
]
},
{
name: "京都府",
citys: [
{ name: "京都", id: "260010" },
{ name: "舞鶴", id: "260020" },
]
},
{
name: "大阪府",
citys: [
{ name: "大阪", id: "270000" },
]
},
{
name: "兵庫県",
citys: [
{ name: "神戸", id: "280010" },
{ name: "豊岡", id: "280020" },
]
},
{
name: "奈良県",
citys: [
{ name: "奈良", id: "290010" },
{ name: "風屋", id: "290020" },
]
},
{
name: "和歌山県",
citys: [
{ name: "和歌山", id: "300010" },
{ name: "潮岬", id: "300020" },
]
},
{
name: "鳥取県",
citys: [
{ name: "鳥取", id: "310010" },
{ name: "米子", id: "310020" },
]
},
{
name: "島根県",
citys: [
{ name: "松江", id: "320010" },
{ name: "浜田", id: "320020" },
{ name: "西郷", id: "320030" },
]
},
{
name: "岡山県",
citys: [
{ name: "岡山", id: "330010" },
{ name: "津山", id: "330020" },
]
},
{
name: "広島県",
citys: [
{ name: "広島", id: "340010" },
{ name: "庄原", id: "340020" },
]
},
{
name: "山口県",
citys: [
{ name: "下関", id: "350010" },
{ name: "山口", id: "350020" },
{ name: "柳井", id: "350030" },
{ name: "萩", id: "350040" },
]
},
{
name: "徳島県",
citys: [
{ name: "徳島", id: "360010" },
{ name: "日和佐", id: "360020" },
]
},
{
name: "香川県",
citys: [
{ name: "高松", id: "370000" },
]
},
{
name: "愛媛県",
citys: [
{ name: "松山", id: "380010" },
{ name: "新居浜", id: "380020" },
{ name: "宇和島", id: "380030" },
]
},
{
name: "高知県",
citys: [
{ name: "高知", id: "390010" },
{ name: "室戸岬", id: "390020" },
{ name: "清水", id: "390030" },
]
},
{
name: "福岡県",
citys: [
{ name: "福岡", id: "400010" },
{ name: "八幡", id: "400020" },
{ name: "飯塚", id: "400030" },
{ name: "久留米", id: "400040" },
]
},
{
name: "佐賀県",
citys: [
{ name: "佐賀", id: "410010" },
{ name: "伊万里", id: "410020" },
]
},
{
name: "長崎県",
citys: [
{ name: "長崎", id: "420010" },
{ name: "佐世保", id: "420020" },
{ name: "厳原", id: "420030" },
{ name: "福江", id: "420040" },
]
},
{
name: "熊本県",
citys: [
{ name: "熊本", id: "430010" },
{ name: "阿蘇乙姫", id: "430020" },
{ name: "牛深", id: "430030" },
{ name: "人吉", id: "430040" },
]
},
{
name: "大分県",
citys: [
{ name: "大分", id: "440010" },
{ name: "中津", id: "440020" },
{ name: "日田", id: "440030" },
{ name: "佐伯", id: "440040" },
]
},
{
name: "宮崎県",
citys: [
{ name: "宮崎", id: "450010" },
{ name: "延岡", id: "450020" },
{ name: "都城", id: "450030" },
{ name: "高千穂", id: "450040" },
]
},
{
name: "鹿児島県",
citys: [
{ name: "鹿児島", id: "460010" },
{ name: "鹿屋", id: "460020" },
{ name: "種子島", id: "460030" },
{ name: "名瀬", id: "460040" },
]
},
{
name: "沖縄県",
citys: [
{ name: "那覇", id: "471010" },
{ name: "名護", id: "471020" },
{ name: "久米島", id: "471030" },
{ name: "南大東", id: "472000" },
{ name: "宮古島", id: "473000" },
{ name: "石垣島", id: "474010" },
{ name: "与那国島", id: "474020" },
]
},
]
};
本当はこちらも外部から取得したかったのですが、CORSの制限でブラウザJSからは取得できなかったのでひとまずJS上でデータを持つようにしました。
参照させてもらったデータはこちらです。
メイン処理JS
const app = new Vue({
el: '#app',
data: {
prefs: infoTbl.prefs,
citys: null,
curPref: null,
curCity: null,
curWether: null,
hasError: false,
errorMessage: "",
loading: false,
},
computed: {
btnClass: function () {
return {
button: true,
'is-primary': true,
'is-loading': this.loading
};
},
canSendBtn: function () {
return this.curPref && this.curCity && !this.loading ? false : true;
}
},
methods: {
prefChange: function () {
let pref = this.prefs.filter(pref => pref.name === this.curPref);
if (pref.length != 0) {
this.citys = pref[0].citys;
this.curCity = null;
}
},
getWeather: function () {
this.hasError = false;
this.errorMessage = "";
this.loading = true;
this.curWether = null;
axios.get('https://weather.tsukumijima.net/api/forecast/city/' + this.curCity)
.then(function (response) {
if (response.data) {
if (response.data.error) {
this.hasError = true;
this.errorMessage = response.data.error;
} else {
this.curWether = response.data;
}
}
}.bind(this))
.catch(function (error) {
this.hasError = true;
this.errorMessage = error;
}.bind(this))
.finally(function () {
this.loading = false;
}.bind(this))
}
}
})
ひとまず全体像です。
各詳細を以降で解説していきます。
computed
算出プロパティと呼ばれるもので、メソッドと違って関数内部で依存している値が変化したら自動的にキャッシュに反映してくれて、変化がなければキャッシュから値を取得するようにできます。
btnClass: function () {
return {
button: true,
'is-primary': true,
'is-loading': this.loading
};
},
こちらの処理は取得ボタンのclass属性を返却する算出プロパティなのですが、this.loading
というプロパティがtrueならis-loadingというクラスが取得ボタンに追加されるようになっています。
このようにしておくことで、取得ボタンを押してから、this.loading
がfalseになるまではボタンの見た目をローディング表示に変えることが可能です。
canSendBtn: function () {
return this.curPref && this.curCity && !this.loading ? false : true;
}
こちらは取得ボタンを押せるようにするかしないかを返却する算出プロパティです。
this.curPref
には都道府県選択プルダウンメニューで現在選択中の値が、this.curCity
には地域選択プルダウンメニューで現在選択中の値が入るようになっており、未選択状態であればボタンをdisabledにして押せなくするようにしています。
今回はそれらに加えて、ローディング表示中も同様にボタンを押せなくしており、こうすることでAPIからの呼び出しが完了するまでの間はボタンを押せないので2重送信をできないようにすることができます。
methods
prefChange: function () {
let pref = this.prefs.filter(pref => pref.name === this.curPref);
if (pref.length != 0) {
this.citys = pref[0].citys;
this.curCity = null;
}
},
こちらは都道府県選択プルダウンメニューで選択値が変化した際に呼ばれるメソッドです。
this.curPref
には現在選択中の都道府県の名称が入っているので、this.prefs
というマスタデータ内を検索して、一致したデータから子要素の地域情報を取得してthis.citys
に代入しています。
this.citys
はHTML側ではv-forディレクティブで参照されており、内容が変わると自動的にHTML側にも反映されます。
最後にthis.curCity
をnullにすることで、地域選択プルダウンメニューを未選択上に初期化しています。
getWeather: function () {
this.hasError = false;
this.errorMessage = "";
this.loading = true;
this.curWether = null;
axios.get('https://weather.tsukumijima.net/api/forecast/city/' + this.curCity)
.then(function (response) {
if (response.data) {
if (response.data.error) {
this.hasError = true;
this.errorMessage = response.data.error;
} else {
this.curWether = response.data;
}
}
}.bind(this))
.catch(function (error) {
this.hasError = true;
this.errorMessage = error;
}.bind(this))
.finally(function () {
this.loading = false;
}.bind(this))
}
最後に外部API呼び出し部分です。
このメソッドは取得ボタンクリック時に呼ばれていて、API呼び出し前にまずは各種ステータスや変数を初期化しています。
ここでthis.loading
をtrueにすることで、算出プロパティが更新されて画面にも反映されるようになります。
外部取得用の処理にはaxiosというブラウザやnode.jsで動くPromiseベースの HTTPクライアントを使用しています。
jQueryで良く使用されていた$.ajaxと似たようなものですが、最近ではこちらを利用するのが主流のようです。
通信完了後はエラーがあればcatchへ、通信上ではエラーがなければthenの中の処理が実行されます。
ただし、thenにはHTTPのレスポンスコードが2xx系などの時に来るようになっているので、リクエストパラメーターにエラーがあった際などには外部APIの仕様次第ですが、レスポンスコードは2xx系で返却し、エラーメッセージをレスポンスデータに格納しているパターンもあります。
天気予報 API(livedoor 天気互換)はレスポンスコードは2xx系でもエラーメッセージが格納されていることもあるので、thenの中でもエラーがないかを確認してから取得したデータをVueオブジェクトのプロパティへ代入するようにしています。
この辺りは呼び出し対象のAPIの仕様書を読むか、動作確認を行ってわざとエラーを発生させてみてなどの挙動を確認しておくと良いと思います。
最後に
いかがでしたでしょうか?
今回はJSから外部APIを呼び出してVue.jsで利用する実装例をご紹介しました。
jQueryでは重宝していたajaxの代用になるaxiosというライブラリがあることも知れたのと、利用方法はajaxとそんなに変わっていないのですんなりと処理を記述できたことがjQueryでの開発経験が少し役に立ったようで良かったです。
あとは登録不要で簡単に利用できるtsukumijimaさんの天気予報 API(livedoor 天気互換)には本当に感謝です!おかげで学習がはかどりました!
皆さんも外部API呼び出しの学習用やテスト用に使えるAPIなどを是非ご自身で調べてみてください。
※利用規約などは事前にきちんと確認しましょう!
それではまた次の記事でお会いしましょう!