天気予報アプリを作ろう
初めに 【5分】
今回の目的
JavascriptとReactを組み合わせて、簡単なWebアプリケーションを作成してみよう!
Reactって?
Reactとは、Javascriptの開発で使用できるライブラリの一種です。
ライブラリとは簡単に言うと「ある機能を切り出して、使いまわせるようにしたもの」といった感じのものです。
使用感としてはフレームワークに近い部分がありますが、位置づけとしてはライブラリであるということを覚えておいてください。
Reactを使用したフレームワークも存在し、下記のようなものが代表的です。
- Gatsby.js
- Next.js
要件定義をしよう!
要件定義とは簡単に言うと、「こういうのを作ろう!」と事前に決めておくことです。
例えば料理を作る際、献立も何も決めずに調理に取り掛かる人は少ないと思います。
まずは献立を決めて、それに沿って材料の切り方や火の通し方を考え、調理を始めますね?
事前に作るものを決めておかないと、「無駄な作業が発生したり、逆に必要な工程が足りなかったり」してしまいます。
開発を行う際も同じように、「実装される機能やシステム仕様」をあらかじめ定めておくことが何よりも重要になります。
今回は簡単に、下記のような内容を要件として定めます。
- 1時間おきの気象情報を表示する
- 表示する気象情報は、天候名(晴れ、雨...)だけでよい
- 複数地点の気象情報を表示する
1. アプリケーション作成準備 【10分】
1-1. システム設定値を修正 (2分)
Linux上でReact開発を行う場合、ファイル監視数上限に引っかかり下記のようなエラーが出る場合があります。
Error: ENOSPC: System limit for number of file watchers reached
これの対策として、システム上の上限値を事前に修正しておきます。
$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
fs.inotify.max_user_watches=524288
$ sudo sysctl -p
fs.inotify.max_user_watches = 524288
$ cat /proc/sys/fs/inotify/max_user_watches
524288
1-2. Reactアプリケーションの初期設定 (5分)
create-react-app
コマンドを使用することで、Reactアプリケーションの初期構成設定を自動で行うことが可能です。
自身でReact用のファイル構成などを整えるのは手間なので、特別な理由がなければコマンド経由でアプリケーションの初期設定を行うことをお勧めします。
# usage: npx create-react-app <アプリケーション名>
$ npx create-react-app weather-app
# create-react-appコマンドがインストールされていないと下記が出ます。
# 「y」を入力しEnterでOK
Need to install the following packages:
create-react-app@5.0.1
Ok to proceed? (y)
# 下記のような表示が出て、初期テンプレートのインストールが始まります。
Creating a new React app in /home/ec2-user/weather-app.
# 下記のように出ればReactアプリケーションのテンプレートが作成完了です。
Success! Created weather-app at /home/ec2-user/weather-app
Inside that directory, you can run several commands:
npm start
Starts the development server.
npm run build
Bundles the app into static files for production.
npm test
Starts the test runner.
npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd weather-app
npm start
Happy hacking!
npx
経由で実行することで、create-react-app
を導入・未導入関わらず実行が可能です。
1-3. ライブラリインストール (2分)
今回のコーディングにおいて使用するnpmライブラリを事前にインストールしておきます。
ライブラリは、必要になったタイミングでのインストールでもOKです。
$ cd weather-app
$ npm install axios date-fns react-icons
# 下記のような表示が出ればインストールOK(数値に差異があるのはOK)
added 14 packages in 4s
ライブラリ | 説明 |
---|---|
axios | HTTPリクエスト(APIコール)を行うためのクライアントです |
date-fns | Date(日付)型のデータを手軽に扱うためのライブラリです |
react-icons | 様々なアイコンを使用できるライブラリです |
create-react-app
コマンドで簡単にReactアプリケーションの初期構成が可能です。
2. Reactアプリケーションの起動 【2分】
アプリケーション作成後のメッセージで表示されているコマンドを使用し、Reactアプリケーションを起動してみよう!
2-1. npm start (2分)
Reactアプリケーションの起動にはnpm start
を実行します。
# 必ずアプリケーションのルートディレクトリ(package.jsonがある場所)で行う
$ pwd
/path/to/weather-app
$ npm start
# 下記のように出ればアプリケーションの立ち上げ成功です。
# 表示されているhttp://localhost:3000にアクセスしてみましょう。
Compiled successfully!
You can now view weather-app in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.94.126:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
webpack compiled successfully
Reactアプリケーションの起動はnpm start
で行います。
3. アプリケーションの編集 【3分】
3-1. Hello World表示 (3分)
本格的な編集に入る前に、まずはアプリケーションのお試し編集としていわゆるハローワールドを表示させてみましょう。
先ほど試しに起動したアプリケーションに表示されていた画面は、src/App.js
で記述されています。
初期設定ではこのsrc/App.js
がいわゆるアプリケーションのコンテンツになりますので、このファイルを修正すれば画面に表示される内容を変更することが可能です。
$ vi src/App.js
# vscodeの人は下記でもOK
$ code src/App.js
/* 下記削除 */
import logo from './logo.svg';
import './App.css';
/* ここまで */
// function App() {
const App = () => {
return (
{/* 下記追加 */}
<div>Hello, React JS.</div>
{/* 下記削除 */}
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
{/* ここまで */}
);
}
export default App;
アプリケーションの編集は、保存したタイミングでリアルタイム反映されます。
この機能をホットリロードと呼び、Reactでの開発がお手軽な要素の一端を担っています。
Reactアプリケーションの編集は「ホットリロード」で行われるため、反映するための再起動は不要です。
4. 東京の天気を取得しよう 【15分】
それでは天気予報アプリケーションの作成を始めましょう!
第1段階のゴールは「東京都の天気を表示する」にします。
気象情報の取得には下記のAPIを使用します。
サービス名 | 利用目的 | URL |
---|---|---|
OpenMeteo API | 気象データの取得 | https://open-meteo.com/ |
4-1. 地点情報定義 (3分)
このAPIの仕様として、気象情報の取得は緯度/経度をもとに行います。
そのため東京都の緯度/経度情報が欲しいですが、サイトによって微妙に値が変わっていたりしますのでしっかり信用できる情報源を探しましょう。
今回は下記を参考。
サービス名 | 利用目的 | URL |
---|---|---|
個人のサイト | 緯度/経度情報の取得 | http://agora.ex.nii.ac.jp/digital-typhoon/search_place.html.ja |
記載の緯度/経度情報を使用して、下記のように東京都の地点情報を定義します。
/* 下記追加 */
const locationList = [
{ enName: 'tokyo', jpName: '東京', lat: 35.689, lon: 139.692 }
];
/* ここまで */
const App = () => {
/* 下記追加 */
const location = locationList[0];
/* ここまで */
return (
<div>Hello, React JS.</div>
);
}
変数名 | 説明 | 値例 |
---|---|---|
locationList | [配列] 地点情報の配列 |
[ { 地点情報01 }, { 地点情報02 }, ... ] |
location | [オブジェクト] 現在選択されている地点情報 |
{ enName: <地名英字>, jpName: <地名表示名>, lat: <緯度>, lon: <経度> } |
東京都の地点情報を配列の要素として定義したのは、「後ほど別地点の情報も追加できるようにしたいため」です。
事前に「複数地点の気象情報を表示する」と要件定義しているので、このように先回りした実装を行います。
4-2. 気象情報取得APIコール (5分)
実際に東京都の気象情報をAPIで取得してみます。
アプリケーション内からコールするURLは下記のような形式になります。
(例) 東京の気象情報取得API |
---|
https://api.open-meteo.com/v1/forecast?latitude=35&longitude=135&hourly=weather_code&timezone=Asia%2FTokyo |
それではアプリケーション内からコールしてみましょう。
Reactを代表するユーティリティである、useEffectとuseStateを使います。
/* 下記追加 */
import { useState, useEffect } from 'react';
import axios from 'axios';
// 気象情報APIをコールするクライアント
const weatherClient = axios.create({
headers: {
'Content-Type': 'application/json'
}
});
const openMeteoApiBase = 'https://api.open-meteo.com/v1/forecast'; // 気象情報API
/* ここまで */
const locationList = [
{ enName: 'tokyo', jpName: '東京', lat: 35.689, lon: 139.692 }
];
const App = () => {
const location = locationList[0];
/* 下記追加 */
const [weatherInfo, setWeatherInfo] = useState([]); // 気象情報を保持するstate
// 気象情報APIコールを実行
// useEffect() : 画面ロード後に実行される処理
// (async function)() : 非同期処理を行うためにasync関数で即時実行関数を組みます
// try~catch : try~catch構文でエラーが起きた場合にケアできるようにします
useEffect(() => {
(async() => {
try {
// APIコールし気象情報取得
const res = await weatherClient.get(
`${openMeteoApiBase}?timezone=Asia/Tokyo&latitude=${location.lat}&longitude=${location.lon}&hourly=weather_code`
);
const weatherData = res.data;
setWeatherInfo(weatherData); // stateに気象データをセット
} catch (error) {
alert(error.message);
}
})();
}, [])
/* ここまで */
return (
{/* 下記削除 */}
<div>Hello, React JS.</div>
{/* 下記追加 */}
<div style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(weatherInfo, null, 2)}</div> {/* 気象情報表示 */}
{/* ここまで */}
);
}
詳しく説明をすると時間がかかるので省きますが、useEffectとuseStateはともにReact Hooksと呼ばれる非常に便利なユーティリティです。
特にこの2つに関しては、Reactで開発を行う上で非常に出番の多い重要なhooksとなります。
簡潔に説明するのは難しいですが、下記にそれぞれの役割を短くまとめておきます。
hooks | 説明 | 備考 |
---|---|---|
useState | クライアントサイド(ブラウザ上)で、 ある変数の値を保持・更新できます。 配列の第1要素が値を格納する変数(state) 第2要素が変数の値を書き換える関数。 |
useSate()に渡している引数は stateの初期値になります。 |
useEffect | クライアントサイド(ブラウザ上)で、 下記の場合に第1引数の関数を実行。 ・初回ロード後(ページ開いた際) ・第2引数の配列に与えた変数の更新時 |
第2引数を与えない場合、 バグを生む可能性あり。 依存する変数が無い場合も 必ず空配列を与えること。 |
大事なポイントは「クライアント、つまりページを受け取るユーザ側(ブラウザ上)で行いたい処理があるときに使う」という点です。
イメージしづらい人はそれだけで一旦はOKです。
4-3. 取得データ成型 (3分)
APIから取得したデータには、今回のアプリケーションでは画面に表示しない余分な内容があります。
今回必要なデータはhourly
に入っているデータのみなので、下記のようにして必要なデータのみ抜き出します。
...
const App = () => {
const location = locationList[0];
const [weatherInfo, setWeatherInfo] = useState([]);
// 気象情報APIコールを実行
useEffect(() => {
(async() => {
try {
const res = await weatherClient.get(
`${openMeteoApiBase}?timezone=Asia/Tokyo&latitude=${location.lat}&longitude=${location.lon}&hourly=weather_code`
);
/* 下記削除 */
const weatherData = res.data;
setWeatherInfo(weatherData);
/* 下記追加 */
const weatherData = res.data.hourly;
const weatherDataByTime = weatherData.time.map((time, index) => {
return {
datetime: time,
weatherCode: weatherData.weather_code[index]
}
}, {});
setWeatherInfo(weatherDataByTime);
/* ここまで */
} catch (error) {
alert(error.message);
}
})();
}, [])
return (
<div style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(weatherInfo, null, 2)}</div>
);
}
Reactアプリケーションの開発では、React Hooksというユーティリティの利用が大変便利です。
5. UI設定 【10分】
必要な気象データを取得し、画面に表示しました。
しかし今画面に出ているのは生のAPIデータなので、非常に見にくいです。
そのため続いては画面UIの設定をしていきましょう。
5-1. 気象コードを気象名と対応させる (5分)
画面設定の前に、設定が必要な要素があります。
現在は気象の名前が取得できておらず、気象コードしかデータ内に存在しません。
気象名についてはAPIから取得することはできないので、自分で設定を行いましょう。
また気象名だけでは味気ないので、視認性UPも兼ねて天気のアイコンも表示させます。
インストールしていたreact-icons
を使いましょう。
Document |
---|
https://react-icons.github.io/react-icons/ |
新規ファイルsrc/weather_names.js
を作成し、その中で気象コードを気象名に対応させたリストを定義します。
次のファイルはちょっと説明が長くなるのでコピペでOKです。
$ vi src/weather_names.js
import { cloneElement } from 'react';
import { MdSunny, MdCloud, MdFoggy } from 'react-icons/md';
import { IoRainy, IoSnowSharp } from 'react-icons/io5';
import { AiFillThunderbolt } from 'react-icons/ai';
// 気象名のリスト { 気象コード: [気象名, 気象カテゴリ], ... }
const weatherNames = {
0: ['快晴', 'sunny'],
1: ['晴れ', 'sunny'],
2: ['薄曇り', 'cloudy'],
3: ['曇り', 'cloudy'],
45: ['霧', 'foggy'],
48: ['氷霧','foggy'],
51: ['薄い霧雨', 'rainy'],
53: ['霧雨', 'rainy'],
55: ['濃い霧雨', 'rainy'],
56: ['薄い着氷性の霧雨', 'rainy'],
57: ['濃い着氷性の霧雨', 'rainy'],
61: ['小雨', 'rainy'],
63: ['雨', 'rainy'],
65: ['大雨', 'rainy'],
66: ['弱い氷雨', 'rainy'],
67: ['強い氷雨', 'rainy'],
71: ['小雪', 'snowy'],
73: ['雪', 'snowy'],
75: ['大雪', 'snowy'],
77: ['霧雪', 'snowy'],
80: ['にわか雨', 'rainy'],
81: ['通り雨', 'rainy'],
82: ['集中豪雨', 'rainy'],
85: ['弱いにわか雪', 'snowy'],
86: ['強いにわか雪', 'snowy'],
95: ['雷雨', 'thunderstorm'],
96: ['霰を伴う雷雨', 'thunderstorm'],
99: ['雹を伴う雷雨', 'thunderstorm'],
}
// 天気アイコンの設定 { 気象カテゴリ: アイコン, ... }
const weatherIcons = {
sunny: <MdSunny color='orange' />,
cloudy: <MdCloud color='darkgray' />,
rainy: <IoRainy color='royalblue' />,
snowy: <IoSnowSharp color='skyblue' />,
foggy: <MdFoggy color='lightgray' />,
thunderstorm: <AiFillThunderbolt color='gold' />
}
// 気象名&アイコン表示コンポーネント
// export : 別ファイルから呼び出すための指定
export const WeatherNameWithIcon = ({ weatherCode }) => {
const [wName, wCategory] = weatherNames[weatherCode];
const wIcon = cloneElement(weatherIcons[wCategory], {
size: 25,
style: {
marginRight: '0.5em'
}
});
return (
<span style={{ display: 'flex', alignItems: 'center' }}>
{wIcon} {wName}
</span>
)
}
5-2. 気象名の表示 (5分)
それでは画面を作成していきます。
まずは下記を設置しましょう。
- タイトル
- 気象情報一覧のテーブル
...
const App = () => {
...
return (
{/* 下記削除 */}
<div style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(weatherInfo, null, 2)}</div>
{/* 下記追加 */}
<div>
<h1>{location.jpName} の天気</h1>
<div>
<table id='weather-table' border={1}>
<tbody>
{/* 取得した1時間おきの気象データを順番に表示します */}
{weatherInfo.map((info) => (
<tr key={info.datetime}>
<td>{info.datetime}</td>
<td>{info.weatherCode}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* ここまで */}
);
}
ここで先ほど設定した気象名を使用し、気象コードを気象名に変換して表示します。
import { useEffect, useState } from 'react';
import axios from 'axios';
/* 下記追加 */
import { WeatherNameWithIcon } from './weather_names';
/* ここまで */
...
const App = () => {
...
return (
<div>
<h1>{location.jpName} の天気</h1>
<div>
<table border={1}>
<tbody>
{weatherInfo.map((info) => (
<tr key={info.datetime}>
<td>{info.datetime}</td>
{/* 下記削除 */}
<td>{info.weatherCode}</td>
{/* 下記追加 */}
<td>
<WeatherNameWithIcon weatherCode={info.weatherCode} />
</td>
{/* ここまで */}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
ついでに日時情報も読みにくいので、表記を切り替えます。
事前にインストールしていた、date-fns
を使用しましょう。
Document |
---|
https://date-fns.org/ |
date-fnsは、日時データの操作に長けた便利なライブラリです。
今回はformat
関数をdate-fnsから読み込んで使いましょう。
import { useEffect, useState } from 'react';
import axios from 'axios';
/* 下記追加 */
import { format } from 'date-fns';
/* ここまで */
import { WeatherNameWithIcon } from './weather_names';
...
const App = () => {
...
return (
<div>
<h1>{location.jpName} の天気</h1>
<div>
<table id='weather-table' border={1}>
<tbody>
{weatherInfo.map((info) => (
<tr key={info.datetime}>
{/* 下記削除 */}
<td>{info.datetime}</td>
{/* 下記追加 */}
<td>
{format(new Date(info.datetime), 'MM/dd - HH:mm')}
</td>
{/* ここまで */}
<td>
<WeatherNameWithIcon weatherCode={info.weatherCode} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
関数名 | 説明 |
---|---|
format | 第1引数のDateオブジェクトを、 第2引数に指定したフォーマットで文字列に変換します。 |
date-fnsは日付の操作に便利です。
6. 表示できる地点を増やそう 【10分】
現在は東京の天気のみ表示可能ですが、表示できる地点を増やしてみましょう。
6-1. 地点選択機能 (3分)
最初に定義していた、locationList
に別の地点情報を追加します。
加えて地点の切り替えは画面上から行いたいので、地点を選択できるセレクトボックスを設置します。
...
const locationList = [
{ enName: 'tokyo', jpName: '東京', lat: 35.689, lon: 139.692 }, // 末尾にカンマを足す
/* 下記追加 */
{ enName: 'osaka', jpName: '大阪', lat: 34.686, lon: 135.520 },
{ enName: 'saga', jpName: '佐賀', lat: 33.249, lon: 130.300 }
/* ここまで */
];
const App = () => {
...
return (
<div>
{/* 下記追加 */}
<div>
<select id='location-select'>
{/* locationListの要素数分の選択肢を出します */}
{locationList.map((lo) => (
<option key={lo.enName} value={lo.enName}>{lo.jpName}</option>
))}
</select>
</div>
{/* ここまで */}
<h1>{location.jpName} の天気</h1>
...
6-2. 選択された地点ごとの気象データ取得 (5分)
地点が選択できるようになりましたが、現状では地点を切り替えても何も起こりません。
「地点を切り替えたら〇〇するよ!」という設定がされていないためですね。
次に必要な設定は、「セレクトボックスで地点を選択すると、新しい地点の気象データを取得してweatherInfo
に格納する」という処理になります。
「ブラウザ側で選択されている地点を切り替える」という処理を行いたいです。
このような場合は...アレですね!
先ほど使用したuseState
をここでも使用しましょう。
...
const App = () => {
/* 下記削除 */
const location = locationList[0];
/* 下記追加 */
const [location, setLocation] = useState(locationList[0]);
/* ここまで */
const [weatherInfo, setWeatherInfo] = useState([]);
/* 下記追加 */
// 地点選択時のアクション
const onChangeLocation = (event) => {
const currentLocationData = locationList.find((lo) => event.target.value === lo.enName);
setLocation(currentLocationData);
}
/* ここまで */
// 気象情報APIコールを実行
useEffect(() => {
...
}, [location]) // locationを第2引数に追加(location更新時にuseEffectを再実行)
return (
<div>
<div>
{/* selectが変更された際にonChangeLocation関数を作動させます */}
<select id='location-select' onChange={onChangeLocation}>
{locationList.map((lo) => (
<option key={lo.enName} value={lo.enName}>{lo.jpName}</option>
))}
</select>
</div>
<h1>{location.jpName} の天気</h1>
...
これで様々な地点の天気情報が表示できましたね。
アプリケーションの要件として定めた機能が一通り完成となります。
useEffectの第2引数である配列に変数を与えると、その変数が更新された際にuseEffect内の処理を再実行します。(今回の場合はlocation
=地点が切り替わると、気象データの取得処理が再度行われます)
7. デザイン設定 【5分】
最後に、作成したアプリケーションのデザインを調整しましょう。
今回の講義では、デザイン面であるCSSの構文等は詳しく触れません。
興味があればご自身で調べてみてください。
7-1. クラス名を指定しCSS適用 (3分)
まずはreact側にcssの設定ポイントとなるクラス名を指定します
...
return (
<div>
<div className='center-item'> {/* className追加 */}
<select id='location-select' onChange={onChangeLocation}>
{locationList.map((lo) => (
<option key={lo.enName} value={lo.enName}>{lo.jpName}</option>
))}
</select>
</div>
<h1 className='center-item'>{location.jpName} の天気</h1> {/* className追加 */}
<div className='center-item'> {/* className追加 */}
<table id='weather-table' border={1} >
<tbody>
{weatherInfo.map((info) => (
<tr key={info.datetime}>
<td>
{format(new Date(info.datetime), 'MM/dd - HH:mm')}
</td>
<td>
<WeatherNameWithIcon weatherCode={info.weatherCode} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
続いてcssファイルにcssを記述します。
コピペでOK
$ vi src/index.css
# vscodeは下記でもOK
$ code src/index.css
/*
元の設定はすべて削除
*/
body {
margin: 0;
padding: 2em;
font-family: 'sans-serif';
background-color: lightcyan;
}
td {
padding: 0.5em;
}
.center-item {
margin-bottom: 1em;
display: flex;
justify-content: center;
}
#location-select {
padding: 0.5em 1em;
width: 200px;
background-color: lemonchiffon;
border: 2px solid orange;
border-radius: 10px;
}
#weather-table {
background-color: white;
}
色味やレイアウトが変わって、見やすく使いやすくなりましたね。
これで今回のアプリケーションは完成です。
お疲れさまでした。
最後に
下記にて今回のアプリケーションを機能拡張しています。
本資料より発展的な内容で、ちょっと難しいかもですが、
よかったら取り組んでみてください。