Lesson 9
Vue アプリの作成
目次
Lesson 9
Chapter 1
作成するアプリケーションの概要
これまでのレッスンで、Vue の基本的な使い方を一通り学んできました。
今回のレッスンでは、学んできた技術を使用して、簡単なアプリケーションの作成を行います。
1. アプリケーションのイメージ
ここで作成するのは、天気予報を表示するアプリケーションです。
完成イメージは次のとおりです。
一般的に Web アプリケーションの作成をする場合は、フロントエンドとともにバックエンドの実装が必要ですが、ここでは、バックエンドの代わりに、気象庁が公開している天気予報の API を使用して作成していきます。
2. アプリケーションの構成
どのような構成のアプリケーションにするか、簡単な設計を行っていきます。
2-1. 画面構成
画面は、次の 3 つを用意します。
- ホーム 画面(Home.vue):全国の天気予報を表示
- 天気概況 画面(Overview.vue):選択した地域の天気概況を表示
- 週間予報 画面(Week.vue):選択した地域の週間予報を表示
次の図のように、ヘッダー部分のタブで画面遷移を行います。
左メニューで、どの地域の情報を表示するかを選択できるようにします。
なお、このメニューは、ヘッダーの左側のボタンで開閉できるようにします。
2-2. ルーティング
作成する 3 つの画面に対応するように、次のようにルーティングを設定します。
No | パス | 表示コンポーネント | 備考 |
---|---|---|---|
1 |
/
|
Home.vue | ホーム画面。全国の天気を表示。 |
2 |
/overview/:code
|
Overview.vue |
天気概況画面。指定された :code に対応する地域の天気概況を表示する。
|
3 |
/week/:code
|
Week.vue |
週間予報画面。指定された :code に対応する地域の週間天気予報を表示する。
|
なお、天気概況画面、週間予報画面は、選択した地域によって表示内容を出し分けるため、パラメータ code
を含む動的ルートマッチングにします。
2-3. ファイルの構成
ファイル構成は次のように設計します。
src
├ components
│ └ WeekTable.vue // 週間天気予報を表示する共通テーブル
├ router
│ └ index.js // Vue Router のルートファイル
├ stores
│ ├ area.js // 地域リストを管理する
│ └ forecast.js // 天気予報データを管理する
├ views
│ ├ Home.vue // ホーム画面表示
│ ├ Overview.vue // 天気概況画面表示
│ └ Week.vue // 週間予報画面表示
├ App.vue
├ constant.js // 定数を定義する
├ main.js
└ ...
ディレクトリ構成は、Vite プロジェクト生成時の形式をそのまま踏襲します。
views ディレクトリには、タブの切り替えで表示される 3 つのコンポーネントを定義します。
components ディレクトリには、週間予報を表形式で表示するための WeekTable コンポーネントを定義します。これは、複数のコンポーネントから共通で使用されるものです。
router ディレクトリには、ルーティングの定義ファイルを置きます。
stores ディレクトリには、2 つのストアを定義します。
area.js
ストアは、地域に関するデータを取得・管理します。
forecast.js
ストアは、天気予報に関するデータを取得・管理します。
2-4. 使用するプラグインなど
作成するアプリには、次のプラグインを使用します。
No | プラグイン | 説明 |
---|---|---|
1 | Vue Router | ルーティングは、Home,Overview,Week の 3 コンポーネントに対して行う。 |
2 | Pinia | 天気予報 API からのデータ取得と管理を行う。 |
3 | Quasar | Vue の UI フレームワーク。画面全体の枠組みを作成するために使用。 |
Vue Router と Pinia については、既に学習をしました。
新しく使用する Quasar(クエーサー)は、Vue の UI を作成するためのフレームワークです。ここでは、画面実装の枠組みを作るために使用します。
2-5. 天気予報 API について
① 天気予報 API の利用にあたって
天気予報 API は、Lesson 8 で使用したものと同じ、気象庁の API を使用します(※)。
天気予報 API
この天気予報 API は、元々、気象庁の Web サイトで使用するために作成されたもので、一般に利用されることを想定して作られたものではありません。
そのため、「仕様の継続性や運用状況のお知らせを気象庁はお約束していない」ものであり、予告なく仕様変更が行われる可能性があります(参考)。
なお、一般に利用すること自体は問題なく、その際は、他の政府公開情報と同様に「政府標準利用規約に準拠」することが必要となります。
この API は、個人的な学習に使用することは問題ありません。
一般に公開するアプリケーションなどに使用する場合は
「出典を明記すること」、「第三者の権利を侵害しないこと」など、気象庁ホームページの 利用規約
を守ってご使用ください。
② 使用する API の一覧
今回のアプリケーションでは、天気予報 API のうち、以下の API(URL)を使用します。
この URL は、どこかに仕様が公開されているわけではなく、Web サイト上の断片的な情報と、実際に気象庁の天気予報サイトを確認しながら収集したものです。
No | 内容 | url |
---|---|---|
1 | 地域コード一覧取得 | https://www.jma.go.jp/bosai/common/const/area.json |
2 | 全国主要地域の天気予報取得 | https://www.jma.go.jp/bosai/forecast/data/forecast/010000.json |
3 | 指定地域の天気予報概況取得 | https://www.jma.go.jp/bosai/forecast/data/overview_forecast/地域コード.json |
4 | 指定地域の週間天気予報取得 | https://www.jma.go.jp/bosai/forecast/data/forecast/地域コード.json |
上記表中の No 3 と No 4 の URL に含まれる 地域コード
は、地域によって異なります。
例えば、東京の場合は 130000
、神奈川の場合は 140000
というようになっています。
これらのコードについては No 1 の「地域コード一覧取得」の API により取得することができます(細かくは後ほど見ていきます)。
設計というには簡単ですが、おおよそ以上の構成にて、アプリケーションの作成を進めていきます。

Lesson 9
Chapter 2
Vite プロジェクト作成
新しく Vite プロジェクトを作成して、開発を進めていきます。
1. create-vue ライブラリで作成
create-vue ライブラリを使用して、Vite プロジェクト作成時に、Vue Router と Pinia の 2 つのプラグインをあらかじめインストールするようにします。
Node.js は、現時点で最新の v18.13.0 を使用します。
No | 名称 | バージョン | 備考 |
---|---|---|---|
1 | Node.js | 18.13.0 | 本カリキュラム作成時で最新の LTS 版 |
2 | Vue.js | 3.2 | Vite プロジェクト作成時は、メジャーバージョン 3 のみ指定する |
Lesson 7 でも create-vue ライブラリを使用して Vite プロジェクトを作成しましたが、復習を兼ねて、ここでも 1 つずつ説明していきます(詳細は、公式ページ参照)。
① 作業フォルダに移動
まず、PowerShell を開いて、プロジェクトを作成するフォルダに移動します。
PS C:\Users\username> cd C:\vue_lesson
PS C:\vue_lesson>
② Vite プロジェクト生成コマンドの実行
Vue プロジェクトを作成するコマンドを入力します。
PS C:\vue_lesson> npm init vue@3
③ プロジェクト名の指定
プロジェクト名、ここでは「weather-app」と入力して Enter キーを押します(他の名前でも OK です)。
④ TypeScript を使用するか否か
ここでは、TypeScript は使用しませんので、No のまま Enter キーを押します。
⑤ JSX Support を使用するか否か
ここでは、JSX Support は使用しませんので、No のまま Enter キーを押します。
⑥ Vue Router を使用するか否か
Vue Router は使用しますので「Yes」を選択して Enter キーを押します。
⑦ Pinia を使用するか否か
Pinia も使用しますので「Yes」を選択して Enter キーを押します。
⑧ その他の選択肢
以下は、全て「No」のままで Enter キーを押していきます。
以上の状態になれば、プロジェクトの作成は完了です。
⑨ npm install の実行
プロジェクトが作成されたら weather-app
プロジェクトを VSCode で開き、ターミナルを開いて npm install
を実行します。
プロジェクトを作成する時期によって Warnig が出たりしますが、Error さえ出なければ OK としてください。
⑩ ローカルサーバを起動してブラウザで表示
npm run dev
コマンドを打ってサーバを起動しましょう。
以上のように表示されれば、新規プロジェクトの作成は完了です。
確認ができたら「ctrl + c」または「q」を入力して、サーバを停止しておいてください。
Git を使用する場合
本カリキュラムでは、特に Git を使用する必要はありません。
既に、Git の講座を受けられているなどで、Git で管理をされたい方は、この時点で、ターミナルから以下のコマンドを打って Git を導入しても差し支えありません。
$ git init
なお、本レッスンで表示する VSCode の画面は、Git を使用しているため変更箇所にマーキングが付いていたりします。お手元の画面と異なっていても問題ありませんので気にせず進めてください。
2. Quasar の導入
2-1. Quasar のインストール
次に、Quasar(クエーサー)をインストールします。
Quasar にも以下のような公式ページがあります。
Vite プロジェクトへのインストール方法は、以下のページに説明がありますので、そのとおりに行います。
ここでは npm を使用しますので、次のコマンドでインストールを行います。
sass@1.32.12
も使用しますので、コマンドは 2 つとも実行する必要があります。
$ npm install quasar @quasar/extras
$ npm install -D @quasar/vite-plugin sass@1.32.12
それでは、VSCode のターミナルを開いて、下図のように 2 つのコマンドを実行してみましょう。
インストールが成功すると、次のように package.json
ファイルに quasar
と sass
に関するライブラリが追加されていることが確認できます。
2-2. Quasar をプラグインとして追加する
次に、クエーサーを Vite プロジェクトのプラグインとして追加します。
先ほど見た、Quasar 公式のインストール説明ページのすぐ下 Using Quasar のところに、導入方法の説明があります。
様々なスイッチボタンがあり、画面の設定内容を選べるようになっていますが、今回は、デフォルトのままで進めます。
① main.js ファイルの修正
最初に書いてあるのは main.js
ファイルの記載方法です。
先ほど作成した weather-app プロジェクトの src\main.js
ファイルを開いて、次のように赤枠部分を追加します。また、緑枠の部分は削除します。
コードをテキストで表示
src\main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { Quasar } from 'quasar'
// Import icon libraries
import '@quasar/extras/material-icons/material-icons.css'
// Import Quasar css
import 'quasar/src/css/index.sass'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(Quasar, {
plugins: {}, // import Quasar plugins and add here
})
app.mount('#app')
import のところでは、クエーサー本体と、アイコンのライブラリおよび CSS も読み込んでいます。
なお、src\assets\main.css
の import を削除したので、以下の 2 つのファイル(main.css
および base.css
)も削除しておきます。
② vite.config.js ファイルの修正
次に vite.config.js
ファイルの修正です。
ルート直下にある vite.config.js
ファイルを開いて、次の赤枠部分を追加・修正します。
コードをテキストで表示
vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: { transformAssetUrls }
}),
quasar({
sassVariables: 'src/quasar-variables.sass'
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
plugins:
の中で指定している 'src/quasar-variables.sass'
というファイルは、次の項で追加します。
③ quasar-variables.sass ファイルの追加
次に quasar-variables.sass
ファイルの追加です。
src ディレクトリの直下に、以下のように quasar-variables.sass
ファイルを追加します。
コードをテキストで表示
src\quasar-variables.sass
$primary : #1976D2
$secondary : #26A69A
$accent : #9C27B0
$dark : #1D1D1D
$positive : #21BA45
$negative : #C10015
$info : #31CCEC
$warning : #F2C037
以上で、Quasar をプラグインとして追加することが完了しました。
2-3. Quasar のベース画面を追加する
次に、App.vue
に、Quasar のベースの画面を追加します。
公式ページのヘッダーにある「Tools」から「Layout Builder」を選択します。
次のような Layout Builder の画面が表示されます。
ここでは、下図のとおり、QHeader(ヘッダー部分)、left-side QDrawer(左メニュー)、navigation tabs(切り替えタブ)の 3 つを選択して「EXPORT LAYOUT」ボタンをクリックします。
次のように、デフォルトのコードが表示されます。
右上のコピーボタンからコードのテキストをコピーして使用します。
Layout Builder のコピーテキスト
Layout Builder のコピーテキスト
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/page1" label="Page One" />
<q-route-tab to="/page2" label="Page Two" />
<q-route-tab to="/page3" label="Page Three" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script>
import { ref } from 'vue'
export default {
setup () {
const leftDrawerOpen = ref(false)
return {
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
}
}
</script>
src\App.vue
を開いて修正を行います。
Layout Builder で生成されたテキストで全部のコードを置換えても動作しますが、ここでは次のように修正をしておきます。
<template> 部分については、Layout Builder のテキストからそのまま使用します。
<script> の部分は、script setup 構文に書き換えて使用します。
<styles> の部分は不要なため、全て削除しておきます。
コードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/page1" label="Page One" />
<q-route-tab to="/page2" label="Page Two" />
<q-route-tab to="/page3" label="Page Three" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped></style>
2-4. ブラウザで確認
まだ、Quasar の画面を適用したのみですが、ローカルサーバを起動して画面を確認してみましょう。
次のように表示されれば導入完了です。
まだ、ルーティングの設定ができていないことから、タブをクリックしても正常に動作はしませんが、左メニューの開閉などがデフォルトで実装されている状態になっています。

Lesson 9
Chapter 3
ルーティングの設定
まず、ルーティングの設定から行っていきましょう。
1. コンポーネントの仮作成
ルーティングで表示するコンポーネントを仮作成しておきます。
1-1. 不要なファイルの削除
先に、プロジェクト作成時に自動生成されている不要なファイルを削除します。
以下の赤枠で囲んでいる 5 つのコンポーネント、および緑枠で囲んでいる icons
フォルダを削除しておきます。
削除したフォルダおよびファイルは次のとおりです。これ以外のファイルは削除しないようにしてください。
- src\components\icons
- src\components\HelloWorld.vue
- src\components\TheWelcome.vue
- src\components\WelcomeItem.vue
- src\views\AboutView.vue
- src\views\HomeView.vue
1-2. コンポーネントの追加
それでは、必要なコンポーネントを追加していきます。
以下のように、src\views
ディレクトリに 3 つのコンポーネントを追加します(Home.vue
,Overview.vue
,Week.vue
)。
それぞれのコンポーネントに記載したコードは、次のとおりです。単にコンポーネント名を表示するだけです。
src\views\Home.vue
<template>
<h2>Home</h2>
</template>
src\views\Overview.vue
<template>
<h2>Overview</h2>
</template>
src\views\Week.vue
<template>
<h2>Week</h2>
</template>
2. ルーティングの修正
それでは、ルーティングの設定をしていきましょう。
ルーティングは、次の内容で設定を行います。
No | パス | 表示コンポーネント | 備考 |
---|---|---|---|
1 |
/
|
Home.vue | ホーム画面。全国の天気を表示。 |
2 |
/overview/:code
|
Overview.vue |
天気概況画面。指定された :code に対応する地域の天気概況を表示する。
|
3 |
/week/:code
|
Week.vue |
週間予報画面。指定された :code に対応する地域の週間天気予報を表示する。
|
① ルーティングの修正
src\router\index.js
ファイルを開いて次のように修正します(赤枠部分)。
コード全体をテキストで表示
src\App.vue
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Overview from '../views/Overview.vue'
import Week from '../views/Week.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/overview/:code',
name: 'overview',
component: Overview
},
{
path: '/week/:code',
name: 'week',
component: Week
},
]
})
export default router
ルートの設定部分は次のようになっています。
src\router\index.js
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/overview/:code',
name: 'overview',
component: Overview
},
{
path: '/week/:code',
name: 'week',
component: Week
},
]
overview
と week
の 2 つのルートについては、動的ルートマッチング(Lesson 7 参照)にて設定を行っています。
② App.vue コンポーネントの修正
続いて、App.vue コンポーネントの切り替えタブの部分を修正していきます。
修正部分は、次の赤枠部分のとおりです。
コード全体をテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped></style>
修正部分には、次のように Quasar の q-route-tab
タグが使用されています。
src\App.vue
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
この q-route-tab
タグには、router-link プロパティもバインドされているため、to
属性にルートの遷移先を指定することができます(公式ページ 参照)。
なお、overview
と week
の遷移先 URL には、パラメータとして地域コード(code
)の指定が必要ですが、現段階では 1
で固定しておきます。
③ ブラウザで動作確認を行う
それでは、設定したルーティングについて、ブラウザで動作確認を行いましょう。
ローカルサーバを起動して、ブラウザで表示すると、まず、Home コンポーネントが表示されることが確認できます。
続いて、天気概況タブ、週間予報タブを押してみて、コンポーネントの表示と URL が適切に切り替わっているか確認してみてください。
以上で、ルーティングの設定は完了です。

Lesson 9
Chapter 4
ストアの作成と HTTP 通信
本アプリケーションでは、Store は 2 つ作成します。
1 つは「地域コードを取得する area ストア」で、もう 1 つは「天気予報情報を取得する forecast ストア」です。
1. area ストアの作成
まず「地域コードを取得する area ストア」の作成から行います。
src/stores
ディレクトリに、area.js
ファイルを作成します(赤枠部分)。
なお、デフォルトで作成されている counter.js
ファイルは削除しておいてください(緑枠部分)。
area.js
ファイルには次のコードを記載します。
src\stores\area.js
import { defineStore } from 'pinia'
export const useAreaStore = defineStore('area', {
state: () => {
return {
area: {
offices: {}
},
code: '130000'
}
},
actions: {
async fetchArea() {
const url = 'https://www.jma.go.jp/bosai/common/const/area.json'
const response = await fetch(url)
this.area = await response.json()
}
}
})
以下、ファイルに記載した内容について見ていきます。
2. State の定義内容
State は次のように定義しています。
state: () => {
return {
area: {
offices: {}
},
code: '130000'
}
},
以下の 2 つのプロパティを設定しています。
No | プロパティ | 説明 |
---|---|---|
1 |
area
|
地域コード一覧取得 API から返却されたレスポンスを保存する。 |
2 |
code
|
ユーザーが選択中の地域コードを保存する。 |
area
プロパティには、Actions で取得した、地域コード一覧取得 API のレスポンスが格納されます。
3. Actions の定義内容
actions では、fetchArea()
というアクションを 1 つ設定しています。
この fetchArea()
アクションは、天気予報 API のうち、地域コード一覧を取得する API を実行するものです。
actions: {
async fetchArea() {
const url = 'https://www.jma.go.jp/bosai/common/const/area.json'
const response = await fetch(url)
this.area = await response.json()
}
}
上記のコードは、Lesson 8 でも同様のものを書きましたが、要は「URL を指定して、そこから返却されるレスポンスデータを取得する」というものになります。
このコードの内容については、次の項で詳しく見ていきます。
4. Web サーバとの通信
URL からデータを取得するには、クライアント PC と Web サーバの間で HTTP 通信を行う必要があります(※)。
HTTP 通信とは
「HTTP 通信」を簡単に言うと、クライアント側(ユーザー側)と Web サーバの間で、データのやり取りを行う仕組みのことです。
基本的には、下表にある HTTP メソッドと URL などを送信情報としてサーバに送り、データの取得/登録/更新/削除などの処理を行うことになります。
No | HTTP メソッド | 主な送信情報 | 説明 |
---|---|---|---|
1 | GET | URL、クエリパラメータ | URL とクエリパラメータで指定されたデータを取得する |
2 | POST | URL、リクエストボディ | URL とリクエストボディで指定された内容でデータを生成する |
3 | PUT,PATCH | URL、リクエストボディ | URL とリクエストボディで指定された内容でデータを更新する |
4 | DELETE | URL | URL で指定されたデータを削除する(クエリパラメータを指定する場合もある) |
例えば、ブラウザを使用すれば、次のように URL を入力するだけで HTTP 通信(GET メソッド)を行うことができ、該当ページのデータを取得することができます。
上のような HTTP 通信を JavaScript 上で制御するには、幾つか方法がありますが、ここでは簡単かつ直感的に使用できる Fetch API というものを使用します(※)。
Fetch API について
Fetch API は、2017年頃に JavaScript の標準仕様とされた比較的新しい API で、サーバとの HTTP 通信を行うために使用するものです。
Fetch API の登場以前、JavaScript でサーバと HTTP 通信を行うには XMLHttpRequest オブジェクトというものを使用する必要があり、実装が非常に複雑になっていました。
その経緯から、HTTP 通信を実装する場合は、jQuery や axios などのライブラリを使用して実装するのが一般的となっていました。
なお、これらの HTTP 通信は非同期で行うことから非同期通信(Ajax 通信)とも呼ばれています。
Fetch API は、上記のライブラリと遜色なくシンプルな実装を行うことができます。
当初は、Fetch API に対応していないブラウザなどもありましたが、現在ではほとんどのブラウザで使用することができるようになっています。
今後は、ライブラリ無しで使用できる Fetch API の利用が増えていくことが予想されます。
Fetch API を使用して、サーバからデータを取得する基本構文は次のようになります。
const response = await fetch(対象のURL);
const data = await response.json()
上記の構文通りに指定をすれば、取得したいレスポンスデータが data
変数に格納されるということだけ理解していれば大丈夫です。
詳しく理解したい方のため、以下、簡単に説明をしておきます。
・fetch() 関数
1 行目の fetch()
関数で、Fetch API を呼び出しています。
fetch(対象のURL)
というように引数に URL を指定すると、GET メソッドでデータを取得します。この関数は非同期で実行されるため await
を付ける必要があります。
戻り値を受け取る response
には、HTTP レスポンス全体を含む Response オブジェクト が取得されます。ここには、レスポンスのヘッダー情報やその成否など様々な情報が含まれていますが、このままでは、実際に使用したい JSON データは抽出できません。
・json() メソッド
次に構文の 2 行目です。1 行目で取得した Response オブジェクトから JSON 本体の内容を抽出するには json() メソッドを使用する必要があります。
このメソッドの戻り値は、JSON 形式のデータを JavaScript のオブジェクト形式に変換したものとなります。
この json() メソッドも、非同期処理で実行されるため、await
を付ける必要があります。
5. 地域コード一覧取得 API の詳細
Chapter 1 でも紹介しましたが、地域コードを取得する API は、次の URL を使用します。
API 名 | url |
---|---|
地域コード一覧取得API | https://www.jma.go.jp/bosai/common/const/area.json |
この API から返されるデータを使用して、アプリケーションを実装していくことになりますが、そのためには、返却されたデータの中身を正しく把握しておく必要があります。
上記の URL をクリックして開き、API の返却するレスポンスデータを確認してみましょう。
次のように、レスポンスとして、JSON 形式の文字列がズラッと表示されます。しかし、このような平坦な文字列から内容を読み解くのは大変です。
内容を読み解く方法の 1 つとして、Chrome の開発者用画面(デベロッパーツール)を使用する方法があります。
ここでは、その方法を使用してレスポンスデータの中身を確認していきます。
URL からブラウザが開いたら、次の順で操作をしてください。
① Chrome の開発者用画面(デベロッパーツール)を開く(右クリック →「検証」選択)
② 開発者用画面の「Network」タブをクリックする
③ ブラウザをリロードして、再度 API からデータを読み込む
④ 開発者用画面の下半分のところにある area.json
をクリックする
area.json
はレスポンスデータ本体であり、これをクリックすることでその中身を確認することができます。
「Headers」タブを選択すると、行われた HTTP 通信の基本情報が確認できます(下図)。
赤枠の部分に、指定された URL と HTTP メソッドが表示されています。
次に「Preview」タブをクリックしてみてください。
JSON 文字列の内容が、整理されて人の目でも分かりやすいように表示されます(下図)。
ここに、様々な地名と、それに対応するコードが定義されています。
今回使用するのは、以下の offices
プロパティで定義されているデータとなります。
上の図のように、▶
をクリックして中身を展開してみましょう。
列挙されたプロパティキー(青枠)部分に「地域コード」、その値の中の name
プロパティに「地域名」が記載されていることが確認できます(※)。
レスポンスデータの解析
ここでは、データの解析の方法については省略しています(本題ではないため)。
一般に、外部から提供されている API については、公開されたドキュメントがありますので、それを読み解きながら実装していきます。
また、自前で API を作る場合は、その API 設計書などを確認しながら、実装を行うことになります。
結果として、レスポンスデータのうち、必要な情報の構造は、以下のとおりとなります。
{
...,
offices: {
地域コード1: { name: 地域名1, ...},
地域コード2: { name: 地域名2, ...},
地域コード3: { name: 地域名3, ...},
...
}
}
上記の offices
の内容が、Stateの area
プロパティの中に格納されることになります(下記コード参照)。
State の定義(再掲)
state: () => {
return {
area: {
offices: {}
},
code: '130000'
}
},
以上、地域コードを取得/格納する area ストアの実装とコードの確認を行いました。
慣れない方には、難しいことが多いかもしれません。全てを理解できなくても、コードを書きながらイメージを掴みつつ、進めていきましょう。
作業を繰り返すうちに、少しずつ理解が深まっていくようになります。
6. forecast ストアの作成
もう 1 つの「天気予報情報を取得する forecast ストア」も作成しておきます。
コードは、Lesson 8 の「Chapter 6 - 2. Actions で非同期通信を行う」で作成した weather.js
ストアの名前等を変更してそのまま使用します。
area.js
ストアと同じディレクトリ(src/stores
ディレクトリ)に、forecast.js
ファイルを作成してください。
以下の青枠部分が、Lesson 8 で作成した Store から変更しているところです。
全てのコードをテキストで表示
src\stores\forecast.js
import { defineStore } from 'pinia'
export const useForecastStore = defineStore('forecast', {
state: () => {
return {
overview: {
headlineText: '',
publishingOffice: '',
reportDatetime: '',
targetArea: '',
text: '',
}
}
},
actions: {
async fetchOverview() {
const url = 'https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json'
const response = await fetch(url);
this.overview = await response.json();
}
}
})
fetchOverview()
アクションでは、次の天気予報概況の API からデータを取得します。
API 名 | url |
---|---|
天気予報概況取得API | https://www.jma.go.jp/bosai/forecast/data/overview_forecast/地域コード.json |
この forecast.js
ストアは、一旦、ここまでの仮実装としておきます。
以降のチャプターで、順次、State や Actions を追加していきます。

Lesson 9
Chapter 5
コンポーネントとストアの接続
このチャプターでは、ストアとコンポーネント間の通信の設定を行っていきます。
1. 地域一覧の表示
まず、前チャプターで作成した area ストアを使用して、地域一覧のメニューを作っていきましょう。
手始めに、単純にストアから地域一覧データを取得して、左側のメニュー(ドロワー)に表示するだけの仮実装を行います。
1-1. App.vue コンポーネントの仮実装
src\App.vue
ファイルを開いて、下図の赤枠部分を追加します。
なお、下図の 15 行目から 32 行目までは、折り畳んで省略していますので、行番号を見ながら修正部分を確認してください。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useAreaStore } from './stores/area'
const areaStore = useAreaStore()
areaStore.fetchArea()
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
<div v-for="(office, code) in areaStore.area.offices" :key="code">
{{ code }}: {{ office.name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped></style>
① script の追加部分
script の追加部分は次のとおりです。
import { useAreaStore } from './stores/area'
const areaStore = useAreaStore()
areaStore.fetchArea()
area ストアのインスタンスを取得して、fetchArea()
メソッドを実行しています。
これで、App.vue
インスタンス生成時に、fetchArea()
が実行され、area ストアに地域一覧のデータが取得/格納されることになります。
② template の追加部分
次に、template の追加部分を見てみましょう。
これは、取得したデータを表示するだけの仮実装です。
<div v-for="(office, code) in areaStore.area.offices" :key="code">
{{ code }}: {{ office.name }}
</div>
area ステートの offices オブジェクトに格納されているデータを v-for ディレクトリを使用して表示しています。
(office, code)
の部分のうち、code
には、offices オブジェクトの各プロパティのキー(すなわち地域コード)が取得され、office
にはキーに対応する値(これもオブジェクト)が取得されます。
前チャプターで確認したとおり、office
オブジェクトの name
プロパティから地域名を取得することができます。
③ ブラウザで動作確認
正しくデータが取得できるかどうか、ブラウザを開いて確認してみましょう。
コードの記述に誤りが無ければ、次のように地域コードが 100000
の群馬県から順番に表示されます。
無事に、API からデータが取得され、一見これで問題ないように見えます。
ところが、一覧の部分を下の方までスクロールしていくと、474000
の八重山地方の次に、011000
の宗谷地方が続いています。
プロパティのキー順に取り出せない理由は何でしょうか。
これは、JavaScript のオブジェクトをループ処理で取り出す場合は(一般的に)その順番は保証されないという仕様によるものです。
今回は、v-for ディレクティブでオブジェクトを取り出しましたが、地域コード(キー)の順番には取り出せないという結果となりました。
「順番はあきらめる」という選択肢もありますが、ここは、ちゃんと地域コード順に並ぶようにソースコードを修正していきましょう。
1-2. 取得した地域コードを並べ替える
先ほど仮実装をした src\App.vue
ファイルを修正していきます。
次の赤枠部分のとおりに追加/修正を行ってください。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref, computed } from 'vue'
import { useAreaStore } from './stores/area'
const areaStore = useAreaStore()
areaStore.fetchArea()
const offices = computed(() => {
return new Map(
Object.entries(areaStore.area.offices)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([key, value]) => [key, value.name])
)
})
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
<div v-for="[code, name] in offices" :key="code">
{{ code }}: {{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped></style>
① script の追加部分
script に次のような算出プロパティを追加しました。
areaStore.area.offices
オブジェクトに変化があるたびに、それを加工した Map オブジェクトを返すというものです(変化するのは初期値取得時と API 取得時の 2 回のみです)。
const offices = computed(() => {
return new Map(
Object.entries(areaStore.area.offices)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([key, value]) => [key, value.name])
)
})
処理している内容を簡単に説明します。
まず、Object.entries()
メソッドによりオブジェクトを配列形式に変換します。
Object.entries() メソッドの使用方法
Object.entries()
メソッドについて、簡単に使用方法を説明しておきます。
わかる方は、ここは読まなくとも大丈夫です。
次のように、引数に変換対象オブジェクトを指定して、戻り値として変換後の配列を取得します。
const obj = { foo: 'bar', baz: 42 };
console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]
つまり、次のような JavaScript オブジェクトを
変換前のオブジェクト
const obj = {
foo: 'bar',
baz: 42
}
次のような 2 次元配列の形式に変換します。
変換後の配列
const array = [
['foo', 'bar'],
['baz', 42]
]
変換後の配列では、1 つ目の要素のキー foo
は array[0][0]
で取得でき、その要素のプロパティ bar
は array[0][1]
で取得できることになります。
次に、sort()
メソッドで、キーの昇順に並べ替えています。
sort() メソッドの使用方法
sort()
メソッドについても、簡単に使用方法を説明しておきます。
アロー関数を使用した sort()
メソッドの構文は次のようになります。
sort((a, b) => { /* 値を返却する */ } )
引数 a
には比較する第 1 要素、b
には比較する第 2 要素が順に入っていきます。
このアロー関数の戻り値が正か負かにより、並び順が決まっていきます。
戻り値 | ソート順 |
---|---|
正( 戻り値 > 0 ) |
a を b の後に並べる |
負( 戻り値 ) |
a を b の前に並べる |
ゼロ( 戻り値 === 0 ) |
a を b の元の順序を維持する |
単純に数値の昇順で並べ替える場合は、次のように指定すれば良いことになります。
sort((a, b) => a - b )
最後に、map()
メソッドを使用して、Map オブジェクトの key
に元のオブジェクトのプロパティキーを、Map オブジェクトの value
に元のオブジェクトの値の name プロパティを充てています。
このあたりは、一般的な JavaScript の知識ですので、不明なことはご自身で調べながら内容を理解していただければと思います。
② template の修正部分
続いて、template の修正部分です。
これは、最初に書いた内容を全修正しています。
<div v-for="[code, name] in offices" :key="code">
{{ code }}: {{ name }}
</div>
これは、Map オブジェクトを v-for ディレクトリを使用してループしているものです(※)。
[code, name]
のところで、Map の key と value を 1 つずつ取り出しています。
Map オブジェクトを v-for で使用する方法
Map オブジェクトを v-for でループする場合の基本構文は次のとおりです。
<div v-for="[key, value] in Mapオブジェクト" :key="key">
{{ key }}: {{ value }}
</div>
上記の形で、Map オブジェクトから key と value を取り出すことができます。
なお、オブジェクトや配列の場合と異なり、[key, value]
というように角括弧を使用することに注意してください。
③ ブラウザで確認する
それでは、ブラウザで確認してみましょう。
次のように宗谷地方(011000)から順に並んでいれば OK です。
1-3. 地域コードの並べ替えデータをストアの Getters から取得する
先ほど App.vue
コンポーネントに実装した地域コードの Map オブジェクトは、このままでは他のコンポーネントから使用できません。
この Map オブジェクトに、他のコンポーネントからもアクセスできるようにするため、area ストアの Getters として定義する形に変更します。
① area.js ストアの修正
src\stores\area.js
ストアに、以下のように offices
ゲッターを追加します。
全てのコードをテキストで表示
src\stores\area.js
import { defineStore } from 'pinia'
export const useAreaStore = defineStore('area', {
state: () => {
return {
area: {
offices: {}
},
code: '130000'
}
},
getters: {
offices: (state) => {
return new Map(
Object.entries(state.area.offices)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([key, value]) => [key, value.name])
)
}
},
actions: {
async fetchArea() {
const url = 'https://www.jma.go.jp/bosai/common/const/area.json'
const response = await fetch(url)
this.area = await response.json()
}
}
})
追加した offices
ゲッターは次のとおりです。
offices: (state) => {
return new Map(
Object.entries(state.area.offices)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([key, value]) => [key, value.name])
)
}
先に App.vue
に追加した処理と基本的に同じものです。
異なるのは、area.offices
のデータを、引数 state
から取得している点のみです。
② App.vue コンポーネントの修正
src\App.vue
コンポーネントで、area
ストアの offices
ゲッターを使用するように修正します。
基本的に、以下の赤枠部分を追加するのみで OK です。
なお、算出プロパティ offices
は不要となるので削除しておきます(緑枠部分)。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useAreaStore } from './stores/area'
const areaStore = useAreaStore()
areaStore.fetchArea()
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
<div v-for="[code, name] in areaStore.offices" :key="code">
{{ code }}: {{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped></style>
ゲッターの追加については、以上のとおりです。
1-4. ページ遷移のリンク追加
左メニューの地域をクリックした際にページ遷移できるようにリンクを追加します。
① App.vue コンポーネントの修正
src\App.vue
コンポーネントに以下の赤枠部分を追加して、地域名をクリックしたときに「天気概況」ページに遷移するようにします。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const movePage = (code) => {
router.push({ name: 'overview', params: { code: code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<!-- drawer content -->
<div v-for="[code, name] in areaStore.offices" :key="code" @click="movePage(code)">
{{ code }}: {{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style scoped></style>
ページ遷移は useRouter
を使用して、次のメソッドで定義しています。
名前付きルートにパラメータ code
を渡す形で指定しています(Lesson 7 Chapter 5 参照)。
const movePage = (code) => {
router.push({ name: 'overview', params: { code: code } })
}
params: { code: code }
の部分は params: { code }
と省略形で書いても OK です。
次の @click="movePage(code)"
で、クリック時にページ遷移のメソッドを実行するようにしています。
<div v-for="[code, name] in areaStore.offices" :key="code" @click="movePage(code)">
{{ code }}: {{ name }}
</div>
② ブラウザで確認する
それでは、ページ遷移ができるかブラウザで確認してみましょう。
例えば「青森県」をクリックすると、次のように天気概況ページに遷移します。
なお、URL のパスも /overview/020000
というように、正しく遷移しているかも確認しておいてください。
1-5. 左メニューの見た目の修正
左メニューについて、クリックできるような見た目になっていないため、簡単な CSS を追加しておきます。
① App.vue コンポーネントの修正
src\App.vue
コンポーネントの template と style につき、次の赤枠部分のように追加・修正を行います。
template 部分は、div
タグを折り返したため大きく変わっているように見えますが、class 属性を追加し、{{ code }}
を非表示としただけです。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const movePage = (code) => {
router.push({ name: 'overview', params: { code: code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab to="/overview/1" label="天気概況" />
<q-route-tab to="/week/1" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
style については、次のように SCSS を使用して記述しています(本アプリでは、Quasar のインストール時に SCSS もインストールされています)。
SCSS を使用する場合は、style
タグ内に lang="scss"
と記述する必要があります。
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
なお、SCSS を使用せず、一般的な CSS で記述すると次のようになります。
(参考)SCSS を使用しない場合
<style scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
}
.area-item:hover {
background-color: #EEEEEE;
}
</style>
② ブラウザで確認する
ブラウザ上で、地域名にカーソルを合わせると、背景が薄いグレーになります。
また、マウスカーソルもリンクポインタ(人の手の形)に変わることが確認できると思います。
Quasar で指定する例
参考までに、Quasar に用意されたタブを使用して、次のように指定することもできます。
このようにすれば、CSS(style)の指定は不要となります。
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<q-list dense>
<q-item
v-for="[code, name] in areaStore.offices"
clickable tag="a"
@click="movePage(code)"
>
<q-item-section>
<q-item-label>{{ name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-drawer>
本レッスンでは、Quasar については深入りして解説はしませんが、興味がありましたら公式ページなどを見つつ、ご自身で実装をしてみてください。
2. 天気概況の表示
次に、forecast ストアを使用して、天気概況の表示を行います。
まずは、地域ごとの切り替えは行わず、単純に Store で取得したデータを画面に表示するまでを行います。
2-1. Overview.vue コンポーネントの仮実装
src\views\Overview.vue
ファイルを開いて、下図のように全面的に修正を行います。
全てのコードをテキストで表示
src\views\Overview.vue
<script setup>
import { useForecastStore } from '../stores/forecast'
const forecastStore = useForecastStore()
forecastStore.fetchOverview()
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ forecastStore.overview.targetArea }} 天気概況</h4>
<table>
<tr><th>発表日時</th><td>{{ forecastStore.overview.reportDatetime }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
tr {
vertical-align: top;
text-align: left;
white-space: pre-wrap;
th {
min-width: 80px;
padding: 8px;
}
td {
padding: 8px;
}
}
</style>
① script の追加部分
script 部分は、次のように script setup 構文で指定しています。
<script setup>
import { useForecastStore } from '../stores/forecast'
const forecastStore = useForecastStore()
forecastStore.fetchOverview()
</script>
これで、Overview.vue
コンポーネントの表示時に、forecast ストアの fetchOverview()
メソッドが実行され forecast ストアに天気概況のデータが取得/格納されることになります。
なお、forecast ストアの fetchOverview()
メソッドは、現在は、次のように、地域コードが 130000
(東京)で固定されています。
src\stores\forecast.js(抜粋)
async fetchOverview() {
const url = 'https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json'
const response = await fetch(url);
this.overview = await response.json();
}
② template の追加部分
template 部分は次のように全部書き換えを行っています。
コードは、Lesson 8 の「Chapter 6 - 2. Actions で非同期通信を行う」で作成した
About.vue
コンポーネントと同様です。
<template>
<div class="wrap">
<h4 class="heading">{{ forecastStore.overview.targetArea }} 天気概況</h4>
<table>
<tr><th>発表日時</th><td>{{ forecastStore.overview.reportDatetime }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</div>
</template>
API の実行により取得した天気概況データ(overview
ステート)のうち、
targetArea
プロパティ、reportDatetime
プロパティ、text
プロパティから、
場所
、発表日時
、天気概況
のデータを取得して表示しています。
③ style の追加部分
style 部分は次のように lang="scss"
を指定して SCSS で記述しています。
こちらは、単なる体裁の調整です。
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
tr {
vertical-align: top;
text-align: left;
white-space: pre-wrap;
th {
min-width: 80px;
padding: 8px;
}
td {
padding: 8px;
}
}
</style>
なお、.wrap
クラスには、スクロールバーを設定しています。
height: calc(100vh - 98px);
は、「画面全体の高さ - ヘッダー部分の高さ」という計算ですが、ヘッダー部分の高さ 98px
は現在の Quasar のデフォルトテンプレートの値のため、時期により異なる場合もあると思われます。
スクロールが上手く動作しなければ、ご自身で高さを適宜調整してください。
④ ブラウザで動作確認
正しくデータが取得・表示できるかどうか、ブラウザを開いて確認してみましょう。
どこでも良いのですが、左メニューの「宗谷地方」をクリックしてみます。
すると、東京都の天気概況が画面に表示されるはずです。
当然ですが、どこの地域名をクリックしても、東京の天気概況が表示されます。
次の項で、コードを修正して、地域ごとの天気概況を表示できるようにしていきます。
2-2. 地域ごとの天気概況を表示する
続いて、地域ごとの天気概況を表示できるようにコードの修正をしていきます。
① forecast ストアの修正
src\stores\forecast.js
ファイルを開いて、下図の赤枠部分を修正します。
全てのコードをテキストで表示
src\stores\forecast.js
import { defineStore } from 'pinia'
import { useAreaStore } from './area'
export const useForecastStore = defineStore('forecast', {
state: () => {
return {
overview: {
headlineText: '',
publishingOffice: '',
reportDatetime: '',
targetArea: '',
text: '',
}
}
},
actions: {
async fetchOverview(code) {
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`
try {
const response = await fetch(url);
this.overview = await response.json();
} catch {
this.$reset()
}
}
}
})
修正後の fetchOverview()
アクションは、次のとおりです。
async fetchOverview(code) {
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`
try {
const response = await fetch(url);
this.overview = await response.json();
} catch {
this.$reset()
}
}
地域コードを指定するために、fetchOverview()
アクションに引数 code
を追加して、URL に埋め込むようにしています。
また、useAreaStore
から、area ストアを呼び出して、その code ステートに、引数で受けた code
の値をセットします。これで、現在選択中の地域コードを保持/管理します。
最後に、指定した地域コードのデータが API で取得できなかった場合のために、try - catch 構文でエラー処理を追加しておきます。
② Overview.vue コンポーネントの修正
src\views\Overview.vue
ファイルを開いて、下図の赤枠部分を追加します。
全てのコードをテキストで表示
src\views\Overview.vue
<script setup>
import { useRoute } from 'vue-router'
import { useForecastStore } from '../stores/forecast'
import { onBeforeRouteUpdate } from 'vue-router'
const route = useRoute()
const forecastStore = useForecastStore()
forecastStore.fetchOverview(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchOverview(to.params.code)
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ forecastStore.overview.targetArea }} 天気概況</h4>
<table>
<tr><th>発表日時</th><td>{{ forecastStore.overview.reportDatetime }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
tr {
vertical-align: top;
text-align: left;
white-space: pre-wrap;
th {
min-width: 80px;
padding: 8px;
}
td {
padding: 8px;
}
}
</style>
修正後のコードでは、forecast ストアから、fetchOverview()
アクションを呼び出す記述を 2 箇所に行っています。
1 つ目の呼出しは、script 内の直下に記述しています(下記)。
こちらは、Overview コンポーネントのインスタンス生成時に呼び出されます。
forecastStore.fetchOverview(route.params.code)
2 つ目の呼出しは、onBeforeRouteUpdate
フックを使用してに記述しています(下記)。
こちらは、同一コンポーネントでページ遷移する時に呼び出されます(Lesson 7 の Chapter 4 参照)。
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchOverview(to.params.code)
})
この 2 つ目の呼出しを行わないと、同一コンポーネント間でのページ遷移時に画面のデータが切り替わらないことになります。
③ ブラウザで動作確認
それでは、ブラウザで開いて動作確認をしてみましょう。
Chrome の開発者用画面も開いて Console タブを選択しておきます。
ブラウザを開いたら「宗谷地方」をクリックしてみます。
次のような画面が表示されれば OK です。
続いて「上川・留萌地方」「網走・北見・紋別地方」「十勝地方」と順にクリックしてみます。すると「十勝地方」をクリックした時点で、次のようにエラーが発生します(2023 年 2 月時点)。
なお、見出しも「十勝地方 天気概況」とは表示されず単に「天気概況」となっています。
先に結論を言いますと、気象庁の API では「十勝地方(地域コード: 014030)」のデータが提供されていないためです。
なお、十勝地方の情報は「釧路・根室地方」に統合されています(下図)。
このあたりは、気象庁側の運用なので、利用する側ではそれに従うしかありません。
このように、データが取得できない場合についても、想定した実装を行う必要があります。
同様のエラーが出るのは、「奄美地方(地域コード: 460040)」の場合、ヘッダーの「天気概況」タブをクリックした場合がありますので、そちらも試しておいてください。
コードの修正は、次の項で行っていきます。
2-3. データを取得できない場合の処理を追加する
それでは、API からデータを取得できなかった場合の処理を追加していきます。
データが取得できなかった場合の実装方法として、次のようなものが考えられます。
- ⑴ データを取得できない地域はリストから外してしまう
- ⑵ データが取得できなかった旨を画面に表示する
気象庁側の運用により、地域コードの一覧が変更されたり、データを取得できない地域が変更になる場合も考えられます。
そうすると、⑴ の方法では汎用性に欠けてしまうことになります。
ここでは、できるだけ汎用的に対応できるように、⑵ の「データが取得できなかった旨を画面に表示する」方法を採用するようにします。
① area ストアの修正(地域名を取得するゲッターを追加)
API からのデータ取得失敗時にも地域名の表示ができるように area ストアに地域名を取得するゲッターを追加します。
src\stores\area.js
ファイルを開いて、getters 内に、下図の赤枠部分を追加します。
全てのコードをテキストで表示
src\stores\area.js
import { defineStore } from 'pinia'
export const useAreaStore = defineStore('area', {
state: () => {
return {
area: {
offices: {}
},
code: '130000'
}
},
getters: {
offices: (state) => {
return new Map(
Object.entries(state.area.offices)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([key, value]) => [key, value.name])
)
},
officeName(state) {
return this.offices.get(state.code)
}
},
actions: {
async fetchArea() {
const url = 'https://www.jma.go.jp/bosai/common/const/area.json'
const response = await fetch(url)
this.area = await response.json()
}
}
})
追加した officeName
ゲッターは次のとおりです。
このゲッターは、state 内の code
プロパティに対応する地域名を取得するものとなります。
officeName(state) {
return this.offices.get(state.code)
}
上記では、同じストア内の offices
ゲッターを this
を使用して呼び出しています。
この offices
は Map オブジェクトなので get メソッドを使用してキー(state.code
)からその値である地域名の取得ができます。
なお、this
を使う場合は、アロー関数式は使用できないので、一般的な関数の形式で定義しています。
② Overview.vue コンポーネントの修正
src\views\Overview.vue
ファイルを開いて、下図の赤枠部分を追加します。
全てのコードをテキストで表示
src\views\Overview.vue
<script setup>
import { useRoute } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { onBeforeRouteUpdate } from 'vue-router'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
forecastStore.fetchOverview(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchOverview(to.params.code)
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ forecastStore.overview.targetArea }} 天気概況</h4>
<h4 class="heading">{{ areaStore.officeName }} 天気概況</h4>
<template v-if="forecastStore.overview.reportDatetime">
<table>
<tr><th>発表日時</th><td>{{ forecastStore.overview.reportDatetime }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</template>
<template v-else>
<p>{{ areaStore.officeName }} の天気概況を取得できませんでした。</p>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
tr {
vertical-align: top;
text-align: left;
white-space: pre-wrap;
th {
min-width: 80px;
padding: 8px;
}
td {
padding: 8px;
}
}
</style>
見出し部分は、暫定的に 2 つのパターンを並列で表示させています(後で確認するため)。
先に表示されるのが「指定地域の天気予報概況を取得する API」から得た地域名、次に表示されるのが「地域コード一覧を取得する API」から得た地域名となります。
<h4 class="heading">{{ forecastStore.overview.targetArea }} 天気概況</h4>
<h4 class="heading">{{ areaStore.officeName }} 天気概況</h4>
次の部分で、v-if および v-else ディレクティブを使用して、データ取得の成否で表示内容の出し分けを行っています。
<template v-if="forecastStore.overview.reportDatetime">
<table>
<tr><th>発表日時</th><td>{{ forecastStore.overview.reportDatetime }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</template>
<template v-else>
<p>{{ areaStore.officeName }} の天気概況を取得できませんでした。</p>
</template>
v-if で判定する条件式には発表日時(forecastStore.overview.reportDatetime
)を指定し、このデータが無ければデータ取得失敗と判定しています(判定できるプロパティであれば何でも構いません)。
なお、template タグは HTML には描画されないため、v-if と v-else で表示/非表示の条件分岐を指定する際などに使用すると便利です。
参考までに、v-if と v-else は、次のように table
タグと p
タグに直接指定しても OK です。
参考(template を使用しない場合)
<table v-if="forecastStore.overview.reportDatetime">
<tr><th>発表日時</th><td>{{ forecastStore.overview.reportDatetime }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
<p v-else>{{ areaStore.officeName }} の天気概況を取得できませんでした。</p>
③ App.vue コンポーネントの修正
src\App.vue
ファイルを開いて、下図の赤枠部分を修正します。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const movePage = (code) => {
router.push({ name: 'overview', params: { code: code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
vue-router のリンク先のパラメータに areaStore.code
を指定したのみです。
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
④ ブラウザで動作確認
ブラウザで開いて動作確認を行います。
まず、エラーの発生していた「十勝地方」をクリックしてください。
次のように「十勝地方 天気概況」というように見出しが表示されるとともに、「十勝地方 の天気概況を取得できませんでした。」というメッセージも表示されています。
これだけを見ると緑枠の「天気概況」の部分を消せば良さそうです。
続いて「釧路・根室地方」をクリックしてみましょう。
すると、2 つの見出しで異なる内容が表示されました。
緑枠の「釧路・根室・十勝地方」は、天気概況のデータとともに取得した地域名で、表示内容とマッチしています。
赤枠部分の「釧路・根室地方」は、地域コード一覧から取得したデータですが、表示内容と齟齬が出てしまっています。
上記の矛盾につきまして、完全な対応は難しいですが、なるべく正しいデータを表示できるようにしていきたいところです。
様々な補正方法があると思いますが、ここでは次の方針で修正を行っていきます。
- 天気概況データが取得できた場合は、そのデータに含まれる地域名を表示する
- 天気概況データが取得できない場合は、area ストア内のデータから地域名を表示する
2-4. コードの補正
それでは、先ほど確認を行った「地域名」の表示について補正を行っていきます。
あわせて、発表日時の表示が「発表日時 2023-02-07T10:36:00+09:00」となっているものを「発表日時 2023年2月7日(火) 10時36分」という表示に直すようにします。
① Overview.vue コンポーネントの修正
src\views\Overview.vue
ファイルを開いて、下図の赤枠部分を追加・修正します。
全てのコードをテキストで表示
src\views\Overview.vue
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { onBeforeRouteUpdate } from 'vue-router'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
/** 地域名 */
const officeName = computed(() => {
if (forecastStore.overview.targetArea !== '') {
return forecastStore.overview.targetArea
} else {
return areaStore.officeName
}
})
const weekDays = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
/** 発表日時 */
const reportDateTimeDisplay = computed(() => {
if (!forecastStore.overview.reportDatetime) return '' // 日時が取得できない場合は return
const d = new Date(forecastStore.overview.reportDatetime) // 日時を Date 型に変換
const date = `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日` // 年月日
const weekDay = weekDays[d.getDay()] // 曜日
const time = `${d.getHours()}時${d.getMinutes()}分` // 時間
return `${date}(${weekDay}) ${time}` // 年月日・曜日・時間を連結して返却
})
forecastStore.fetchOverview(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchOverview(to.params.code)
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ officeName }} 天気概況</h4>
<template v-if="forecastStore.overview.reportDatetime">
<table>
<tr><th>発表日時</th><td>{{ reportDateTimeDisplay }}</td></tr>
<tr><th>天気概況</th><td>{{ forecastStore.overview.text }}</td></tr>
</table>
</template>
<template v-else>
<p>{{ officeName }} の天気概況を取得できませんでした。</p>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
tr {
vertical-align: top;
text-align: left;
white-space: pre-wrap;
th {
min-width: 80px;
padding: 8px;
}
td {
padding: 8px;
}
}
</style>
以下、コードの内容を見ていきます。
②「地域名」の表示
まず、「地域名」は、以下の算出プロパティ(computed)で取得するようにしています。
/** 地域名 */
const officeName = computed(() => {
if (forecastStore.overview.targetArea !== '') {
return forecastStore.overview.targetArea
} else {
return areaStore.officeName
}
})
単純なコードですが、「天気予報概況を取得する API」から地域名が取得できればそれを表示し、取得できなければ area ストアの地域名を表示するようにしています。
なお、条件分岐の式である if (forecastStore.overview.targetArea !== '')
のところは、if (!forecastStore.overview.targetArea)
と書くこともできます。
template 側では、次のように指定して表示しています。
<h4 class="heading">{{ officeName }} 天気概況</h4>
<p>{{ officeName }} の天気概況を取得できませんでした。</p>
③「発表日時」の表示
次に、「発表日時」については、以下の算出プロパティ reportDateTimeDisplay
を作成しています。
const weekDays = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
/** 発表日時 */
const reportDateTimeDisplay = computed(() => {
if (!forecastStore.overview.reportDatetime) return '' // 日時が取得できない場合は return
const d = new Date(forecastStore.overview.reportDatetime) // 日時を Date 型に変換
const date = `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日` // 年月日
const weekDay = weekDays[d.getDay()] // 曜日
const time = `${d.getHours()}時${d.getMinutes()}分` // 時間
return `${date}(${weekDay}) ${time}` // 年月日・曜日・時間を連結して返却
})
単に「2023-02-07T10:36:00+09:00」の文字列を「2023年2月7日(火) 10時36分」と変換するだけなので、実装者によって記述の仕方は異なると思います。
上記は、一例として捉えていただければと思います。
template 側では、次のように指定して表示しています。
<tr><th>発表日時</th><td>{{ reportDateTimeDisplay }}</td></tr>
④ ブラウザで動作確認
ブラウザで開いて動作確認を行いましょう。
ここでも、まずは「十勝地方」をクリックします。意図した形で表示されていることが確認できると思います。
次に「釧路・根室地方」をクリックします。
こちらでは「天気概況を取得する API」で取得した地域名が表示されています(下図)。
また「発表日時」も、正しく日本語の表示になりました。
本チャプターは以上です。
次のチャプターから、週間天気予報の表示を実装していきます。

Lesson 9
Chapter 6
詳細ページの実装
このチャプターでは、週間天気予報ページに関する実装を行います。
技術的なことよりも、ロジックを考えながら実装を行う側面が強くなりますので、頭の体操がてら挑戦をしてみてください。
コードをコピペすれば動くようになっていますので、全てを理解しなくとも大丈夫です。
それでは始めていきましょう。
1. 週間天気予報取得 API で取得できるデータの確認
週間天気予報を取得するために、次の API を使用します。
API 名 | url |
---|---|
指定地域の週間天気予報取得 API | https://www.jma.go.jp/bosai/forecast/data/forecast/地域コード.json |
1-1. API で使用されている用語について
先に、気象庁の API で使用されている用語について、確認をしておきます。
以下の用語が分かっているとレスポンスデータの内容が、より分かりやすくなります。
No | 用語 | 説明 |
---|---|---|
1 | pop | probability of precipitation(降水確率)を略したもの |
2 | temp | temperature(温度)を略したもの |
3 | srf | short-range forecast(短期予報)を略したもの |
4 | reliability | reliability(信頼性)は天気予報の精度(確度が高い順に A,B,C で表す) |
1-2. JSON の構造確認
この API から返されるデータを確認してみましょう。
開発者用画面の Preview タブで確認すると、以下のようなデータが取得できています。
大きく 2 つの配列に分かれており、インデックス 0
が「短期予報」、インデックス 1
が「週間予報」となっています。
本アプリケーションで使用するデータは下表のとおりです。
①から⑫の数字は上図の番号に対応しています(⑫は図中では隠れています)。
区分 | No | プロパティ名 | 備考 |
---|---|---|---|
短期予報 | ① |
[0]timeSeries[0].areas[].area.name
|
地域名 |
② |
[0]timeSeries[0].areas[].area.code
|
地域コード | |
③ |
[0]timeSeries[0].areas[].weatherCodes[]
|
天気コード(3日分) | |
④ |
[0]timeSeries[0].timeDefines[]
|
年月日(3日分) | |
⑤ |
[0]timeSeries[1].areas[].pops[]
|
降水確率(2日分) | |
⑥ |
[0]timeSeries[2].areas[].temps[]
|
温度(2日分) | |
週間予報 | ⑦ |
[1]timeSeries[0].areas[].area.name
|
地域名 |
⑧ |
[1]timeSeries[0].areas[].area.code
|
地域コード | |
⑨ |
[1]timeSeries[0].areas[].pops[]
|
降水確率(7日分) | |
⑩ |
[1]timeSeries[0].areas[].reliabilities[]
|
信頼度(7日分) | |
⑪ |
[1]timeSeries[0].areas[].weatherCodes[]
|
天気コード(7日分) | |
⑫ |
[1]timeSeries[0].timeDefines[]
|
年月日(7日分) | |
⑬ |
[1]timeSeries[1].areas[].tempsMin[]
|
最低温度(7日分) | |
⑭ |
[1]timeSeries[1].areas[].tempsMax[]
|
最高温度(7日分) |
どのプロパティが、どのデータを表しているかは、実際の気象庁の東京都の天気予報ページなどを見ながら確認しています。
なお、短期予報には、本日から 3 日分のデータが格納されていますが、3 日目(明後日)のデータには気温や降水量が含まれていません。
また、週間予報には、明日以降の 7 日分のデータが格納されていますが、明日分のデータには気温や降水量が含まれていません(ややこしいですね)。
結果、データ内容を表にすると次のようになり、本日と明日のデータは短期予報のものを使用して、3 日目以降のデータは週間予報のものを使用するという建付けになります。
区分 | 保有データ | 説明 |
---|---|---|
短期予報 | 今日・明日・明後日の 3 日間 | 全データが揃っているのは今日・明日の 2 日分 |
週間予報 | 明日から 1 週間分のデータ | 全データが揃っているのは明後日から 6 日分 |
なお、短期予報のエリア数と、長期予報のエリア数が異なる地域も存在します。
このあたりは、色々とややこしいのですが、後ほど、一つずつ解決をしていきます。
週間天気予報取得 API で取得できる内容は、ざっと以上のようになっています。
興味のある方は、Web ページの表示内容と JSON のデータを比較しつつ確かめてみてください(お勧めはしません)。
1-3. 天気コードの確認
ここで「天気コード」なるものが出てきます。
例えば、weatherCodes: ["114", "210", "101"]
のような感じで 114
とか 210
とか 101
という数字で表されています。
これらの番号に、どのような天気情報が紐づくのかということは、東京都の天気予報ページや全国の天気予報ページなどを開発者用画面で開くことで確認ができます。
上記の青枠のところの TEROPS
というプロパティ内に、天気コードと対応する天気情報が表示されています。
例えば、101
は「晴時々曇」、114
は「晴後雨」というのが見て取れます。
この TEROPS
をコピペして成形すると、次のようなオブジェクトになっています。
TELOPS: {
100: ["100.svg","500.svg","100","晴","CLEAR"],
101: ["101.svg","501.svg","100","晴時々曇","PARTLY CLOUDY"],
102: ["102.svg","502.svg","300","晴一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS"],
103: ["102.svg","502.svg","300","晴時々雨","CLEAR, FREQUENT SCATTERED SHOWERS"],
104: ["104.svg","504.svg","400","晴一時雪","CLEAR, SNOW FLURRIES"],
105: ["104.svg","504.svg","400","晴時々雪","CLEAR, FREQUENT SNOW FLURRIES"],
// (中略)
426: ["400.svg","400.svg","400","雪後みぞれ","SNOW, SLEET LATER"],
427: ["400.svg","400.svg","400","雪一時みぞれ","SNOW, OCCASIONAL SLEET"],
450: ["400.svg","400.svg","400","雪で雷を伴う","SNOW AND THUNDER"]
}
全てのコードをテキストで表示
TELOPS
TELOPS: {
100: ["100.svg","500.svg","100","晴","CLEAR"],
101: ["101.svg","501.svg","100","晴時々曇","PARTLY CLOUDY"],
102: ["102.svg","502.svg","300","晴一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS"],
103: ["102.svg","502.svg","300","晴時々雨","CLEAR, FREQUENT SCATTERED SHOWERS"],
104: ["104.svg","504.svg","400","晴一時雪","CLEAR, SNOW FLURRIES"],
105: ["104.svg","504.svg","400","晴時々雪","CLEAR, FREQUENT SNOW FLURRIES"],
106: ["102.svg","502.svg","300","晴一時雨か雪","CLEAR, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES"],
107: ["102.svg","502.svg","300","晴時々雨か雪","CLEAR, FREQUENT SCATTERED SHOWERS OR SNOW FLURRIES"],
108: ["102.svg","502.svg","300","晴一時雨か雷雨","CLEAR, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER"],
110: ["110.svg","510.svg","100","晴後時々曇","CLEAR, PARTLY CLOUDY LATER"],
111: ["110.svg","510.svg","100","晴後曇","CLEAR, CLOUDY LATER"],
112: ["112.svg","512.svg","300","晴後一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS LATER"],
113: ["112.svg","512.svg","300","晴後時々雨","CLEAR, FREQUENT SCATTERED SHOWERS LATER"],
114: ["112.svg","512.svg","300","晴後雨","CLEAR,RAIN LATER"],
115: ["115.svg","515.svg","400","晴後一時雪","CLEAR, OCCASIONAL SNOW FLURRIES LATER"],
116: ["115.svg","515.svg","400","晴後時々雪","CLEAR, FREQUENT SNOW FLURRIES LATER"],
117: ["115.svg","515.svg","400","晴後雪","CLEAR,SNOW LATER"],
118: ["112.svg","512.svg","300","晴後雨か雪","CLEAR, RAIN OR SNOW LATER"],
119: ["112.svg","512.svg","300","晴後雨か雷雨","CLEAR, RAIN AND/OR THUNDER LATER"],
120: ["102.svg","502.svg","300","晴朝夕一時雨","OCCASIONAL SCATTERED SHOWERS IN THE MORNING AND EVENING, CLEAR DURING THE DAY"],
121: ["102.svg","502.svg","300","晴朝の内一時雨","OCCASIONAL SCATTERED SHOWERS IN THE MORNING, CLEAR DURING THE DAY"],
122: ["112.svg","512.svg","300","晴夕方一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS IN THE EVENING"],
123: ["100.svg","500.svg","100","晴山沿い雷雨","CLEAR IN THE PLAINS, RAIN AND THUNDER NEAR MOUTAINOUS AREAS"],
124: ["100.svg","500.svg","100","晴山沿い雪","CLEAR IN THE PLAINS, SNOW NEAR MOUTAINOUS AREAS"],
125: ["112.svg","512.svg","300","晴午後は雷雨","CLEAR, RAIN AND THUNDER IN THE AFTERNOON"],
126: ["112.svg","512.svg","300","晴昼頃から雨","CLEAR, RAIN IN THE AFTERNOON"],
127: ["112.svg","512.svg","300","晴夕方から雨","CLEAR, RAIN IN THE EVENING"],
128: ["112.svg","512.svg","300","晴夜は雨","CLEAR, RAIN IN THE NIGHT"],
130: ["100.svg","500.svg","100","朝の内霧後晴","FOG IN THE MORNING, CLEAR LATER"],
131: ["100.svg","500.svg","100","晴明け方霧","FOG AROUND DAWN, CLEAR LATER"],
132: ["101.svg","501.svg","100","晴朝夕曇","CLOUDY IN THE MORNING AND EVENING, CLEAR DURING THE DAY"],
140: ["102.svg","502.svg","300","晴時々雨で雷を伴う","CLEAR, FREQUENT SCATTERED SHOWERS AND THUNDER"],
160: ["104.svg","504.svg","400","晴一時雪か雨","CLEAR, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS"],
170: ["104.svg","504.svg","400","晴時々雪か雨","CLEAR, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS"],
181: ["115.svg","515.svg","400","晴後雪か雨","CLEAR, SNOW OR RAIN LATER"],
200: ["200.svg","200.svg","200","曇","CLOUDY"],
201: ["201.svg","601.svg","200","曇時々晴","MOSTLY CLOUDY"],
202: ["202.svg","202.svg","300","曇一時雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS"],
203: ["202.svg","202.svg","300","曇時々雨","CLOUDY, FREQUENT SCATTERED SHOWERS"],
204: ["204.svg","204.svg","400","曇一時雪","CLOUDY, OCCASIONAL SNOW FLURRIES"],
205: ["204.svg","204.svg","400","曇時々雪","CLOUDY FREQUENT SNOW FLURRIES"],
206: ["202.svg","202.svg","300","曇一時雨か雪","CLOUDY, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES"],
207: ["202.svg","202.svg","300","曇時々雨か雪","CLOUDY, FREQUENT SCCATERED SHOWERS OR SNOW FLURRIES"],
208: ["202.svg","202.svg","300","曇一時雨か雷雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER"],
209: ["200.svg","200.svg","200","霧","FOG"],
210: ["210.svg","610.svg","200","曇後時々晴","CLOUDY, PARTLY CLOUDY LATER"],
211: ["210.svg","610.svg","200","曇後晴","CLOUDY, CLEAR LATER"],
212: ["212.svg","212.svg","300","曇後一時雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS LATER"],
213: ["212.svg","212.svg","300","曇後時々雨","CLOUDY, FREQUENT SCATTERED SHOWERS LATER"],
214: ["212.svg","212.svg","300","曇後雨","CLOUDY, RAIN LATER"],
215: ["215.svg","215.svg","400","曇後一時雪","CLOUDY, SNOW FLURRIES LATER"],
216: ["215.svg","215.svg","400","曇後時々雪","CLOUDY, FREQUENT SNOW FLURRIES LATER"],
217: ["215.svg","215.svg","400","曇後雪","CLOUDY, SNOW LATER"],
218: ["212.svg","212.svg","300","曇後雨か雪","CLOUDY, RAIN OR SNOW LATER"],
219: ["212.svg","212.svg","300","曇後雨か雷雨","CLOUDY, RAIN AND/OR THUNDER LATER"],
220: ["202.svg","202.svg","300","曇朝夕一時雨","OCCASIONAL SCCATERED SHOWERS IN THE MORNING AND EVENING, CLOUDY DURING THE DAY"],
221: ["202.svg","202.svg","300","曇朝の内一時雨","CLOUDY OCCASIONAL SCCATERED SHOWERS IN THE MORNING"],
222: ["212.svg","212.svg","300","曇夕方一時雨","CLOUDY, OCCASIONAL SCCATERED SHOWERS IN THE EVENING"],
223: ["201.svg","601.svg","200","曇日中時々晴","CLOUDY IN THE MORNING AND EVENING, PARTLY CLOUDY DURING THE DAY,"],
224: ["212.svg","212.svg","300","曇昼頃から雨","CLOUDY, RAIN IN THE AFTERNOON"],
225: ["212.svg","212.svg","300","曇夕方から雨","CLOUDY, RAIN IN THE EVENING"],
226: ["212.svg","212.svg","300","曇夜は雨","CLOUDY, RAIN IN THE NIGHT"],
228: ["215.svg","215.svg","400","曇昼頃から雪","CLOUDY, SNOW IN THE AFTERNOON"],
229: ["215.svg","215.svg","400","曇夕方から雪","CLOUDY, SNOW IN THE EVENING"],
230: ["215.svg","215.svg","400","曇夜は雪","CLOUDY, SNOW IN THE NIGHT"],
231: ["200.svg","200.svg","200","曇海上海岸は霧か霧雨","CLOUDY, FOG OR DRIZZLING ON THE SEA AND NEAR SEASHORE"],
240: ["202.svg","202.svg","300","曇時々雨で雷を伴う","CLOUDY, FREQUENT SCCATERED SHOWERS AND THUNDER"],
250: ["204.svg","204.svg","400","曇時々雪で雷を伴う","CLOUDY, FREQUENT SNOW AND THUNDER"],
260: ["204.svg","204.svg","400","曇一時雪か雨","CLOUDY, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS"],
270: ["204.svg","204.svg","400","曇時々雪か雨","CLOUDY, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS"],
281: ["215.svg","215.svg","400","曇後雪か雨","CLOUDY, SNOW OR RAIN LATER"],
300: ["300.svg","300.svg","300","雨","RAIN"],
301: ["301.svg","701.svg","300","雨時々晴","RAIN, PARTLY CLOUDY"],
302: ["302.svg","302.svg","300","雨時々止む","SHOWERS THROUGHOUT THE DAY"],
303: ["303.svg","303.svg","400","雨時々雪","RAIN,FREQUENT SNOW FLURRIES"],
304: ["300.svg","300.svg","300","雨か雪","RAINORSNOW"],
306: ["300.svg","300.svg","300","大雨","HEAVYRAIN"],
308: ["308.svg","308.svg","300","雨で暴風を伴う","RAINSTORM"],
309: ["303.svg","303.svg","400","雨一時雪","RAIN,OCCASIONAL SNOW"],
311: ["311.svg","711.svg","300","雨後晴","RAIN,CLEAR LATER"],
313: ["313.svg","313.svg","300","雨後曇","RAIN,CLOUDY LATER"],
314: ["314.svg","314.svg","400","雨後時々雪","RAIN, FREQUENT SNOW FLURRIES LATER"],
315: ["314.svg","314.svg","400","雨後雪","RAIN,SNOW LATER"],
316: ["311.svg","711.svg","300","雨か雪後晴","RAIN OR SNOW, CLEAR LATER"],
317: ["313.svg","313.svg","300","雨か雪後曇","RAIN OR SNOW, CLOUDY LATER"],
320: ["311.svg","711.svg","300","朝の内雨後晴","RAIN IN THE MORNING, CLEAR LATER"],
321: ["313.svg","313.svg","300","朝の内雨後曇","RAIN IN THE MORNING, CLOUDY LATER"],
322: ["303.svg","303.svg","400","雨朝晩一時雪","OCCASIONAL SNOW IN THE MORNING AND EVENING, RAIN DURING THE DAY"],
323: ["311.svg","711.svg","300","雨昼頃から晴","RAIN, CLEAR IN THE AFTERNOON"],
324: ["311.svg","711.svg","300","雨夕方から晴","RAIN, CLEAR IN THE EVENING"],
325: ["311.svg","711.svg","300","雨夜は晴","RAIN, CLEAR IN THE NIGHT"],
326: ["314.svg","314.svg","400","雨夕方から雪","RAIN, SNOW IN THE EVENING"],
327: ["314.svg","314.svg","400","雨夜は雪","RAIN,SNOW IN THE NIGHT"],
328: ["300.svg","300.svg","300","雨一時強く降る","RAIN, EXPECT OCCASIONAL HEAVY RAINFALL"],
329: ["300.svg","300.svg","300","雨一時みぞれ","RAIN, OCCASIONAL SLEET"],
340: ["400.svg","400.svg","400","雪か雨","SNOWORRAIN"],
350: ["300.svg","300.svg","300","雨で雷を伴う","RAIN AND THUNDER"],
361: ["411.svg","811.svg","400","雪か雨後晴","SNOW OR RAIN, CLEAR LATER"],
371: ["413.svg","413.svg","400","雪か雨後曇","SNOW OR RAIN, CLOUDY LATER"],
400: ["400.svg","400.svg","400","雪","SNOW"],
401: ["401.svg","801.svg","400","雪時々晴","SNOW, FREQUENT CLEAR"],
402: ["402.svg","402.svg","400","雪時々止む","SNOWTHROUGHOUT THE DAY"],
403: ["403.svg","403.svg","400","雪時々雨","SNOW,FREQUENT SCCATERED SHOWERS"],
405: ["400.svg","400.svg","400","大雪","HEAVYSNOW"],
406: ["406.svg","406.svg","400","風雪強い","SNOWSTORM"],
407: ["406.svg","406.svg","400","暴風雪","HEAVYSNOWSTORM"],
409: ["403.svg","403.svg","400","雪一時雨","SNOW, OCCASIONAL SCCATERED SHOWERS"],
411: ["411.svg","811.svg","400","雪後晴","SNOW,CLEAR LATER"],
413: ["413.svg","413.svg","400","雪後曇","SNOW,CLOUDY LATER"],
414: ["414.svg","414.svg","400","雪後雨","SNOW,RAIN LATER"],
420: ["411.svg","811.svg","400","朝の内雪後晴","SNOW IN THE MORNING, CLEAR LATER"],
421: ["413.svg","413.svg","400","朝の内雪後曇","SNOW IN THE MORNING, CLOUDY LATER"],
422: ["414.svg","414.svg","400","雪昼頃から雨","SNOW, RAIN IN THE AFTERNOON"],
423: ["414.svg","414.svg","400","雪夕方から雨","SNOW, RAIN IN THE EVENING"],
425: ["400.svg","400.svg","400","雪一時強く降る","SNOW, EXPECT OCCASIONAL HEAVY SNOWFALL"],
426: ["400.svg","400.svg","400","雪後みぞれ","SNOW, SLEET LATER"],
427: ["400.svg","400.svg","400","雪一時みぞれ","SNOW, OCCASIONAL SLEET"],
450: ["400.svg","400.svg","400","雪で雷を伴う","SNOW AND THUNDER"]
}
各要素は、キーが「天気コード」、値が「天気情報を表す配列」となっています。
「天気情報を表す配列」は 5 つの要素から構成されており、次のような内容を表します。
配列番号 | 表示例 | 説明 |
---|---|---|
0 |
"101.svg"
|
天気の画像(昼) |
1 |
"501.svg"
|
天気の画像(夜) |
2 |
"100"
|
(不明) |
3 |
"晴時々曇"
|
天気を表すテキスト(日本語) |
4 |
"PARTLY CLOUDY"
|
天気を表すテキスト(英語) |
2. 天気コードの定義ファイル作成
それでは、実際に実装を進めていきましょう。
まず、天気コードから天気情報を取得するためのファイルを作成します。
以下のように src
ディレクトリ直下に constant.js
ファイルを作成してください。
先ほどコピペで作成した天気コードのオブジェクトを export するようにしているだけです。
全てのコードをテキストで表示
src\constant.js
export const TELOPS = {
100: ["100.svg","500.svg","100","晴","CLEAR"],
101: ["101.svg","501.svg","100","晴時々曇","PARTLY CLOUDY"],
102: ["102.svg","502.svg","300","晴一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS"],
103: ["102.svg","502.svg","300","晴時々雨","CLEAR, FREQUENT SCATTERED SHOWERS"],
104: ["104.svg","504.svg","400","晴一時雪","CLEAR, SNOW FLURRIES"],
105: ["104.svg","504.svg","400","晴時々雪","CLEAR, FREQUENT SNOW FLURRIES"],
106: ["102.svg","502.svg","300","晴一時雨か雪","CLEAR, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES"],
107: ["102.svg","502.svg","300","晴時々雨か雪","CLEAR, FREQUENT SCATTERED SHOWERS OR SNOW FLURRIES"],
108: ["102.svg","502.svg","300","晴一時雨か雷雨","CLEAR, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER"],
110: ["110.svg","510.svg","100","晴後時々曇","CLEAR, PARTLY CLOUDY LATER"],
111: ["110.svg","510.svg","100","晴後曇","CLEAR, CLOUDY LATER"],
112: ["112.svg","512.svg","300","晴後一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS LATER"],
113: ["112.svg","512.svg","300","晴後時々雨","CLEAR, FREQUENT SCATTERED SHOWERS LATER"],
114: ["112.svg","512.svg","300","晴後雨","CLEAR,RAIN LATER"],
115: ["115.svg","515.svg","400","晴後一時雪","CLEAR, OCCASIONAL SNOW FLURRIES LATER"],
116: ["115.svg","515.svg","400","晴後時々雪","CLEAR, FREQUENT SNOW FLURRIES LATER"],
117: ["115.svg","515.svg","400","晴後雪","CLEAR,SNOW LATER"],
118: ["112.svg","512.svg","300","晴後雨か雪","CLEAR, RAIN OR SNOW LATER"],
119: ["112.svg","512.svg","300","晴後雨か雷雨","CLEAR, RAIN AND/OR THUNDER LATER"],
120: ["102.svg","502.svg","300","晴朝夕一時雨","OCCASIONAL SCATTERED SHOWERS IN THE MORNING AND EVENING, CLEAR DURING THE DAY"],
121: ["102.svg","502.svg","300","晴朝の内一時雨","OCCASIONAL SCATTERED SHOWERS IN THE MORNING, CLEAR DURING THE DAY"],
122: ["112.svg","512.svg","300","晴夕方一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS IN THE EVENING"],
123: ["100.svg","500.svg","100","晴山沿い雷雨","CLEAR IN THE PLAINS, RAIN AND THUNDER NEAR MOUTAINOUS AREAS"],
124: ["100.svg","500.svg","100","晴山沿い雪","CLEAR IN THE PLAINS, SNOW NEAR MOUTAINOUS AREAS"],
125: ["112.svg","512.svg","300","晴午後は雷雨","CLEAR, RAIN AND THUNDER IN THE AFTERNOON"],
126: ["112.svg","512.svg","300","晴昼頃から雨","CLEAR, RAIN IN THE AFTERNOON"],
127: ["112.svg","512.svg","300","晴夕方から雨","CLEAR, RAIN IN THE EVENING"],
128: ["112.svg","512.svg","300","晴夜は雨","CLEAR, RAIN IN THE NIGHT"],
130: ["100.svg","500.svg","100","朝の内霧後晴","FOG IN THE MORNING, CLEAR LATER"],
131: ["100.svg","500.svg","100","晴明け方霧","FOG AROUND DAWN, CLEAR LATER"],
132: ["101.svg","501.svg","100","晴朝夕曇","CLOUDY IN THE MORNING AND EVENING, CLEAR DURING THE DAY"],
140: ["102.svg","502.svg","300","晴時々雨で雷を伴う","CLEAR, FREQUENT SCATTERED SHOWERS AND THUNDER"],
160: ["104.svg","504.svg","400","晴一時雪か雨","CLEAR, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS"],
170: ["104.svg","504.svg","400","晴時々雪か雨","CLEAR, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS"],
181: ["115.svg","515.svg","400","晴後雪か雨","CLEAR, SNOW OR RAIN LATER"],
200: ["200.svg","200.svg","200","曇","CLOUDY"],
201: ["201.svg","601.svg","200","曇時々晴","MOSTLY CLOUDY"],
202: ["202.svg","202.svg","300","曇一時雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS"],
203: ["202.svg","202.svg","300","曇時々雨","CLOUDY, FREQUENT SCATTERED SHOWERS"],
204: ["204.svg","204.svg","400","曇一時雪","CLOUDY, OCCASIONAL SNOW FLURRIES"],
205: ["204.svg","204.svg","400","曇時々雪","CLOUDY FREQUENT SNOW FLURRIES"],
206: ["202.svg","202.svg","300","曇一時雨か雪","CLOUDY, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES"],
207: ["202.svg","202.svg","300","曇時々雨か雪","CLOUDY, FREQUENT SCCATERED SHOWERS OR SNOW FLURRIES"],
208: ["202.svg","202.svg","300","曇一時雨か雷雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER"],
209: ["200.svg","200.svg","200","霧","FOG"],
210: ["210.svg","610.svg","200","曇後時々晴","CLOUDY, PARTLY CLOUDY LATER"],
211: ["210.svg","610.svg","200","曇後晴","CLOUDY, CLEAR LATER"],
212: ["212.svg","212.svg","300","曇後一時雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS LATER"],
213: ["212.svg","212.svg","300","曇後時々雨","CLOUDY, FREQUENT SCATTERED SHOWERS LATER"],
214: ["212.svg","212.svg","300","曇後雨","CLOUDY, RAIN LATER"],
215: ["215.svg","215.svg","400","曇後一時雪","CLOUDY, SNOW FLURRIES LATER"],
216: ["215.svg","215.svg","400","曇後時々雪","CLOUDY, FREQUENT SNOW FLURRIES LATER"],
217: ["215.svg","215.svg","400","曇後雪","CLOUDY, SNOW LATER"],
218: ["212.svg","212.svg","300","曇後雨か雪","CLOUDY, RAIN OR SNOW LATER"],
219: ["212.svg","212.svg","300","曇後雨か雷雨","CLOUDY, RAIN AND/OR THUNDER LATER"],
220: ["202.svg","202.svg","300","曇朝夕一時雨","OCCASIONAL SCCATERED SHOWERS IN THE MORNING AND EVENING, CLOUDY DURING THE DAY"],
221: ["202.svg","202.svg","300","曇朝の内一時雨","CLOUDY OCCASIONAL SCCATERED SHOWERS IN THE MORNING"],
222: ["212.svg","212.svg","300","曇夕方一時雨","CLOUDY, OCCASIONAL SCCATERED SHOWERS IN THE EVENING"],
223: ["201.svg","601.svg","200","曇日中時々晴","CLOUDY IN THE MORNING AND EVENING, PARTLY CLOUDY DURING THE DAY,"],
224: ["212.svg","212.svg","300","曇昼頃から雨","CLOUDY, RAIN IN THE AFTERNOON"],
225: ["212.svg","212.svg","300","曇夕方から雨","CLOUDY, RAIN IN THE EVENING"],
226: ["212.svg","212.svg","300","曇夜は雨","CLOUDY, RAIN IN THE NIGHT"],
228: ["215.svg","215.svg","400","曇昼頃から雪","CLOUDY, SNOW IN THE AFTERNOON"],
229: ["215.svg","215.svg","400","曇夕方から雪","CLOUDY, SNOW IN THE EVENING"],
230: ["215.svg","215.svg","400","曇夜は雪","CLOUDY, SNOW IN THE NIGHT"],
231: ["200.svg","200.svg","200","曇海上海岸は霧か霧雨","CLOUDY, FOG OR DRIZZLING ON THE SEA AND NEAR SEASHORE"],
240: ["202.svg","202.svg","300","曇時々雨で雷を伴う","CLOUDY, FREQUENT SCCATERED SHOWERS AND THUNDER"],
250: ["204.svg","204.svg","400","曇時々雪で雷を伴う","CLOUDY, FREQUENT SNOW AND THUNDER"],
260: ["204.svg","204.svg","400","曇一時雪か雨","CLOUDY, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS"],
270: ["204.svg","204.svg","400","曇時々雪か雨","CLOUDY, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS"],
281: ["215.svg","215.svg","400","曇後雪か雨","CLOUDY, SNOW OR RAIN LATER"],
300: ["300.svg","300.svg","300","雨","RAIN"],
301: ["301.svg","701.svg","300","雨時々晴","RAIN, PARTLY CLOUDY"],
302: ["302.svg","302.svg","300","雨時々止む","SHOWERS THROUGHOUT THE DAY"],
303: ["303.svg","303.svg","400","雨時々雪","RAIN,FREQUENT SNOW FLURRIES"],
304: ["300.svg","300.svg","300","雨か雪","RAINORSNOW"],
306: ["300.svg","300.svg","300","大雨","HEAVYRAIN"],
308: ["308.svg","308.svg","300","雨で暴風を伴う","RAINSTORM"],
309: ["303.svg","303.svg","400","雨一時雪","RAIN,OCCASIONAL SNOW"],
311: ["311.svg","711.svg","300","雨後晴","RAIN,CLEAR LATER"],
313: ["313.svg","313.svg","300","雨後曇","RAIN,CLOUDY LATER"],
314: ["314.svg","314.svg","400","雨後時々雪","RAIN, FREQUENT SNOW FLURRIES LATER"],
315: ["314.svg","314.svg","400","雨後雪","RAIN,SNOW LATER"],
316: ["311.svg","711.svg","300","雨か雪後晴","RAIN OR SNOW, CLEAR LATER"],
317: ["313.svg","313.svg","300","雨か雪後曇","RAIN OR SNOW, CLOUDY LATER"],
320: ["311.svg","711.svg","300","朝の内雨後晴","RAIN IN THE MORNING, CLEAR LATER"],
321: ["313.svg","313.svg","300","朝の内雨後曇","RAIN IN THE MORNING, CLOUDY LATER"],
322: ["303.svg","303.svg","400","雨朝晩一時雪","OCCASIONAL SNOW IN THE MORNING AND EVENING, RAIN DURING THE DAY"],
323: ["311.svg","711.svg","300","雨昼頃から晴","RAIN, CLEAR IN THE AFTERNOON"],
324: ["311.svg","711.svg","300","雨夕方から晴","RAIN, CLEAR IN THE EVENING"],
325: ["311.svg","711.svg","300","雨夜は晴","RAIN, CLEAR IN THE NIGHT"],
326: ["314.svg","314.svg","400","雨夕方から雪","RAIN, SNOW IN THE EVENING"],
327: ["314.svg","314.svg","400","雨夜は雪","RAIN,SNOW IN THE NIGHT"],
328: ["300.svg","300.svg","300","雨一時強く降る","RAIN, EXPECT OCCASIONAL HEAVY RAINFALL"],
329: ["300.svg","300.svg","300","雨一時みぞれ","RAIN, OCCASIONAL SLEET"],
340: ["400.svg","400.svg","400","雪か雨","SNOWORRAIN"],
350: ["300.svg","300.svg","300","雨で雷を伴う","RAIN AND THUNDER"],
361: ["411.svg","811.svg","400","雪か雨後晴","SNOW OR RAIN, CLEAR LATER"],
371: ["413.svg","413.svg","400","雪か雨後曇","SNOW OR RAIN, CLOUDY LATER"],
400: ["400.svg","400.svg","400","雪","SNOW"],
401: ["401.svg","801.svg","400","雪時々晴","SNOW, FREQUENT CLEAR"],
402: ["402.svg","402.svg","400","雪時々止む","SNOWTHROUGHOUT THE DAY"],
403: ["403.svg","403.svg","400","雪時々雨","SNOW,FREQUENT SCCATERED SHOWERS"],
405: ["400.svg","400.svg","400","大雪","HEAVYSNOW"],
406: ["406.svg","406.svg","400","風雪強い","SNOWSTORM"],
407: ["406.svg","406.svg","400","暴風雪","HEAVYSNOWSTORM"],
409: ["403.svg","403.svg","400","雪一時雨","SNOW, OCCASIONAL SCCATERED SHOWERS"],
411: ["411.svg","811.svg","400","雪後晴","SNOW,CLEAR LATER"],
413: ["413.svg","413.svg","400","雪後曇","SNOW,CLOUDY LATER"],
414: ["414.svg","414.svg","400","雪後雨","SNOW,RAIN LATER"],
420: ["411.svg","811.svg","400","朝の内雪後晴","SNOW IN THE MORNING, CLEAR LATER"],
421: ["413.svg","413.svg","400","朝の内雪後曇","SNOW IN THE MORNING, CLOUDY LATER"],
422: ["414.svg","414.svg","400","雪昼頃から雨","SNOW, RAIN IN THE AFTERNOON"],
423: ["414.svg","414.svg","400","雪夕方から雨","SNOW, RAIN IN THE EVENING"],
425: ["400.svg","400.svg","400","雪一時強く降る","SNOW, EXPECT OCCASIONAL HEAVY SNOWFALL"],
426: ["400.svg","400.svg","400","雪後みぞれ","SNOW, SLEET LATER"],
427: ["400.svg","400.svg","400","雪一時みぞれ","SNOW, OCCASIONAL SLEET"],
450: ["400.svg","400.svg","400","雪で雷を伴う","SNOW AND THUNDER"]
}
3. forecast ストアの修正
次に、forecast ストアで、週間天気予報取得の API からデータを取得できるようにコードを追加します。
src\stores\forecast.js
ファイルを開いて、以下の赤枠部分を追加してください。
全てのコードをテキストで表示
src\constant.js
import { defineStore } from 'pinia'
import { useAreaStore } from './area'
export const useForecastStore = defineStore('forecast', {
state: () => {
return {
overview: {
headlineText: '',
publishingOffice: '',
reportDatetime: '',
targetArea: '',
text: '',
},
forecast: []
}
},
actions: {
async fetchOverview(code) {
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`
try {
const response = await fetch(url);
this.overview = await response.json();
} catch {
this.$reset()
}
},
async fetchForecast(code) {
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/forecast/${code}.json`
try {
const response = await fetch(url)
this.forecast = await response.json()
} catch {
this.$reset()
}
}
}
})
ステートに forecast: []
プロパティを追加しています。
先ほど確認した週間天気予報を取得する API のレスポンスデータは、「短期予報」と「週間予報」の配列でしたので、初期値を空配列としています。
また、新たに追加した fetchForecast
アクションは、既存の fetchOverview
アクションと全く同じ構造です。
こちらも説明不要と思います。
4. App.vue コンポーネントの修正
今まで、左メニュー(ドロワー)の地域コードをクリックすると、必ず「天気概況」ページに飛ぶようになっていました。
これを、「週間予報」ページにいるときは「週間予報」ページ内で遷移するように修正をしておきます。
src\App.vue
ファイルを開いて、以下の赤枠部分を修正してください。
全てのコードをテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const route = useRoute()
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
左側メニュー(ドロワー)の地域名のクリック時にページ遷移を行う movePage
メソッドを修正しています。
現在のルート名が 'week'
であった場合は、router.push
メソッドの
name
プロパティに 'week'
を渡すようにしています(以下)。
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
なお、プロパティの値を略さずに記述すると次のようになります。
参考:プロパティの値の表示を省略しない場合
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name: name, params: { code: code } })
}
5. Week.vue コンポーネントの修正
5-1. 実装後のイメージ
Week.vue コンポーネントでは次のような内容を表示することになります。
これを表示するデータは「エリアごと」かつ「日にちごと」のデータが必要となります。
以下の場合は、エリア数が 2 で、日にち数が 8 となります。
なお、実際は、以下のように、短期予報と週間予報のエリア数が異なる地域が存在します。
福島県の場合は、現在は、短期予報が「中通り」「浜通り」「会津」の 3 つのエリア、長期予報が「中通り・浜通り」「会津」の 2 つのエリアのデータが提供されています。
本来は、短期予報と週間予報のデータの組合せを正しく指定する必要がありますが、一旦、簡略化の観点から「短期予報と週間予報のデータはその並び順で一致する」と仮定した上で実装を進めていきます(※最後に補正を行います)。
5-2. コードの修正
src\views\Week.vue
コンポーネントを開いて、内容を全部修正します。
修正後のコードは次のようになります。
src\views\Week.vue
<script setup>
import { computed } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { TELOPS } from '../constant'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
forecastStore.fetchForecast(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchForecast(to.params.code)
})
const weekDay = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
const dateDisplay = (dateText) => {
const d = new Date(dateText)
return `${d.getMonth() + 1}月${d.getDate()}日(${weekDay[d.getDay()]})`;
}
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecast.length === 0) return []
/** 天気データを格納する配列 */
const list = []
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecastStore.forecast[0].timeSeries
// 地域(Area)数分ループする
for (let i = 0; i < srf[0].areas.length; i++) {
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
/** 1地域分のデータ(最初は短期予報の2日分を入れる) */
const areaData = []
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: srf[0].areas[i].area.name, // 地域名
weatherCode: srf[0].areas[i].weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
list.push(areaData)
}
// 週間予報のデータ処理
const week = forecastStore.forecast[1].timeSeries
for (let i = 0; i < week[0].areas.length && i < srf[0].areas.length; i++) {
for (let j = 1; j < week[0].timeDefines.length; j++) {
list[i].push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: '', // 地域名(短期予報の地域名を使用のため不要)
weatherCode: week[0].areas[i].weatherCodes[j], // 天気コード
pop: week[0].areas[i].pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas[i].reliabilities[j] ? '-' : week[0].areas[i].reliabilities[j],
tempMin: week[1].areas[i].tempsMin[j], // 最低温度
tempMax: week[1].areas[i].tempsMax[j] // 最高温度
}
)
}
}
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ areaStore.officeName }} 週間天気予報</h4>
<template v-if="forecastStore.forecast.length > 0">
<div v-for="area in weatherList" :key="area[0].areaName" class="weather-list">
<div class="area-name">● {{ area[0].areaName }}</div>
<table>
<tr class="date-heading">
<th>日付</th>
<td v-for="day in area" :key="day.timeDefines">
{{ dateDisplay(day.timeDefines) }}
</td>
</tr>
<tr>
<th>天気</th>
<td v-for="day in area" :key="day.timeDefines">
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
</td>
</tr>
<tr>
<th>降水確率</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.pop }}</td>
</tr>
<tr>
<th>信頼度</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.reliability }}</td>
</tr>
<tr>
<th>気温</th>
<td v-for="day in area" :key="day.timeDefines">
<div class="temp-max">{{ day.tempMax }}</div>
<div class="temp-min">{{ day.tempMin }}</div>
</td>
</tr>
</table>
</div>
</template>
<template v-else>
<div>{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
.weather-list {
margin-bottom: 48px;
}
.area-name {
font-size: 24px;
}
.date-heading {
background-color: #EEEEEE;
}
.temp-max {
color: #FF3300;
}
.temp-min {
color: #0066FF;
}
table {
border-collapse: collapse !important;
tr {
text-align: center;
white-space: pre-wrap;
th {
padding: 8px;
border: 1px solid gray;
}
td {
padding: 8px;
border: 1px solid gray;
}
}
}
</style>
簡略化はしているものの、それなりに長いコードとなります。
以下、コードの内容について見ていきます。
5-3. 修正したコードの概要
5-3-1. 算出プロパティ weatherList について
まず、表示用の 2 次元配列を作成している算出プロパティ weatherList
から見ていきます。
配列の中の 1 要素は、次のような 1 日分の天気情報を含んだオブジェクトとなります。
コード上では、次のような形式のオブジェクトを定義しています。
{
timeDefines: string, // 時間
areaName: string, // 地域名
weatherCode: string, // 天気コード
pop: string, // 降水確率
reliability: string, // 信頼度
tempMin: string, // 最低温度
tempMax: string // 最高温度
}
上記のオブジェクトを 1 つの要素として、2 次元配列として次のように格納していく必要があります。
コードでは、1 回目のループで「短期予報」のデータを格納し、2 回目のループで「長期予報」のデータを格納するようにしています。
① 短期予報のデータ格納
実際に「短期予報」のデータを格納するコードは以下のようになっています。
変数 i
でエリア数分ループして、変数 j
で日付分のループをしています。
下の方の areaData.push
で、要素を 1 つずつ格納していることが確認できると思います。
/** 天気データを格納する配列 */
const list = []
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecastStore.forecast[0].timeSeries
// 地域(Area)数分ループする
for (let i = 0; i < srf[0].areas.length; i++) {
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
/** 1地域分のデータ(最初は短期予報の2日分を入れる) */
const areaData = []
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: srf[0].areas[i].area.name, // 地域名
weatherCode: srf[0].areas[i].weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
list.push(areaData)
}
このコードを分かりずらくしているのが「降水確率」と「温度」のデータを加工している部分です。
・降水確率
短期予報の降水確率はレスポンスデータの [0]timeSeries[1].areas[].pops[]
から配列形式で受け取れます。
今日と明日の降水確率を 6 時間ごとに出しているので、4 回 × 2 日間で 8 要素となるのですが、本日分の過去のデータは含まれないため、要素数は 4 ~ 8 というように変動します。
そのため、要素数を 8 に統一するように以下の処理を行っています。
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
まず、concat()
メソッドで、レスポンスデータから降水確率の配列をコピーしています。
そして、8 に満たない要素の数だけ、unshift()
メソッドを使用して、配列の先頭から '-'
という文字列データを追加しています。
・温度
短期予報の温度はレスポンスデータの [0]timeSeries[2].areas[].temps[]
から配列形式で受け取れます。
今日と明日の 2 日間につき「本日の最低気温」「本日の最高気温」「明日の最低気温」「明日の最高気温」の順で格納しているため、要素数は最大 4 となります。
こちらも、過去のデータは含まれないため、要素数は 2 ~ 4 というように変動することから、以下のような処理を行っています。
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
降水確率と同じように、足りない要素の数だけ '-'
で埋めています。
なお、本日分の最低気温のデータは、基本的に非表示となるため、その要素にも '-'
を上書きしています。
② 週間予報のデータ格納
また、「週間予報」のデータを格納するコードは以下のとおりです。
list[i].push
という形で、短期予報の i
個目のエリアデータに週間予報の i
個目のエリアデータを単純に追加しています。
// 週間予報のデータ処理
const week = forecastStore.forecast[1].timeSeries
for (let i = 0; i < week[0].areas.length && i < srf[0].areas.length; i++) {
for (let j = 1; j < week[0].timeDefines.length; j++) {
list[i].push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: '', // 地域名(短期予報の地域名を使用のため不要)
weatherCode: week[0].areas[i].weatherCodes[j], // 天気コード
pop: week[0].areas[i].pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas[i].reliabilities[j] ? '-' : week[0].areas[i].reliabilities[j],
tempMin: week[1].areas[i].tempsMin[j], // 最低温度
tempMax: week[1].areas[i].tempsMax[j] // 最高温度
}
)
}
}
週間予報については、難しいことはしておらず、レスポンスデータを単純にオブジェクトに詰め込んでいるだけとなっています。
5-3-2. template の実装
2 次元配列 weatherList
を表示する部分は次のようにしています。
<template v-if="forecastStore.forecast.length > 0">
<div v-for="area in weatherList" :key="area[0].areaName" class="weather-list">
<div class="area-name">● {{ area[0].areaName }}</div>
<table>
<tr class="date-heading">
<th>日付</th>
<td v-for="day in area" :key="day.timeDefines">
{{ dateDisplay(day.timeDefines) }}
</td>
</tr>
<tr>
<th>天気</th>
<td v-for="day in area" :key="day.timeDefines">
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
</td>
</tr>
<tr>
<th>降水確率</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.pop }}</td>
</tr>
<tr>
<th>信頼度</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.reliability }}</td>
</tr>
<tr>
<th>気温</th>
<td v-for="day in area" :key="day.timeDefines">
<div class="temp-max">{{ day.tempMax }}</div>
<div class="temp-min">{{ day.tempMin }}</div>
</td>
</tr>
</table>
</div>
</template>
<template v-else>
<div>{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
① データが取得できなかった場合の処理
API で週間予報のデータが取得できなかった場合の処理は、Overview コンポーネントと同様に、v-if および v-else ディレクティブを使用して、次のように出し分けを行っています。
<template v-if="forecastStore.forecast.length > 0">
<!-- 省略 -->
</template>
<template v-else>
<div>{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
② 2 次元配列のループ処理
コード上では少し複雑な形式に見えてしまいますが、ループ処理の概略だけを示すと、次のような、よくある 2 重ループの形式になっています。
<div v-for="area in weatherList" :key="area[0].areaName">
<div v-for="day in area" :key="day.timeDefines">
<!-- ここに処理を記述 -->
</div>
</div>
外側のループで 2 次元配列 weatherList
からエリアごとのデータを抽出して、内側のループで日ごとのデータを取り出し、適宜テーブル上に割り当てていくという形になっています。
③ 天気コードから画像と日本語文字列を取得する
先に確認したように、101
というような天気コードには「晴時々曇」という日本語テキストや「101.svg」というような画像データの情報が紐づいていました。
これらの紐づくデータは、既に、src\constant.js
というファイルに取り込んでいますので、これを利用して以下のところで、日本語テキストの表示と画像の表示を行っています。
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
天気コードは day.weatherCode
で取得できます。
画像は https://www.jma.go.jp/bosai/forecast/img/ファイル名
で表示することができますので、TELOPS[day.weatherCode][0]
でファイル名を取得して当て込んでいます。
日本語テキストのインデックスは 3
でしたので TELOPS[day.weatherCode][3]
という形で取得して表示しています。
④ 日付の表示
日付については、マスタッシュ構文内で JavaScript 式を使用して表示しています。
{{ dateDisplay(day.timeDefines) }}
ここで使用している dateDisplay()
メソッドは、script 部分で次のように定義しています。
const weekDay = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
const dateDisplay = (dateText) => {
const d = new Date(dateText)
return `${d.getMonth() + 1}月${d.getDate()}日(${weekDay[d.getDay()]})`;
}
これは、"2023-02-08T11:00:00+09:00"
というような日付形式を 2月8日(水)
というように変換する単純なものです。
6. ブラウザで動作確認
ブラウザで動作確認を行ってみましょう。
まず「週間予報」タブをクリックして、続いて「青森県」を選択してください。
以下のように週間天気予報が表示されれば成功です。
この青森県のデータも、短期予報と週間予報のエリア数が異なります。
このあたりを正しく紐づけするために、次の項で追加の修正を行っていきます。
7. 短期予報と長期予報の紐づけを行う
7-1. 修正の方針
ここで行う修正については、API のレスポンスデータの解析の結果となります(解析についての細かい説明は省略します)。
修正の方針は次のとおりです。
- 短期予報と週間予報のエリア紐づけは「地域コード」を使用して行う
- 一致する地域コードがない週間予報は、短期予報の 1 つ目のデータと紐づける(※)
- 紐づけを簡便にするため 2 次元配列を Map オブジェクトに変更する
(※)のところは、おおよその傾向に基づいての実装となります。一部、正しく対応しない地域がありますので、その補正は後ほど行います。
7-2. Week.vue コンポーネントの修正
src\views\Week.vue
コンポーネントを開いて、以下の赤枠部分の修正・追加を行います。
全てのコードをテキストで表示
src\views\Week.vue
<script setup>
import { computed } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { TELOPS } from '../constant'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
forecastStore.fetchForecast(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchForecast(to.params.code)
})
const weekDay = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
const dateDisplay = (dateText) => {
const d = new Date(dateText)
return `${d.getMonth() + 1}月${d.getDate()}日(${weekDay[d.getDay()]})`;
}
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecast.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
/** 短期予報 1 つ目のエリアの地域コード */
let defaultCode = ''
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecastStore.forecast[0].timeSeries
// 地域(Area)数分ループする
for (let i = 0; i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
const code = srf[0].areas[i].area.code
if (i === 0) defaultCode = code
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
/** 1地域分のデータ(最初は短期予報の2日分を入れる) */
const areaData = []
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: srf[0].areas[i].area.name, // 地域名
weatherCode: srf[0].areas[i].weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
list.set(code, areaData)
}
// 週間予報のデータ処理
const week = forecastStore.forecast[1].timeSeries
for (let i = 0; i < week[0].areas.length && i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
let code = week[0].areas[i].area.code
if (!list.has(code)) code = defaultCode // 該当する地域コードがない場合
for (let j = 1; j < week[0].timeDefines.length; j++) {
list.get(code).push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: '', // 地域名(短期予報の地域名を使用のため不要)
weatherCode: week[0].areas[i].weatherCodes[j], // 天気コード
pop: week[0].areas[i].pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas[i].reliabilities[j] ? '-' : week[0].areas[i].reliabilities[j],
tempMin: week[1].areas[i].tempsMin[j], // 最低温度
tempMax: week[1].areas[i].tempsMax[j] // 最高温度
}
)
}
}
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ areaStore.officeName }} 週間天気予報</h4>
<template v-if="forecastStore.forecast.length > 0">
<div v-for="[code, area] in weatherList" :key="code" class="weather-list">
<div class="area-name">● {{ area[0].areaName }}</div>
<table>
<tr class="date-heading">
<th>日付</th>
<td v-for="day in area" :key="day.timeDefines">
{{ dateDisplay(day.timeDefines) }}
</td>
</tr>
<tr>
<th>天気</th>
<td v-for="day in area" :key="day.timeDefines">
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
</td>
</tr>
<tr>
<th>降水確率</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.pop }}</td>
</tr>
<tr>
<th>信頼度</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.reliability }}</td>
</tr>
<tr>
<th>気温</th>
<td v-for="day in area" :key="day.timeDefines">
<div class="temp-max">{{ day.tempMax }}</div>
<div class="temp-min">{{ day.tempMin }}</div>
</td>
</tr>
</table>
</div>
</template>
<template v-else>
<div>{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
.weather-list {
margin-bottom: 48px;
}
.area-name {
font-size: 24px;
}
.date-heading {
background-color: #EEEEEE;
}
.temp-max {
color: #FF3300;
}
.temp-min {
color: #0066FF;
}
table {
border-collapse: collapse !important;
tr {
text-align: center;
white-space: pre-wrap;
th {
padding: 8px;
border: 1px solid gray;
}
td {
padding: 8px;
border: 1px solid gray;
}
}
}
</style>
修正コードはそんなに多くはありません。
2 次元配列を、地域コードをキーに持つ Map オブジェクトに変更して、そのキーで、短期予報と週間予報を紐づけています。
7-3. ブラウザで動作確認を行う
それでは、ブラウザで動作確認を行いましょう。
先ほどと同様に「青森県」の週間予報を表示してみてください。
短期予報と週間予報のデータの連結が、変わっていることが確認できると思います。
次に「福島県」の週間予報も見てみましょう。
こちらも「会津」のデータが、正しく連結されていることが確認できます(※)。
修正を見送る事項
なお、厳密にいえば「中通り」に表示されている週間予報は「浜通り」にも適用されるものですので、これで完璧というわけではありません。
この点については、これ以上の修正は行いませんが、中通りに連結した週間予報と同じものを、浜通りにも連結するなどの実装が考えられます。
実は、まだ、一部例外が残っています。
例えば「岩手県」の週間天気予報を開いてみると、次のように週間予報が重なって表示されてしまいます。
この表示のバグについては、次のセクションで補正を行います。
8. 地域コード変換テーブルの作成
先ほど「岩手県」の例で確認しましたように、週間予報の連結がうまくいかない地域が残っています。
現時点で、これを汎用的に解決する方法は見当たらないため、これらの地域については、個別に対応することとします。
8-1. データの内容を確認する
まず、岩手県の週間予報のデータがどのようになっているかを確認してみましょう。
次のリンクから、週間予報の JSON オブジェクトを取得することができます。
https://www.jma.go.jp/bosai/forecast/data/forecast/030000.json
Chrome の開発者用画面からデータの中身を確認すると、次のようになっています。
短期予報では「内陸」のデータと「沿岸北部」「沿岸南部」の計 3 つのデータがあるのに対して、週間予報では「内陸」と「沿岸」の 2 つのデータしかありません。
週間予報の「沿岸」の地域コード 030100
を、短期予報の「沿岸北部」または「沿岸南部」のコード(030020
または 030030
)に結びつける必要があります。
8-2. 修正方針
週間予報の連結に齟齬があるのは「岩手県」のほかに「長野県」もあります。
この 2 つの地域について、以下の表の「方針」のように紐づけを行うこととします。
地域 | 週間予報コード | 短期予報コード | 方針 |
---|---|---|---|
岩手県 |
沿岸 030100
|
沿岸北部 030020 沿岸南部 030030
|
週間予報/沿岸 030100 を短期予報/沿岸北部 030020 に紐づける
|
長野県 |
中部・南部 200100
|
中部 200020 南部 200030
|
週間予報/中部・南部 200100 を短期予報/中部 200020 に紐づける
|
8-3. 変換テーブルの作成
上記の方針に基づき、変換テーブルを作成します。
実際のアプリケーション開発では、DB などにテーブルを作成することになると思いますが、ここでは、src\constant.js
ファイルに、次のようにオブジェクト形式で定義します(赤枠部分)。
全てのコードをテキストで表示
src\constant.js
/** 地域コードの読替え */
export const CODE_CONVERSION = {
'030100': '030020',
'200100': '200020'
}
export const TELOPS = {
100: ["100.svg","500.svg","100","晴","CLEAR"],
101: ["101.svg","501.svg","100","晴時々曇","PARTLY CLOUDY"],
102: ["102.svg","502.svg","300","晴一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS"],
103: ["102.svg","502.svg","300","晴時々雨","CLEAR, FREQUENT SCATTERED SHOWERS"],
104: ["104.svg","504.svg","400","晴一時雪","CLEAR, SNOW FLURRIES"],
105: ["104.svg","504.svg","400","晴時々雪","CLEAR, FREQUENT SNOW FLURRIES"],
106: ["102.svg","502.svg","300","晴一時雨か雪","CLEAR, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES"],
107: ["102.svg","502.svg","300","晴時々雨か雪","CLEAR, FREQUENT SCATTERED SHOWERS OR SNOW FLURRIES"],
108: ["102.svg","502.svg","300","晴一時雨か雷雨","CLEAR, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER"],
110: ["110.svg","510.svg","100","晴後時々曇","CLEAR, PARTLY CLOUDY LATER"],
111: ["110.svg","510.svg","100","晴後曇","CLEAR, CLOUDY LATER"],
112: ["112.svg","512.svg","300","晴後一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS LATER"],
113: ["112.svg","512.svg","300","晴後時々雨","CLEAR, FREQUENT SCATTERED SHOWERS LATER"],
114: ["112.svg","512.svg","300","晴後雨","CLEAR,RAIN LATER"],
115: ["115.svg","515.svg","400","晴後一時雪","CLEAR, OCCASIONAL SNOW FLURRIES LATER"],
116: ["115.svg","515.svg","400","晴後時々雪","CLEAR, FREQUENT SNOW FLURRIES LATER"],
117: ["115.svg","515.svg","400","晴後雪","CLEAR,SNOW LATER"],
118: ["112.svg","512.svg","300","晴後雨か雪","CLEAR, RAIN OR SNOW LATER"],
119: ["112.svg","512.svg","300","晴後雨か雷雨","CLEAR, RAIN AND/OR THUNDER LATER"],
120: ["102.svg","502.svg","300","晴朝夕一時雨","OCCASIONAL SCATTERED SHOWERS IN THE MORNING AND EVENING, CLEAR DURING THE DAY"],
121: ["102.svg","502.svg","300","晴朝の内一時雨","OCCASIONAL SCATTERED SHOWERS IN THE MORNING, CLEAR DURING THE DAY"],
122: ["112.svg","512.svg","300","晴夕方一時雨","CLEAR, OCCASIONAL SCATTERED SHOWERS IN THE EVENING"],
123: ["100.svg","500.svg","100","晴山沿い雷雨","CLEAR IN THE PLAINS, RAIN AND THUNDER NEAR MOUTAINOUS AREAS"],
124: ["100.svg","500.svg","100","晴山沿い雪","CLEAR IN THE PLAINS, SNOW NEAR MOUTAINOUS AREAS"],
125: ["112.svg","512.svg","300","晴午後は雷雨","CLEAR, RAIN AND THUNDER IN THE AFTERNOON"],
126: ["112.svg","512.svg","300","晴昼頃から雨","CLEAR, RAIN IN THE AFTERNOON"],
127: ["112.svg","512.svg","300","晴夕方から雨","CLEAR, RAIN IN THE EVENING"],
128: ["112.svg","512.svg","300","晴夜は雨","CLEAR, RAIN IN THE NIGHT"],
130: ["100.svg","500.svg","100","朝の内霧後晴","FOG IN THE MORNING, CLEAR LATER"],
131: ["100.svg","500.svg","100","晴明け方霧","FOG AROUND DAWN, CLEAR LATER"],
132: ["101.svg","501.svg","100","晴朝夕曇","CLOUDY IN THE MORNING AND EVENING, CLEAR DURING THE DAY"],
140: ["102.svg","502.svg","300","晴時々雨で雷を伴う","CLEAR, FREQUENT SCATTERED SHOWERS AND THUNDER"],
160: ["104.svg","504.svg","400","晴一時雪か雨","CLEAR, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS"],
170: ["104.svg","504.svg","400","晴時々雪か雨","CLEAR, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS"],
181: ["115.svg","515.svg","400","晴後雪か雨","CLEAR, SNOW OR RAIN LATER"],
200: ["200.svg","200.svg","200","曇","CLOUDY"],
201: ["201.svg","601.svg","200","曇時々晴","MOSTLY CLOUDY"],
202: ["202.svg","202.svg","300","曇一時雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS"],
203: ["202.svg","202.svg","300","曇時々雨","CLOUDY, FREQUENT SCATTERED SHOWERS"],
204: ["204.svg","204.svg","400","曇一時雪","CLOUDY, OCCASIONAL SNOW FLURRIES"],
205: ["204.svg","204.svg","400","曇時々雪","CLOUDY FREQUENT SNOW FLURRIES"],
206: ["202.svg","202.svg","300","曇一時雨か雪","CLOUDY, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES"],
207: ["202.svg","202.svg","300","曇時々雨か雪","CLOUDY, FREQUENT SCCATERED SHOWERS OR SNOW FLURRIES"],
208: ["202.svg","202.svg","300","曇一時雨か雷雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER"],
209: ["200.svg","200.svg","200","霧","FOG"],
210: ["210.svg","610.svg","200","曇後時々晴","CLOUDY, PARTLY CLOUDY LATER"],
211: ["210.svg","610.svg","200","曇後晴","CLOUDY, CLEAR LATER"],
212: ["212.svg","212.svg","300","曇後一時雨","CLOUDY, OCCASIONAL SCATTERED SHOWERS LATER"],
213: ["212.svg","212.svg","300","曇後時々雨","CLOUDY, FREQUENT SCATTERED SHOWERS LATER"],
214: ["212.svg","212.svg","300","曇後雨","CLOUDY, RAIN LATER"],
215: ["215.svg","215.svg","400","曇後一時雪","CLOUDY, SNOW FLURRIES LATER"],
216: ["215.svg","215.svg","400","曇後時々雪","CLOUDY, FREQUENT SNOW FLURRIES LATER"],
217: ["215.svg","215.svg","400","曇後雪","CLOUDY, SNOW LATER"],
218: ["212.svg","212.svg","300","曇後雨か雪","CLOUDY, RAIN OR SNOW LATER"],
219: ["212.svg","212.svg","300","曇後雨か雷雨","CLOUDY, RAIN AND/OR THUNDER LATER"],
220: ["202.svg","202.svg","300","曇朝夕一時雨","OCCASIONAL SCCATERED SHOWERS IN THE MORNING AND EVENING, CLOUDY DURING THE DAY"],
221: ["202.svg","202.svg","300","曇朝の内一時雨","CLOUDY OCCASIONAL SCCATERED SHOWERS IN THE MORNING"],
222: ["212.svg","212.svg","300","曇夕方一時雨","CLOUDY, OCCASIONAL SCCATERED SHOWERS IN THE EVENING"],
223: ["201.svg","601.svg","200","曇日中時々晴","CLOUDY IN THE MORNING AND EVENING, PARTLY CLOUDY DURING THE DAY,"],
224: ["212.svg","212.svg","300","曇昼頃から雨","CLOUDY, RAIN IN THE AFTERNOON"],
225: ["212.svg","212.svg","300","曇夕方から雨","CLOUDY, RAIN IN THE EVENING"],
226: ["212.svg","212.svg","300","曇夜は雨","CLOUDY, RAIN IN THE NIGHT"],
228: ["215.svg","215.svg","400","曇昼頃から雪","CLOUDY, SNOW IN THE AFTERNOON"],
229: ["215.svg","215.svg","400","曇夕方から雪","CLOUDY, SNOW IN THE EVENING"],
230: ["215.svg","215.svg","400","曇夜は雪","CLOUDY, SNOW IN THE NIGHT"],
231: ["200.svg","200.svg","200","曇海上海岸は霧か霧雨","CLOUDY, FOG OR DRIZZLING ON THE SEA AND NEAR SEASHORE"],
240: ["202.svg","202.svg","300","曇時々雨で雷を伴う","CLOUDY, FREQUENT SCCATERED SHOWERS AND THUNDER"],
250: ["204.svg","204.svg","400","曇時々雪で雷を伴う","CLOUDY, FREQUENT SNOW AND THUNDER"],
260: ["204.svg","204.svg","400","曇一時雪か雨","CLOUDY, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS"],
270: ["204.svg","204.svg","400","曇時々雪か雨","CLOUDY, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS"],
281: ["215.svg","215.svg","400","曇後雪か雨","CLOUDY, SNOW OR RAIN LATER"],
300: ["300.svg","300.svg","300","雨","RAIN"],
301: ["301.svg","701.svg","300","雨時々晴","RAIN, PARTLY CLOUDY"],
302: ["302.svg","302.svg","300","雨時々止む","SHOWERS THROUGHOUT THE DAY"],
303: ["303.svg","303.svg","400","雨時々雪","RAIN,FREQUENT SNOW FLURRIES"],
304: ["300.svg","300.svg","300","雨か雪","RAINORSNOW"],
306: ["300.svg","300.svg","300","大雨","HEAVYRAIN"],
308: ["308.svg","308.svg","300","雨で暴風を伴う","RAINSTORM"],
309: ["303.svg","303.svg","400","雨一時雪","RAIN,OCCASIONAL SNOW"],
311: ["311.svg","711.svg","300","雨後晴","RAIN,CLEAR LATER"],
313: ["313.svg","313.svg","300","雨後曇","RAIN,CLOUDY LATER"],
314: ["314.svg","314.svg","400","雨後時々雪","RAIN, FREQUENT SNOW FLURRIES LATER"],
315: ["314.svg","314.svg","400","雨後雪","RAIN,SNOW LATER"],
316: ["311.svg","711.svg","300","雨か雪後晴","RAIN OR SNOW, CLEAR LATER"],
317: ["313.svg","313.svg","300","雨か雪後曇","RAIN OR SNOW, CLOUDY LATER"],
320: ["311.svg","711.svg","300","朝の内雨後晴","RAIN IN THE MORNING, CLEAR LATER"],
321: ["313.svg","313.svg","300","朝の内雨後曇","RAIN IN THE MORNING, CLOUDY LATER"],
322: ["303.svg","303.svg","400","雨朝晩一時雪","OCCASIONAL SNOW IN THE MORNING AND EVENING, RAIN DURING THE DAY"],
323: ["311.svg","711.svg","300","雨昼頃から晴","RAIN, CLEAR IN THE AFTERNOON"],
324: ["311.svg","711.svg","300","雨夕方から晴","RAIN, CLEAR IN THE EVENING"],
325: ["311.svg","711.svg","300","雨夜は晴","RAIN, CLEAR IN THE NIGHT"],
326: ["314.svg","314.svg","400","雨夕方から雪","RAIN, SNOW IN THE EVENING"],
327: ["314.svg","314.svg","400","雨夜は雪","RAIN,SNOW IN THE NIGHT"],
328: ["300.svg","300.svg","300","雨一時強く降る","RAIN, EXPECT OCCASIONAL HEAVY RAINFALL"],
329: ["300.svg","300.svg","300","雨一時みぞれ","RAIN, OCCASIONAL SLEET"],
340: ["400.svg","400.svg","400","雪か雨","SNOWORRAIN"],
350: ["300.svg","300.svg","300","雨で雷を伴う","RAIN AND THUNDER"],
361: ["411.svg","811.svg","400","雪か雨後晴","SNOW OR RAIN, CLEAR LATER"],
371: ["413.svg","413.svg","400","雪か雨後曇","SNOW OR RAIN, CLOUDY LATER"],
400: ["400.svg","400.svg","400","雪","SNOW"],
401: ["401.svg","801.svg","400","雪時々晴","SNOW, FREQUENT CLEAR"],
402: ["402.svg","402.svg","400","雪時々止む","SNOWTHROUGHOUT THE DAY"],
403: ["403.svg","403.svg","400","雪時々雨","SNOW,FREQUENT SCCATERED SHOWERS"],
405: ["400.svg","400.svg","400","大雪","HEAVYSNOW"],
406: ["406.svg","406.svg","400","風雪強い","SNOWSTORM"],
407: ["406.svg","406.svg","400","暴風雪","HEAVYSNOWSTORM"],
409: ["403.svg","403.svg","400","雪一時雨","SNOW, OCCASIONAL SCCATERED SHOWERS"],
411: ["411.svg","811.svg","400","雪後晴","SNOW,CLEAR LATER"],
413: ["413.svg","413.svg","400","雪後曇","SNOW,CLOUDY LATER"],
414: ["414.svg","414.svg","400","雪後雨","SNOW,RAIN LATER"],
420: ["411.svg","811.svg","400","朝の内雪後晴","SNOW IN THE MORNING, CLEAR LATER"],
421: ["413.svg","413.svg","400","朝の内雪後曇","SNOW IN THE MORNING, CLOUDY LATER"],
422: ["414.svg","414.svg","400","雪昼頃から雨","SNOW, RAIN IN THE AFTERNOON"],
423: ["414.svg","414.svg","400","雪夕方から雨","SNOW, RAIN IN THE EVENING"],
425: ["400.svg","400.svg","400","雪一時強く降る","SNOW, EXPECT OCCASIONAL HEAVY SNOWFALL"],
426: ["400.svg","400.svg","400","雪後みぞれ","SNOW, SLEET LATER"],
427: ["400.svg","400.svg","400","雪一時みぞれ","SNOW, OCCASIONAL SLEET"],
450: ["400.svg","400.svg","400","雪で雷を伴う","SNOW AND THUNDER"]
}
作成した変換テーブルは以下のとおりです。
変換テーブルというよりも「読替えオブジェクト」と言った方が適切ですが、ここでは引き続き「変換テーブル」と呼びます。
/** 地域コードの読替え */
export const CODE_CONVERSION = {
'030100': '030020',
'200100': '200020'
}
プロパティのキーに対象となる「週間予報の地域コード」を、値には連結先の「短期予報の地域コード」を定義します。
なお、他にもコードの読替えが必要な地域がある場合は、ここに随時追加をしていくことになります。
8-4. Week.vue コンポーネントに変換テーブルを適用する
次に、作成した変換テーブルを Week.vue コンポーネントに適用します。
以下の赤枠部分のコードを修正してください。
全てのコードをテキストで表示
src\views\Week.vue
<script setup>
import { computed } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { TELOPS, CODE_CONVERSION } from '../constant'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
forecastStore.fetchForecast(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchForecast(to.params.code)
})
const weekDay = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
const dateDisplay = (dateText) => {
const d = new Date(dateText)
return `${d.getMonth() + 1}月${d.getDate()}日(${weekDay[d.getDay()]})`;
}
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecast.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
/** 短期予報 1 つ目のエリアの地域コード */
let defaultCode = ''
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecastStore.forecast[0].timeSeries
// 地域(Area)数分ループする
for (let i = 0; i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
const code = srf[0].areas[i].area.code
if (i === 0) defaultCode = code
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
/** 1地域分のデータ(最初は短期予報の2日分を入れる) */
const areaData = []
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: srf[0].areas[i].area.name, // 地域名
weatherCode: srf[0].areas[i].weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
list.set(code, areaData)
}
// 週間予報のデータ処理
const week = forecastStore.forecast[1].timeSeries
for (let i = 0; i < week[0].areas.length && i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
let code = week[0].areas[i].area.code
// 該当する地域コードがない場合
if (!list.has(code)) {
code = code in CODE_CONVERSION ? CODE_CONVERSION[code] : defaultCode
}
for (let j = 1; j < week[0].timeDefines.length; j++) {
list.get(code).push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: '', // 地域名(短期予報の地域名を使用のため不要)
weatherCode: week[0].areas[i].weatherCodes[j], // 天気コード
pop: week[0].areas[i].pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas[i].reliabilities[j] ? '-' : week[0].areas[i].reliabilities[j],
tempMin: week[1].areas[i].tempsMin[j], // 最低温度
tempMax: week[1].areas[i].tempsMax[j] // 最高温度
}
)
}
}
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ areaStore.officeName }} 週間天気予報</h4>
<template v-if="forecastStore.forecast.length > 0">
<div v-for="[code, area] in weatherList" :key="code" class="weather-list">
<div class="area-name">● {{ area[0].areaName }}</div>
<table>
<tr class="date-heading">
<th>日付</th>
<td v-for="day in area" :key="day.timeDefines">
{{ dateDisplay(day.timeDefines) }}
</td>
</tr>
<tr>
<th>天気</th>
<td v-for="day in area" :key="day.timeDefines">
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
</td>
</tr>
<tr>
<th>降水確率</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.pop }}</td>
</tr>
<tr>
<th>信頼度</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.reliability }}</td>
</tr>
<tr>
<th>気温</th>
<td v-for="day in area" :key="day.timeDefines">
<div class="temp-max">{{ day.tempMax }}</div>
<div class="temp-min">{{ day.tempMin }}</div>
</td>
</tr>
</table>
</div>
</template>
<template v-else>
<div>{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
.weather-list {
margin-bottom: 48px;
}
.area-name {
font-size: 24px;
}
.date-heading {
background-color: #EEEEEE;
}
.temp-max {
color: #FF3300;
}
.temp-min {
color: #0066FF;
}
table {
border-collapse: collapse !important;
tr {
text-align: center;
white-space: pre-wrap;
th {
padding: 8px;
border: 1px solid gray;
}
td {
padding: 8px;
border: 1px solid gray;
}
}
}
</style>
次のところで、変換テーブルを使用して地域コードの読替えを行っています。
// 該当する地域コードがない場合
if (!list.has(code)) {
code = code in CODE_CONVERSION ? CODE_CONVERSION[code] : defaultCode
}
code in CODE_CONVERSION
で変換テーブルにコードが存在するかを確認して、存在する場合はその値 CODE_CONVERSION[code]
に置き換えるようにしています。
8-5. ブラウザで確認する
コードの修正が終わったら、ブラウザで開いて確認をしておきましょう。
「岩手県」の週間予報を開いて、次のように表示されれば OK です。
以上で、週間予報ページの実装は完了です。
次のチャプターでは、全国の天気予報を表示する実装を行っていきます。

Lesson 9
Chapter 7
ホーム画面の実装
続いて、Home 画面の実装を行います。
Home 画面には、全国の天気予報を表示するようにします。
1. 全国主要地域の天気予報取得 API で取得できるデータの確認
全国主要地域の天気予報を取得するために、次の API を使用します。
API 名 | url |
---|---|
全国主要地域の天気予報取得 API | https://www.jma.go.jp/bosai/forecast/data/forecast/010000.json |
1-1. JSON の構造確認
この API から返されるデータを、ブラウザの開発者用画面で確認すると、以下のようなデータが取得されています。
全部で 22 の地域についてのデータが配列形式で格納されています。
一番上の「釧路」のデータを見てみましょう。
中身はオブジェクト形式となっており、name
,officeCode
,srf
,week
の 4 つのプロパティが存在します。
各プロパティには、name
に「地域名」、officeCode
に「地域コード(気象台コード)」、srf
に「短期予報」、week
に「週間予報」のデータが格納されています。
短期予報と週間予報に関するデータを見てみると次のとおりです。
①から⑫の数字は上図の番号に対応しています(⑫は図中では隠れています)。
区分 | No | プロパティ名 | 備考 |
---|---|---|---|
短期予報 | ① |
srf.timeSeries[0].areas.area.name
|
地域名 |
② |
srf.timeSeries[0].areas.area.code
|
地域コード | |
③ |
srf.timeSeries[0].areas.weatherCodes[]
|
天気コード(3日分) | |
④ |
srf.timeSeries[0].timeDefines[]
|
年月日(3日分) | |
⑤ |
srf.timeSeries[1].areas.pops[]
|
降水確率(2日分) | |
⑥ |
srf.timeSeries[2].areas.temps[]
|
温度(2日分) | |
週間予報 | ⑦ |
week.timeSeries[0].areas.area.name
|
地域名 |
⑧ |
week.timeSeries[0].areas.area.code
|
地域コード | |
⑨ |
week.timeSeries[0].areas.pops[]
|
降水確率(7日分) | |
⑩ |
week.timeSeries[0].areas.reliabilities[]
|
信頼度(7日分) | |
⑪ |
week.timeSeries[0].areas.weatherCodes[]
|
天気コード(7日分) | |
⑫ |
week.timeSeries[0].timeDefines[]
|
年月日(7日分) | |
⑬ |
week.timeSeries[1].areas.tempsMin[]
|
最低温度(7日分) | |
⑭ |
week.timeSeries[1].areas.tempsMax[]
|
最高温度(7日分) |
短期予報、週間予報とも、中身の構成は「週間天気予報取得 API」と似ていますが、「全国主要地域の天気予報取得 API」の areas
プロパティは、配列となっておらず 1 つのみのエリアのデータが入っている点で異なります。
// 週間天気予報取得 API の「地域名」
timeSeries[0].areas[].area.name
// 全国主要地域の天気予報取得 API の「地域名」
timeSeries[0].areas.area.name
以上の点を踏まえて、実装を進めていきます。
2. forecast ストアの修正
まず、forecast ストアで、全国主要地域の天気予報取得 API からデータを取得できるようにコードを追加します。
src\stores\forecast.js
ファイルを開いて、以下の赤枠部分を追加してください。
全てのコードをテキストで表示
src\constant.js
import { defineStore } from 'pinia'
import { useAreaStore } from './area'
export const useForecastStore = defineStore('forecast', {
state: () => {
return {
overview: {
headlineText: '',
publishingOffice: '',
reportDatetime: '',
targetArea: '',
text: '',
},
forecast: [],
forecastList: []
}
},
actions: {
async fetchOverview(code) {
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${code}.json`
try {
const response = await fetch(url);
this.overview = await response.json();
} catch {
this.$reset()
}
},
async fetchForecast(code) {
const areaStore = useAreaStore()
areaStore.code = code
const url = `https://www.jma.go.jp/bosai/forecast/data/forecast/${code}.json`
try {
const response = await fetch(url)
this.forecast = await response.json()
} catch {
this.$reset()
}
},
async fetchForecastList() {
const url = `https://www.jma.go.jp/bosai/forecast/data/forecast/010000.json`
try {
const response = await fetch(url)
this.forecastList = await response.json()
} catch {
this.$reset()
}
}
}
})
ステートに、全国の天気予報データを格納する forecastList: []
プロパティを追加しています。
新たに追加した fetchForecastList
アクションは、全国主要地域の天気予報取得 API を実行するものです。
地域の指定は不要なため、引数は設定していません。
3. Home.vue コンポーネントの修正
3-1. 実装後のイメージ
ホーム画面(Home.vue コンポーネント)では次のように全国の天気予報を表示することになります。
天気予報を表示する体裁は「週間予報」のものを流用します。
3-2. コードの修正
それでは、src\views\Home.vue
コンポーネントを開いて、内容を修正していきましょう。
修正後のコードは次のとおりです。
元のコードは残らず、全て修正となります。
src\views\Home.vue
<script setup>
import { computed } from 'vue'
import { useForecastStore } from '../stores/forecast'
import { TELOPS } from '../constant'
const forecastStore = useForecastStore()
forecastStore.fetchForecastList()
const weekDay = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
const dateDisplay = (dateText) => {
const d = new Date(dateText)
return `${d.getMonth() + 1}月${d.getDate()}日(${weekDay[d.getDay()]})`;
}
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecastList.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
// 全国の地域ごとにループ処理
forecastStore.forecastList.forEach((forecast) => {
/** 1地域分のデータ */
const areaData = []
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecast.srf.timeSeries
// 2日間の降水確率を取得する
const pops = srf[1].areas.pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas.temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: forecast.name, // 地域名
weatherCode: srf[0].areas.weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
// 週間予報のデータ処理
const week = forecast.week.timeSeries
for (let j = 1; j < week[0].timeDefines.length; j++) {
areaData.push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: forecast.name, // 地域名
weatherCode: week[0].areas.weatherCodes[j], // 天気コード
pop: week[0].areas.pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas.reliabilities[j] ? '-' : week[0].areas.reliabilities[j],
tempMin: week[1].areas.tempsMin[j], // 最低温度
tempMax: week[1].areas.tempsMax[j] // 最高温度
}
)
}
list.set(forecast.officeCode, areaData)
})
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">全国の週間天気予報</h4>
<template v-if="forecastStore.forecastList.length > 0">
<div v-for="[code, area] in weatherList" :key="code" class="weather-list">
<div class="area-name">● {{ area[0].areaName }}</div>
<table>
<tr class="date-heading">
<th>日付</th>
<td v-for="day in area" :key="day.timeDefines">
{{ dateDisplay(day.timeDefines) }}
</td>
</tr>
<tr>
<th>天気</th>
<td v-for="day in area" :key="day.timeDefines">
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
</td>
</tr>
<tr>
<th>降水確率</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.pop }}</td>
</tr>
<tr>
<th>信頼度</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.reliability }}</td>
</tr>
<tr>
<th>気温</th>
<td v-for="day in area" :key="day.timeDefines">
<div class="temp-max">{{ day.tempMax }}</div>
<div class="temp-min">{{ day.tempMin }}</div>
</td>
</tr>
</table>
</div>
</template>
<template v-else>
<div>全国の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
.weather-list {
margin-bottom: 48px;
}
.area-name {
font-size: 24px;
}
.date-heading {
background-color: #EEEEEE;
}
.temp-max {
color: #FF3300;
}
.temp-min {
color: #0066FF;
}
table {
border-collapse: collapse !important;
tr {
text-align: center;
white-space: pre-wrap;
th {
padding: 8px;
border: 1px solid gray;
}
td {
padding: 8px;
border: 1px solid gray;
}
}
}
</style>
コードの内容は、週間予報を表示する Week.vue コンポーネントとほぼ同じです。
ただ、週間予報のように、短期予報と週間予報を紐づける処理は不要となるため、その分、コードはシンプルになっています。
4. ブラウザで動作確認
ブラウザで動作確認を行ってみましょう。
ホーム画面を開いて、以下のように全国の天気予報が表示されれば OK です。
画面に関する実装はこれで、ほぼ出来上がりとなりますが、Week.vue コンポーネントと、Home.vue コンポーネントで同じような表示を行っている部分がありますので、次のセクションでその整理を行います。
5. 週間予報の表示部分を共通コンポーネントとする
5-1. 共通部分の特定
① template の共通部分
さて、ここまでで実装した Home.vue コンポーネントですが、以下の赤枠部分のテンプレートは、Week.vue コンポーネントと全く同じものとなっています。
② style の共通部分
上記のテンプレートに使用している CSS は次の赤枠の部分となり、これも 2 つのコンポーネントで共通のものとなっています。
③ script の共通部分
また、script の部分でも次の赤枠の記述が、上記テンプレートに関係しており、こちらも共通の処理となっています。
5-2. 共通コンポーネントの作成
それでは、以上で確認した共通部分を、新たなコンポーネントとして切り出していきます。
次のように、src\components
ディレクトリに、WeekTable.vue
ファイルを追加してください。
記述するコードは次のようになります。
src\components\WeekTable.vue
<script setup>
import { TELOPS } from '../constant'
const props = defineProps(['weatherList'])
const weekDay = { 0: '日', 1: '月', 2: '火', 3: '水', 4: '木', 5: '金', 6: '土' };
const dateDisplay = (dateText) => {
const d = new Date(dateText)
return `${d.getMonth() + 1}月${d.getDate()}日(${weekDay[d.getDay()]})`;
}
</script>
<template>
<div v-for="[code, area] in weatherList" :key="code" class="weather-list">
<div class="area-name">● {{ area[0].areaName }}</div>
<table>
<tr class="date-heading">
<th>日付</th>
<td v-for="day in area" :key="day.timeDefines">
{{ dateDisplay(day.timeDefines) }}
</td>
</tr>
<tr>
<th>天気</th>
<td v-for="day in area" :key="day.timeDefines">
<img :src="`https://www.jma.go.jp/bosai/forecast/img/${TELOPS[day.weatherCode][0]}`" />
<div>{{ TELOPS[day.weatherCode][3] }}</div>
</td>
</tr>
<tr>
<th>降水確率</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.pop }}</td>
</tr>
<tr>
<th>信頼度</th>
<td v-for="day in area" :key="day.timeDefines">{{ day.reliability }}</td>
</tr>
<tr>
<th>気温</th>
<td v-for="day in area" :key="day.timeDefines">
<div class="temp-max">{{ day.tempMax }}</div>
<div class="temp-min">{{ day.tempMin }}</div>
</td>
</tr>
</table>
</div>
</template>
<style lang="scss" scoped>
.weather-list {
margin-bottom: 48px;
}
.area-name {
font-size: 24px;
}
.date-heading {
background-color: #EEEEEE;
}
.temp-max {
color: #FF3300;
}
.temp-min {
color: #0066FF;
}
table {
border-collapse: collapse !important;
tr {
text-align: center;
white-space: pre-wrap;
th {
padding: 8px;
border: 1px solid gray;
}
td {
padding: 8px;
border: 1px solid gray;
}
}
}
</style>
基本的に共通部分を「そのまま」寄せ集めただけのコードとなっています。
ただし、script の 2 行目の以下の部分は、新しく追加したものとなります。
const props = defineProps(['weatherList'])
これは、親コンポーネントから weatherList のデータを props として受け取るための記述となります(Lesson6 Chapter2 「3. props(親から子へデータを渡す)」参照)。
5-3. Home.vue コンポーネントに共通コンポーネントを適用する
新しく作成した WeekTable.vue コンポーネントを、Home.vue コンポーネントに適用していきます。
共通コンポーネント側に移行した箇所は全て削除して、次のようにファイルを修正します。
追加するのは、以下の赤枠部分の 2 行のみです。
全てのコードをテキストで表示
src\views\Home.vue
<script setup>
import { computed } from 'vue'
import { useForecastStore } from '../stores/forecast'
import WeekTable from '../components/WeekTable.vue'
const forecastStore = useForecastStore()
forecastStore.fetchForecastList()
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecastList.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
// 全国の地域ごとにループ処理
forecastStore.forecastList.forEach((forecast) => {
/** 1地域分のデータ */
const areaData = []
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecast.srf.timeSeries
// 2日間の降水確率を取得する
const pops = srf[1].areas.pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas.temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: forecast.name, // 地域名
weatherCode: srf[0].areas.weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
// 週間予報のデータ処理
const week = forecast.week.timeSeries
for (let j = 1; j < week[0].timeDefines.length; j++) {
areaData.push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: forecast.name, // 地域名
weatherCode: week[0].areas.weatherCodes[j], // 天気コード
pop: week[0].areas.pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas.reliabilities[j] ? '-' : week[0].areas.reliabilities[j],
tempMin: week[1].areas.tempsMin[j], // 最低温度
tempMax: week[1].areas.tempsMax[j] // 最高温度
}
)
}
list.set(forecast.officeCode, areaData)
})
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">全国の週間天気予報</h4>
<template v-if="forecastStore.forecastList.length > 0">
<WeekTable :weatherList="weatherList" />
</template>
<template v-else>
<div>全国の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
</style>
WeekTable.vue
を子コンポーネントとして呼び出しているのは、次の 2 行です。
sctipt 部分
import WeekTable from '../components/WeekTable.vue'
template 部分
<WeekTable :weatherList="weatherList" />
子コンポーネントに props として、weatherList
を渡しています。
これで templete と style が随分とスッキリしました。
5-4. Week.vue コンポーネントに共通コンポーネントを適用する
続いて、Week.vue コンポーネントにも WeekTable.vue コンポーネントを適用しましょう。
src\views\Week.vue
ファイルを開いて、次のように修正します。
共通部分を削除した上で、以下の赤枠部分を修正しています(Home.vue コンポーネントと同様の修正です)。
修正箇所が分からない場合は、コードの下の「全てのコードをテキストで表示」を開いて、確認しながら補正を行ってください。
全てのコードをテキストで表示
src\views\Week.vue
<script setup>
import { computed } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useAreaStore } from '../stores/area'
import { useForecastStore } from '../stores/forecast'
import { CODE_CONVERSION } from '../constant'
import WeekTable from '../components/WeekTable.vue'
const route = useRoute()
const areaStore = useAreaStore()
const forecastStore = useForecastStore()
forecastStore.fetchForecast(route.params.code)
onBeforeRouteUpdate(async (to) => {
forecastStore.fetchForecast(to.params.code)
})
const weatherList = computed(() => {
// 週間予報データが取得できていなければ return
if (forecastStore.forecast.length === 0) return []
/** 天気データを格納するMapオブジェクト */
const list = new Map()
/** 短期予報 1 つ目のエリアの地域コード */
let defaultCode = ''
// 短期予報(Short-Range Forecast)のデータ処理
const srf = forecastStore.forecast[0].timeSeries
// 地域(Area)数分ループする
for (let i = 0; i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
const code = srf[0].areas[i].area.code
if (i === 0) defaultCode = code
// 2日間の降水確率を取得する
const pops = srf[1].areas[i].pops.concat() // 降水確率データをコピー
pops.unshift(...new Array(8 - pops.length).fill('-')) // 8 要素にする
// 2日間の温度(最低/最高)を取得する
const temps = srf[2].areas[i].temps.concat() // 温度データをコピー
temps.unshift(...new Array(4 - temps.length).fill('-')) // 4 要素にする
temps[0] = '-' // 1日目の最低温度は常に非表示
/** 1地域分のデータ(最初は短期予報の2日分を入れる) */
const areaData = []
// 今日・明日の 2 日分のデータをループ処理
for (let j = 0; j < 2; j++) {
areaData.push(
{
timeDefines: srf[0].timeDefines[j], // 時間
areaName: srf[0].areas[i].area.name, // 地域名
weatherCode: srf[0].areas[i].weatherCodes[j], // 天気コード
pop: pops.slice(j * 4, j * 4 + 4).join('/'), // 降水確率
reliability: '-', // 信頼度(短期予報は値無し)
tempMin: temps[2 * j], // 最低温度
tempMax: temps[2 * j + 1] // 最高温度
}
)
}
list.set(code, areaData)
}
// 週間予報のデータ処理
const week = forecastStore.forecast[1].timeSeries
for (let i = 0; i < week[0].areas.length && i < srf[0].areas.length; i++) {
/** Mapオブジェクトのキーとなる地域コード */
let code = week[0].areas[i].area.code
// 該当する地域コードがない場合
if (!list.has(code)) {
code = code in CODE_CONVERSION ? CODE_CONVERSION[code] : defaultCode
}
for (let j = 1; j < week[0].timeDefines.length; j++) {
list.get(code).push(
{
timeDefines: week[0].timeDefines[j], // 時間
areaName: '', // 地域名(短期予報の地域名を使用のため不要)
weatherCode: week[0].areas[i].weatherCodes[j], // 天気コード
pop: week[0].areas[i].pops[j], // 降水確率
// 信頼度(空文字の場合は '-' に変換)
reliability: !week[0].areas[i].reliabilities[j] ? '-' : week[0].areas[i].reliabilities[j],
tempMin: week[1].areas[i].tempsMin[j], // 最低温度
tempMax: week[1].areas[i].tempsMax[j] // 最高温度
}
)
}
}
return list
})
</script>
<template>
<div class="wrap">
<h4 class="heading">{{ areaStore.officeName }} 週間天気予報</h4>
<template v-if="forecastStore.forecast.length > 0">
<WeekTable :weatherList="weatherList" />
</template>
<template v-else>
<div>{{ areaStore.officeName }} の週間天気予報を取得できませんでした。</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wrap {
padding: 48px;
overflow: auto;
height: calc(100vh - 98px);
}
.heading {
margin-top: 0;
}
</style>
以上で、コンポーネントの共通化は完了です。
ブラウザで開いて、正常に動作するかを確認しておいてください。

Lesson 9
Chapter 8
検索ボックスの追加
最後の仕上げとして、アプリケーションに検索ボックスを設置します。
検索の対象は「地域名」で、検索ボックスに文字を入力すると、候補の一覧が表示されるというものです。
1. 検索ボックスの実装方法
検索用のセレクトボックスを作成する方法としては、次のようなものがあります。
- HTML5 の autocomplete 属性 を使用する
- Vue のライブラリを使用する(vue-simple-suggest,Vuetify,Quasar など)
- 自前で作成する
HTML のみで独自に実装していくと、細かい調整によりコード量が増えてしまいますので、ここでは、Quasar で用意されている q-select
タブを使用して実装していくこととします。
1-1. Quasar の q-select タブ
Quasar の q-select
タブを使用することで、HTML の select タブで作成するようなセレクトボックスを見栄えよく作成することができます。
また、オプションを指定することで、セレクトボックス内に文字入力をできるようにするなど、様々な機能を付加することができます。
具体的にどのようなものか、Quasar 公式サイトの以下のページから確認していきましょう。
数ある例のうち、上図の赤枠の Basic filtering を見ていきます。
セレクトボックス(下図の赤枠部分)をクリックすると、選択候補が下側に表示されるのが確認できます。
このボックス内に、例えば「a」と入力すると、下図のように「a」を含む文字列のみに絞込みが行われます。
この機能を使用すれば、地名一覧の検索機能の実装ができそうです。
1-2. サンプルコードの確認
この検索ボックスのソースコードを確認してみましょう。
下図の赤枠 < >
の部分をクリックすると、コードを見ることができます。
以下は template 部分のコードで、今回は、赤枠の部分を移植して使用したいと思います。
次に「script」タブをクリックして、script のコードを確認してみましょう。
以下のようなコードが示されますが、setup script 構文になっていないため、こちらは読み替えて移植を行いたいと思います。
次のセクションでは、上記のコードを天気予報アプリに移植していきます。
2. Quasar のサンプルコードの取込み
ここでは、q-select
のサンプルコードを、そのまま天気予報アプリに移植していきます。
2-1. App.vue コンポーネントの修正
src\App.vue
ファイルを開いてください。
次の図の赤枠部分に「Basic filtering」のサンプルコードを移植していきます。
コード全体をテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const route = useRoute()
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const model = ref(null)
const stringOptions = [
'Google', 'Facebook', 'Twitter', 'Apple', 'Oracle'
]
const options = ref(stringOptions)
const filterFn = (val, update, abort) => {
update(() => {
const needle = val.toLowerCase()
options.value = stringOptions.filter(v => v.toLowerCase().indexOf(needle) > -1)
})
}
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<div class="row justify-between items-center full-width">
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
<div class="row items-center">
<q-select
filled
v-model="model"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
@filter="filterFn"
hint="Basic filtering"
style="width: 250px; padding-bottom: 32px"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
なお、上の図の template 内の青枠の部分は、flexbox を使用して、検索ボックスの表示位置を調整するために追加しています。
以下、追加したコードの確認をしていきます。
2-2. template 内の追加コード確認
まず、template に追加したコードについて見ていきましょう。
修正部分は以下のようになっています。Quasar 独自の書き方が含まれているため、見慣れないコードも多いと思います。
src\App.vue
<div class="row justify-between items-center full-width">
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
<div class="row items-center">
<q-select
filled
v-model="model"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
@filter="filterFn"
hint="Basic filtering"
style="width: 250px; padding-bottom: 32px"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
① Quasar のレイアウト調整
Quasar では、あらかじめ用意された CSS のクラスを使用することによって、flexbox などのレイアウト指定を簡単に行うことができます(公式ページ参照)。
ここでクラスによる指定を行っているのは次の部分です。
<div class="row justify-between items-center full-width">
<div class="row items-center">
例えば、単に row
というクラスを指定するだけで、その子要素(ブロック要素)を横並びに配置することができます。
今回使用したクラスにより、次のような指定が行われています。
No | クラス名 | 説明 |
---|---|---|
1 |
row
|
flexbox の指定をする(HTML 要素を横並びに配置) |
2 |
justify-between
|
justify-content: space-between; を指定する(要素を両端に配置)
|
3 |
items-center
|
align-items: center; を指定する(要素を上下中央に配置)
|
4 |
full-width
|
width: 100%; を指定する
|
上記以外にも、様々なクラスが用意されていますが、ここでの説明は割愛します。
② q-select タブの属性/メソッド
q-select
タブには、次のように様々な属性(またはメソッド)が指定されています。
<q-select
filled
v-model="model"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
@filter="filterFn"
hint="Basic filtering"
style="width: 250px; padding-bottom: 32px"
>
上記のうち、v-model
と style
以外は、全て Quasar が独自に用意した属性となります。
例えば、属性として use-input
を記述することにより、セレクトボックス内に文字入力をすることができるようになります。
ここで使用されている属性(またはメソッド)とその意味は、次の表のとおりとなります。
No | 属性/メソッド | 説明 |
---|---|---|
1 |
filled 属性
|
セレクトボックスに「塗りつぶし」デザインを使用する |
2 |
use-input 属性
|
セレクトボックス内へのテキスト入力を可能とする |
3 |
hide-selected 属性
|
選択した内容をセレクトボックスに表示しないようにする |
4 |
fill-input 属性
|
現在の選択値をセレクトボックスに表示する |
5 |
input-debounce 属性
|
検索文字入力後に結果を表示するタイムラグをミリ秒で指定 |
6 |
options 属性
|
選択肢に表示する文字列を配列で指定{ label: 表示名, value: 値 } 形式のオブジェクトも指定可
|
7 |
@filter メソッド
|
検索文字入力時に絞込みを実行する関数を指定 |
8 |
hint 属性
|
ボックスの下側にヒントを表示する |
上記のうち、特に重要なものを 2 つ見ておきます。
・ options 属性
options には、セレクトメニューに表示される選択肢を配列で指定します。
文字列の配列のほか、オブジェクトの配列を指定することができます。
文字列の配列
['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle']
オブジェクトの配列
[
{ label: 'Google', value: 1 },
{ label: 'Facebook', value: 2 },
{ label: 'Twitter', value: 3 },
{ label: 'Apple', value: 4 },
{ label: 'Oracle', value: 5 }
]
なお、オブジェクトの配列で指定する場合は、label
には表示する値、value
には v-model で受け取る値を指定します。
キー名は、原則として label
と value
で指定する必要がありますが、別のキー名を指定する方法もあります(後述)。
・ @filter メソッド
@filter には、検索文字を入力した際に実行するメソッドを指定します。
@filter="filterFn"
上記の場合は、検索文字を 1 文字入力するたびに、filterFn
メソッドが実行されることとなります。
その他、q-select
タブで使える属性については、公式ページの以下のところで紹介されていますので、必要に応じてご確認ください。
③ no-option スロット
次の v-slot:no-option
内には、検索結果が無かった場合の表示内容を指定します。
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
上記の場合、検索結果が存在しない場合は「No results」と表示されることになります。
2-3. script 内の追加コード確認
次に、script に追加したコードを確認していきます。
src\App.vue
const model = ref(null)
const stringOptions = [
'Google', 'Facebook', 'Twitter', 'Apple', 'Oracle'
]
const options = ref(stringOptions)
const filterFn = (val, update, abort) => {
update(() => {
const needle = val.toLowerCase()
options.value = stringOptions.filter(v => v.toLowerCase().indexOf(needle) > -1)
})
}
変数 options
の初期値には全配列 ['Google', 'Facebook', 'Twitter', 'Apple', 'Oracle']
がセットされます。
そして、filterFn
メソッドが実行されると、options
配列は、第 1 引数 val
(ユーザー入力値)の文字を含んだ文字列のみにフィルタリングされています。
なお、toLowerCase()
メソッドで小文字に変換して比較することで、大文字・小文字の区別なく文字列の一致を判定しています。
上記の filterFn
メソッドは、template 側の @filter
イベントに指定された関数です。
この @filter
イベントは、指定したメソッドに 3 つの引数を渡します(下記)。
@filter -> function(inputValue, doneFn, abortFn)
No | 引数 | 説明 |
---|---|---|
1 |
inputValue
|
ユーザーが入力した値を受け取る |
2 |
doneFn
|
更新を実行する関数を指定する(コールバック関数で指定) |
3 |
abortFn
|
問題が生じた場合に実行する関数を指定する(コールバック関数で指定) |
つまり、第 1 引数 inputValue
でユーザーの入力値を受け取り、第 2 引数の doneFn
を使用して options
配列の要素をフィルタリングするという処理が実行されていることになります。
2-4. ブラウザで確認
さて、ブラウザを開いて、サンプルコードの内容が天気予報アプリに正しく移植できたかを確認してみましょう。
次のように、ブラウザの右上の部分にセレクトボックスが追加され、入力した文字列に従って選択肢がフィルタリングされていれば成功です。
3. 検索ボックスの選択肢に関する実装
次に、検索用のセレクトボックスの選択肢に「地域名」を表示できるようにコードを修正していきます。
地域名は area ストアに保持されていますが、選択肢として表示するためには配列形式とする必要があります。
3-1. area ストアの修正
src\stores\area.js
ファイルを開きます。
そして、以下のように officeArray
ゲッターを追加してください。
コード全体をテキストで表示
src\stores\area.js
import { defineStore } from 'pinia'
export const useAreaStore = defineStore('area', {
state: () => {
return {
area: {
offices: {}
},
code: '130000'
}
},
getters: {
offices: (state) => {
return new Map(
Object.entries(state.area.offices)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.map(([key, value]) => [key, value.name])
)
},
officeName(state) {
return this.offices.get(state.code)
},
officeArray() {
return [...this.offices.entries()].map(([key, value]) => {
return {
code: key,
name: value
}
})
}
},
actions: {
async fetchArea() {
const url = 'https://www.jma.go.jp/bosai/common/const/area.json'
const response = await fetch(url)
this.area = await response.json()
}
}
})
既存の offices
ゲッターをそのまま選択肢の配列として使用できれば良いのですが、これは Map オブジェクトであるため、そのまま options
属性に渡すことができません。
そのため、新たに officeArray
ゲッターを追加し、既存の offices
ゲッターから次の形式の配列を作成するようにしています。
[
{ code: '011000', name: '宗谷地方' },
{ code: '012000', name: '上川・留萌地方' },
{ code: '013000', name: '網走・北見・紋別地方' },
{ code: '014030', name: '十勝地方' },
...
]
コードについて簡単に説明すると、[...this.offices.entries()]
の部分で Map オブジェクトを配列に置き換えて、さらに、map() メソッドで { code: 地域コード, name: 地域名 }
の形式に整形しているということになります。
以下、この officeArray
ゲッターを使用して検索用セレクトボックスの選択肢を作成していきます。
3-2. 選択肢をオブジェクト配列で受け取る
選択肢を「オブジェクト配列」で受け取る場合のデフォルトのプロパティキーは、label
と value
で指定する必要がありました(前出)。
しかし、先ほど作成した officeArray
ゲッターから渡されるオブジェクト配列のプロパティキーは、name
と code
となっています。
① 公式ページのサンプルコードを確認する
このような場合の指定方法は、Quasar 公式ページの Custom prop names のところ(下図)に例が示されていますので、それを使用していきます。
ページを開いたら、以下の赤枠部分 < >
をクリックしてコードを表示させましょう。
次のように、template 部分のサンプルコードが表示されます。
選択肢配列 options
をオブジェクト配列の形式で受け取り、かつ、プロパティキーにデフォルト(label
,value
)以外の名前を付ける場合は、赤枠部分のように指定を行うこととなります。
script 部分については、次のようなサンプルが示されています。
options
が、オブジェクト配列であることが確認できます(こちらのコードは特に使用しません)。
② App.vue コンポーネントの修正
src\App.vue
ファイルを開いて、次の赤枠部分の修正を行います。
コード全体をテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const route = useRoute()
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const selected = ref(null)
const options = ref([])
const filterFn = (val, update) => {
update(() => {
options.value = areaStore.officeArray.filter(v => v.name.indexOf(val) > -1)
})
}
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<div class="row justify-between items-center full-width">
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
Title
</q-toolbar-title>
<div class="row items-center">
<q-select
filled
v-model="selected"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
option-value="code"
option-label="name"
emit-value
map-options
@filter="filterFn"
hint="Basic filtering"
style="width: 250px; padding-bottom: 32px"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
script 部分は次のように修正しています。
const selected = ref(null)
const options = ref([])
const filterFn = (val, update) => {
update(() => {
options.value = areaStore.officeArray.filter(v => v.name.indexOf(val) > -1)
})
}
修正事項は次のとおりです。
- リアクティブ変数の名称を
model
からselected
に修正 options
の初期値を空配列[]
に修正filterFn
メソッドの文字列比較のtoLowerCase()
は不要なため削除options
にareaStore.officeArray
を適用する
template 部分では、以下の 4 つの属性を <q-select>
タグに追加しています。
<q-select
// 省略
option-value="code"
option-label="name"
emit-value
map-options
// 省略
>
それぞれの属性の機能は次のとおりです。
No | 属性 | 説明 |
---|---|---|
1 |
option-value 属性
|
options に渡すオブジェクト配列の value キーに別名を与える
|
2 |
option-label 属性
|
options に渡すオブジェクト配列の label キーに別名を与える
|
3 |
emit-value 属性
|
選択したオブジェクトの値(value )で v-model の値を更新する
|
4 |
map-options 属性
|
選択肢に値ではなくラベルテキスト(label )を表示する
|
option-value
,option-label
を使用することで、{ code: 地域コード, name: 地域名 }
を { value: 地域コード, label: 地域名 }
と読み替えさせることができます。
3-3. ブラウザで動作確認
修正したコードの適用結果をブラウザで確認してみましょう。
検索ボックスをクリックすると、次のように、選択肢に地名が表示されるはずです。
検索ボックスに「山」と打ち込むと、地域名に「山」を含む選択肢のみに絞り込みが行われます。
なお、現時点では、地域名の選択を行っても、画面の変動はありません。
次のセクションで、地域名選択時に発火するイベント処理を追加した上で、最終的な仕上げを行っていきましょう。
4. 地域名選択時のイベント処理追加と見た目の補正
それでは、最後の仕上げを行っていきます。
検索ボックスから地域名を選択した際に発火するイベント処理を追加した上で、見た目の補正も併せて行っていきます。
4-1. App.vue コンポーネントの修正
再度、src\App.vue
ファイルを開いて、以下の赤枠部分の修正を行ってください。
コード全体をテキストで表示
src\App.vue
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAreaStore } from './stores/area'
const route = useRoute()
const router = useRouter()
const areaStore = useAreaStore()
areaStore.fetchArea()
const selected = ref(null)
const options = ref([])
const filterFn = (val, update) => {
update(() => {
options.value = areaStore.officeArray.filter(v => v.name.indexOf(val) > -1)
})
}
const updateSelect = (code) => movePage(code)
const movePage = (code) => {
const name = route.name === 'week' ? 'week' : 'overview'
router.push({ name, params: { code } })
}
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => leftDrawerOpen.value = !leftDrawerOpen.value
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<div class="row justify-between items-center full-width">
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg">
</q-avatar>
天気予報アプリ
</q-toolbar-title>
<div class="row items-center">
<span>地域検索: </span>
<q-select
outlined
v-model="selected"
dense
options-dense
use-input
hide-selected
input-debounce="0"
:options="options"
option-value="code"
option-label="name"
emit-value
map-options
@filter="filterFn"
style="width: 250px;"
bg-color="grey-2"
@update:model-value="updateSelect"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
検索結果なし
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</q-toolbar>
<q-tabs align="left">
<q-route-tab to="/" label="ホーム" />
<q-route-tab :to="`/overview/${areaStore.code}`" label="天気概況" />
<q-route-tab :to="`/week/${areaStore.code}`" label="週間予報" />
</q-tabs>
</q-header>
<q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
<div
v-for="[code, name] in areaStore.offices"
:key="code"
@click="movePage(code)"
class="area-item"
>
{{ name }}
</div>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<style lang="scss" scoped>
.area-item {
line-height: 28px;
padding: 2px 16px;
cursor: pointer;
&:hover {
background-color: #EEEEEE;
}
}
</style>
4-2. コードの修正内容の確認
q-select
タグ内の @update:model-value
メソッドで v-model
の値が変動した際のイベントをキャッチしています。
これにより、ユーザーが検索ボックスから地域名を選択すると updateSelect
メソッドが実行されることになります。
<q-select
// 省略
@update:model-value="updateSelect"
>
script 側で指定している updateSelect
メソッドは次のとおりです。
第 1 引数で、v-model
の値(ここでは地域コード)を受け取ることができますので、これを利用して、既存の movePage
メソッドを実行するようにしています。
const updateSelect = (code) => movePage(code)
以上で、検索ボックスから地域名を選択した場合のイベント処理の実装は完了です。
その他の部分は、体裁等を直すための指定となります。
新たに q-select
タグに指定した属性/メソッドの内容は次のとおりです。
No | 属性/メソッド | 説明 |
---|---|---|
1 |
outlined 属性
|
セレクトボックスに枠で囲んだデザインを使用する |
2 |
dense 属性
|
セレクトボックスの高さを狭くする |
3 |
options-dense 属性
|
選択肢の個々の高さを狭くする |
4 |
bg-color 属性
|
セレクトボックスの背景色を指定する |
5 |
@update:model-value メソッド
|
v-model の値が変動した際に実行するメソッドを指定する |
4-3. ブラウザで動作確認
それでは、ブラウザで動作確認を行いましょう。
検索ボックスに「川」と打ち込んで「神奈川県」を選択してみます。
次のように、神奈川県の天気概況が表示されれば OK です。
続いて「週間予報」タブをクリックしてみましょう。
選択した神奈川県の週間予報が表示されることが確認できるはずです。
以上、これで Vue アプリの作成は完了です。
上手く動作しない場合は、原因を検討しながら、試行錯誤をしてみてください。
なお、掲載しているコードをコピペすれば動くようにしていますので、どうしてもアプリが動かない場合は、適宜コピペをするなどしていただければと思います。
本アプリについては、次の Lesson 10「Vue のテスト」でも使用しますので、削除せずに残しておいてください。
