第10回:JavaScript応用とAPI
🎯 学習目標
- 配列とオブジェクトの高度な操作をマスターする
- 非同期処理(Promise、async/await)を理解する
- fetch APIを使ってAPI連携ができる
- JSONデータを適切に扱える
📚 導入(5分)
配列・オブジェクト操作の重要性
現代のWebアプリケーションでは、データの操作が中心的な役割を果たします:
- API からのデータ処理:サーバーから取得したデータの加工
- ユーザー入力の管理:フォームデータの検証と変換
- リアルタイムデータ更新:画面の動的更新
外部データとの連携について
// 従来の方法(同期的)
const data = getDataFromServer(); // ブロッキング
console.log(data);
// 現代的な方法(非同期的)
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data)); // ノンブロッキング💡 理論学習(30分)
配列とオブジェクトの高度な操作(15分)
配列の高度なメソッド
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const users = [
{ id: 1, name: '田中', age: 25, city: '東京', salary: 400000 },
{ id: 2, name: '佐藤', age: 30, city: '大阪', salary: 500000 },
{ id: 3, name: '鈴木', age: 28, city: '東京', salary: 450000 },
{ id: 4, name: '高橋', age: 35, city: '名古屋', salary: 600000 },
{ id: 5, name: '田中', age: 22, city: '福岡', salary: 350000 }
];
// map: 各要素を変換(新しい配列を返す)
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
const userNames = users.map(user => user.name);
console.log(userNames); // ['田中', '佐藤', '鈴木', '高橋', '田中']
// より複雑な変換
const userSummary = users.map(user => ({
id: user.id,
name: user.name,
profile: `${user.name}さん(${user.age}歳、${user.city}在住)`,
salaryLevel: user.salary >= 500000 ? 'high' : user.salary >= 400000 ? 'medium' : 'low'
}));
// filter: 条件に合う要素を抽出
const evenNumbers = numbers.filter(n => n % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]
const tokyoUsers = users.filter(user => user.city === '東京');
const seniorUsers = users.filter(user => user.age >= 30);
const highSalaryUsers = users.filter(user => user.salary >= 450000);
// 複数条件
const tokyoSeniors = users.filter(user =>
user.city === '東京' && user.age >= 25
);
// reduce: 要素を集約(最も強力なメソッド)
const sum = numbers.reduce((acc, n) => acc + n, 0);
console.log(sum); // 55
const totalSalary = users.reduce((total, user) => total + user.salary, 0);
console.log(totalSalary); // 2300000
// より複雑な集約:都市別のユーザー数
const usersByCity = users.reduce((acc, user) => {
acc[user.city] = (acc[user.city] || 0) + 1;
return acc;
}, {});
console.log(usersByCity); // { '東京': 2, '大阪': 1, '名古屋': 1, '福岡': 1 }
// 年代別の平均給与
const averageSalaryByAgeGroup = users.reduce((acc, user) => {
const ageGroup = Math.floor(user.age / 10) * 10; // 20代、30代など
const key = `${ageGroup}代`;
if (!acc[key]) {
acc[key] = { total: 0, count: 0 };
}
acc[key].total += user.salary;
acc[key].count += 1;
acc[key].average = Math.round(acc[key].total / acc[key].count);
return acc;
}, {});
// find: 条件に合う最初の要素
const firstTokyoUser = users.find(user => user.city === '東京');
console.log(firstTokyoUser); // { id: 1, name: '田中', ... }
const userById = users.find(user => user.id === 3);
// findIndex: 条件に合う最初の要素のインデックス
const firstHighSalaryIndex = users.findIndex(user => user.salary >= 500000);
console.log(firstHighSalaryIndex); // 1
// some: 条件に合う要素が1つでもあるか
const hasTokyoUser = users.some(user => user.city === '東京');
console.log(hasTokyoUser); // true
const hasMinor = users.some(user => user.age < 20);
console.log(hasMinor); // false
// every: すべての要素が条件に合うか
const allAdults = users.every(user => user.age >= 20);
console.log(allAdults); // true
const allHighSalary = users.every(user => user.salary >= 500000);
console.log(allHighSalary); // false
// sort: 配列のソート(元の配列を変更)
const sortedByAge = [...users].sort((a, b) => a.age - b.age);
const sortedBySalary = [...users].sort((a, b) => b.salary - a.salary); // 降順
const sortedByName = [...users].sort((a, b) => a.name.localeCompare(b.name));
// includes: 要素が含まれるかチェック
const cities = users.map(user => user.city);
const uniqueCities = [...new Set(cities)]; // 重複除去
console.log(uniqueCities); // ['東京', '大阪', '名古屋', '福岡']
// メソッドチェーン(関数型プログラミングの活用)
const tokyoUsersHighSalary = users
.filter(user => user.city === '東京')
.filter(user => user.salary >= 400000)
.map(user => ({
name: user.name,
salary: user.salary,
displaySalary: `¥${user.salary.toLocaleString()}`
}))
.sort((a, b) => b.salary - a.salary);
console.log(tokyoUsersHighSalary);オブジェクトの高度な操作
// オブジェクトの分割代入
const user = { id: 1, name: '田中', age: 25, city: '東京', salary: 400000 };
const { name, age, city } = user;
console.log(name, age, city); // '田中' 25 '東京'
// 別名での分割代入
const { name: userName, age: userAge } = user;
// デフォルト値
const { name, age, department = '未設定' } = user;
// ネストしたオブジェクトの分割代入
const userDetail = {
id: 1,
name: '田中',
address: {
prefecture: '東京都',
city: '渋谷区',
zipCode: '150-0001'
},
hobbies: ['読書', '映画鑑賞']
};
const {
name: fullName,
address: { prefecture, city: cityName },
hobbies: [hobby1, hobby2]
} = userDetail;
// スプレッド演算子によるオブジェクト結合
const baseUser = { id: 1, name: '田中' };
const userExtension = { age: 25, city: '東京' };
const settings = { theme: 'dark', language: 'ja' };
const fullUser = { ...baseUser, ...userExtension, ...settings };
console.log(fullUser);
// オブジェクトのコピー(浅いコピー)
const userCopy = { ...user };
const userCopy2 = Object.assign({}, user);
// プロパティの動的アクセス
const propertyName = 'salary';
console.log(user[propertyName]); // 400000
// オブジェクトのキー・値・エントリーの取得
const keys = Object.keys(user); // ['id', 'name', 'age', 'city', 'salary']
const values = Object.values(user); // [1, '田中', 25, '東京', 400000]
const entries = Object.entries(user); // [['id', 1], ['name', '田中'], ...]
// オブジェクトの変換
const userWithUppercaseKeys = Object.fromEntries(
Object.entries(user).map(([key, value]) => [key.toUpperCase(), value])
);
// オブジェクトのフィルタリング
const numericProperties = Object.fromEntries(
Object.entries(user).filter(([key, value]) => typeof value === 'number')
);非同期処理とAPI(15分)
非同期処理の基礎
// 同期処理(ブロッキング)
console.log('開始');
// 重い処理(仮想的)
for (let i = 0; i < 1000000000; i++) {} // UIがフリーズ
console.log('終了');
// 非同期処理(ノンブロッキング)
console.log('開始');
setTimeout(() => {
console.log('2秒後に実行');
}, 2000);
console.log('終了'); // すぐに実行されるPromiseの基礎
// Promiseの作成
const myPromise = new Promise((resolve, reject) => {
// 非同期処理をシミュレート
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('成功しました!');
} else {
reject(new Error('失敗しました'));
}
}, 1000);
});
// Promiseの使用
myPromise
.then(result => {
console.log('成功:', result);
return result.toUpperCase(); // 次のthenに渡される
})
.then(upperResult => {
console.log('大文字:', upperResult);
})
.catch(error => {
console.error('エラー:', error.message);
})
.finally(() => {
console.log('処理完了');
});
// 複数のPromiseの処理
const promise1 = fetch('/api/user/1').then(r => r.json());
const promise2 = fetch('/api/user/2').then(r => r.json());
const promise3 = fetch('/api/user/3').then(r => r.json());
// すべて完了を待つ
Promise.all([promise1, promise2, promise3])
.then(users => {
console.log('すべてのユーザー:', users);
})
.catch(error => {
console.error('いずれかが失敗:', error);
});
// 最初に完了したものを取得
Promise.race([promise1, promise2, promise3])
.then(firstUser => {
console.log('最初に取得:', firstUser);
});
// すべて完了を待つ(一部失敗しても継続)
Promise.allSettled([promise1, promise2, promise3])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`ユーザー${index + 1}:`, result.value);
} else {
console.error(`ユーザー${index + 1}エラー:`, result.reason);
}
});
});async/awaitを使った書き方
// Promiseチェーンでの書き方
function getUserData() {
return fetch('/api/user')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(user => {
return fetch(`/api/user/${user.id}/posts`);
})
.then(response => response.json())
.then(posts => {
return { user, posts };
})
.catch(error => {
console.error('Error:', error);
throw error;
});
}
// async/awaitでの書き方(推奨)
async function getUserData() {
try {
// ユーザー情報を取得
const userResponse = await fetch('/api/user');
if (!userResponse.ok) {
throw new Error('ユーザー情報の取得に失敗しました');
}
const user = await userResponse.json();
// ユーザーの投稿を取得
const postsResponse = await fetch(`/api/user/${user.id}/posts`);
if (!postsResponse.ok) {
throw new Error('投稿の取得に失敗しました');
}
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error('エラー:', error.message);
throw error;
}
}
// 並行処理
async function getMultipleUsers() {
try {
// 並行して実行
const [user1, user2, user3] = await Promise.all([
fetch('/api/user/1').then(r => r.json()),
fetch('/api/user/2').then(r => r.json()),
fetch('/api/user/3').then(r => r.json())
]);
return [user1, user2, user3];
} catch (error) {
console.error('ユーザー取得エラー:', error);
throw error;
}
}fetch APIの基本的な使用方法
// GETリクエスト
async function fetchData() {
try {
const response = await fetch('/api/data');
// レスポンスの状態チェック
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Content-Typeに応じた処理
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
return data;
} else {
const text = await response.text();
return text;
}
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// POSTリクエスト
async function postData(data) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Post error:', error);
throw error;
}
}
// PUTリクエスト(更新)
async function updateUser(id, userData) {
try {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(userData)
});
return await response.json();
} catch (error) {
console.error('Update error:', error);
throw error;
}
}
// DELETEリクエスト
async function deleteUser(id) {
try {
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
return { success: true };
} else {
throw new Error('削除に失敗しました');
}
} catch (error) {
console.error('Delete error:', error);
throw error;
}
}JSONデータの扱い
// オブジェクト → JSON文字列
const user = {
name: "田中太郎",
age: 25,
city: "東京",
hobbies: ["読書", "映画鑑賞"]
};
const jsonString = JSON.stringify(user);
console.log(jsonString);
// '{"name":"田中太郎","age":25,"city":"東京","hobbies":["読書","映画鑑賞"]}'
// インデントを付けて見やすく
const prettyJson = JSON.stringify(user, null, 2);
// 特定のプロパティのみをシリアライズ
const limitedJson = JSON.stringify(user, ['name', 'age']);
// JSON文字列 → オブジェクト
const parsedUser = JSON.parse(jsonString);
console.log(parsedUser);
// エラーハンドリング付きパース
function safeJsonParse(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error('JSON parse error:', error);
return null;
}
}
// LocalStorageでのJSON保存
function saveToStorage(key, data) {
localStorage.setItem(key, JSON.stringify(data));
}
function loadFromStorage(key) {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
}🛠️ 実習(50分)
天気情報アプリ作成(45分)
HTML構造
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天気情報アプリ</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<header class="header">
<h1 class="title">🌤️ 天気情報アプリ</h1>
</header>
<main class="main">
<!-- 都市検索 -->
<section class="search-section">
<div class="search-form">
<input
type="text"
id="cityInput"
placeholder="都市名を入力(例:Tokyo, London)"
autocomplete="off"
>
<button id="searchBtn">検索</button>
</div>
<div class="quick-cities">
<button class="city-btn" data-city="Tokyo">東京</button>
<button class="city-btn" data-city="Osaka">大阪</button>
<button class="city-btn" data-city="London">ロンドン</button>
<button class="city-btn" data-city="New York">ニューヨーク</button>
</div>
</section>
<!-- ローディング -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p>天気情報を取得中...</p>
</div>
<!-- エラー表示 -->
<div class="error" id="error">
<div class="error-icon">❌</div>
<h3>エラーが発生しました</h3>
<p id="errorMessage"></p>
<button id="retryBtn">再試行</button>
</div>
<!-- 天気情報表示 -->
<div class="weather-info" id="weatherInfo">
<div class="current-weather">
<div class="city-name" id="cityName"></div>
<div class="weather-icon" id="weatherIcon"></div>
<div class="temperature" id="temperature"></div>
<div class="description" id="description"></div>
</div>
<div class="weather-details">
<div class="detail-item">
<span class="detail-label">体感温度</span>
<span class="detail-value" id="feelsLike"></span>
</div>
<div class="detail-item">
<span class="detail-label">湿度</span>
<span class="detail-value" id="humidity"></span>
</div>
<div class="detail-item">
<span class="detail-label">風速</span>
<span class="detail-value" id="windSpeed"></span>
</div>
<div class="detail-item">
<span class="detail-label">気圧</span>
<span class="detail-value" id="pressure"></span>
</div>
<div class="detail-item">
<span class="detail-label">可視距離</span>
<span class="detail-value" id="visibility"></span>
</div>
<div class="detail-item">
<span class="detail-label">UV指数</span>
<span class="detail-value" id="uvIndex"></span>
</div>
</div>
</div>
<!-- お気に入り都市 -->
<section class="favorites-section">
<h2>お気に入り都市</h2>
<div class="favorites-list" id="favoritesList">
<!-- 動的に生成 -->
</div>
<button id="addFavoriteBtn" class="add-favorite-btn">現在の都市をお気に入りに追加</button>
</section>
<!-- 履歴 -->
<section class="history-section">
<h2>検索履歴</h2>
<div class="history-list" id="historyList">
<!-- 動的に生成 -->
</div>
<button id="clearHistoryBtn" class="clear-history-btn">履歴をクリア</button>
</section>
</main>
</div>
<script src="js/script.js"></script>
</body>
</html>CSS(css/style.css)
/* 基本設定 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #74b9ff, #0984e3);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* ヘッダー */
.header {
text-align: center;
margin-bottom: 30px;
}
.title {
color: white;
font-size: 2.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
margin-bottom: 10px;
}
/* 検索セクション */
.search-section {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.search-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#cityInput {
flex: 1;
padding: 15px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s ease;
}
#cityInput:focus {
outline: none;
border-color: #74b9ff;
}
#searchBtn {
padding: 15px 25px;
background: linear-gradient(135deg, #74b9ff, #0984e3);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: bold;
transition: transform 0.3s ease;
}
#searchBtn:hover {
transform: translateY(-2px);
}
.quick-cities {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.city-btn {
padding: 8px 16px;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.city-btn:hover {
background: #74b9ff;
color: white;
border-color: #74b9ff;
}
/* ローディング */
.loading {
background: white;
padding: 40px;
border-radius: 15px;
text-align: center;
display: none;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.loading.show {
display: block;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #74b9ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* エラー表示 */
.error {
background: white;
padding: 40px;
border-radius: 15px;
text-align: center;
display: none;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.error.show {
display: block;
}
.error-icon {
font-size: 3rem;
margin-bottom: 20px;
}
.error h3 {
color: #e17055;
margin-bottom: 15px;
}
#retryBtn {
padding: 10px 20px;
background: #e17055;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 15px;
}
/* 天気情報 */
.weather-info {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 30px;
display: none;
}
.weather-info.show {
display: block;
}
.current-weather {
background: linear-gradient(135deg, #74b9ff, #0984e3);
color: white;
padding: 40px;
text-align: center;
}
.city-name {
font-size: 2rem;
font-weight: bold;
margin-bottom: 20px;
}
.weather-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.temperature {
font-size: 3rem;
font-weight: bold;
margin-bottom: 10px;
}
.description {
font-size: 1.2rem;
opacity: 0.9;
text-transform: capitalize;
}
.weather-details {
padding: 30px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.detail-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.detail-label {
font-size: 0.9rem;
color: #666;
margin-bottom: 5px;
}
.detail-value {
font-size: 1.1rem;
font-weight: bold;
color: #333;
}
/* お気に入り・履歴 */
.favorites-section,
.history-section {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.favorites-section h2,
.history-section h2 {
margin-bottom: 20px;
color: #333;
}
.favorites-list,
.history-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.favorite-item,
.history-item {
background: #74b9ff;
color: white;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.favorite-item:hover,
.history-item:hover {
background: #0984e3;
transform: translateY(-2px);
}
.remove-favorite {
background: rgba(255, 255, 255, 0.3);
border: none;
color: white;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.add-favorite-btn,
.clear-history-btn {
width: 100%;
padding: 12px;
border: 2px dashed #74b9ff;
background: transparent;
color: #74b9ff;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.add-favorite-btn:hover,
.clear-history-btn:hover {
background: #74b9ff;
color: white;
}
.add-favorite-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* レスポンシブ */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.search-form {
flex-direction: column;
}
.weather-details {
grid-template-columns: 1fr;
gap: 15px;
}
.city-name {
font-size: 1.5rem;
}
.temperature {
font-size: 2.5rem;
}
}JavaScript(js/script.js)
// OpenWeatherMap API設定(実際には環境変数を使用)
const API_KEY = 'your-api-key-here'; // 実際のAPIキーに置き換え
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';
// DOM要素
const cityInput = document.getElementById('cityInput');
const searchBtn = document.getElementById('searchBtn');
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const weatherInfo = document.getElementById('weatherInfo');
const favoritesList = document.getElementById('favoritesList');
const historyList = document.getElementById('historyList');
// 状態管理
let currentWeatherData = null;
let favorites = JSON.parse(localStorage.getItem('weatherFavorites')) || [];
let searchHistory = JSON.parse(localStorage.getItem('weatherHistory')) || [];
// 天気アイコンマッピング
const weatherIcons = {
'01d': '☀️', '01n': '🌙',
'02d': '⛅', '02n': '⛅',
'03d': '☁️', '03n': '☁️',
'04d': '☁️', '04n': '☁️',
'09d': '🌧️', '09n': '🌧️',
'10d': '🌦️', '10n': '🌦️',
'11d': '⛈️', '11n': '⛈️',
'13d': '🌨️', '13n': '🌨️',
'50d': '🌫️', '50n': '🌫️'
};
// 初期化
document.addEventListener('DOMContentLoaded', function() {
updateFavoritesDisplay();
updateHistoryDisplay();
// デフォルトで東京の天気を表示
searchWeather('Tokyo');
});
// 天気情報取得
async function searchWeather(city) {
if (!city.trim()) return;
showLoading();
hideError();
hideWeatherInfo();
try {
const weatherData = await fetchWeatherData(city);
displayWeatherData(weatherData);
addToHistory(city);
currentWeatherData = weatherData;
} catch (error) {
showError(error.message);
console.error('Weather fetch error:', error);
}
}
// API呼び出し
async function fetchWeatherData(city) {
const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric&lang=ja`;
// デモ用のモックデータ(実際のAPIが利用できない場合)
if (API_KEY === 'your-api-key-here') {
return getMockWeatherData(city);
}
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error('指定された都市が見つかりません');
} else if (response.status === 401) {
throw new Error('APIキーが無効です');
} else {
throw new Error(`サーバーエラー: ${response.status}`);
}
}
const data = await response.json();
return data;
}
// モックデータ(デモ用)
function getMockWeatherData(city) {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
name: city,
main: {
temp: Math.round(Math.random() * 30 + 5),
feels_like: Math.round(Math.random() * 30 + 5),
humidity: Math.round(Math.random() * 100),
pressure: Math.round(Math.random() * 100 + 1000)
},
weather: [{
main: 'Clear',
description: '晴れ',
icon: '01d'
}],
wind: {
speed: Math.round(Math.random() * 10 + 1)
},
visibility: Math.round(Math.random() * 10000 + 5000),
sys: {
country: 'JP'
}
};
resolve(mockData);
}, 1000);
});
}
// 天気情報表示
function displayWeatherData(data) {
document.getElementById('cityName').textContent = `${data.name}, ${data.sys.country}`;
document.getElementById('weatherIcon').textContent = weatherIcons[data.weather[0].icon] || '🌤️';
document.getElementById('temperature').textContent = `${Math.round(data.main.temp)}°C`;
document.getElementById('description').textContent = data.weather[0].description;
document.getElementById('feelsLike').textContent = `${Math.round(data.main.feels_like)}°C`;
document.getElementById('humidity').textContent = `${data.main.humidity}%`;
document.getElementById('windSpeed').textContent = `${data.wind.speed} m/s`;
document.getElementById('pressure').textContent = `${data.main.pressure} hPa`;
document.getElementById('visibility').textContent = `${(data.visibility / 1000).toFixed(1)} km`;
document.getElementById('uvIndex').textContent = 'N/A';
showWeatherInfo();
hideLoading();
}
// 履歴追加
function addToHistory(city) {
// 重複を避ける
const existingIndex = searchHistory.findIndex(item =>
item.city.toLowerCase() === city.toLowerCase()
);
if (existingIndex !== -1) {
searchHistory.splice(existingIndex, 1);
}
searchHistory.unshift({
city: city,
timestamp: new Date().toISOString()
});
// 履歴を10件に制限
searchHistory = searchHistory.slice(0, 10);
localStorage.setItem('weatherHistory', JSON.stringify(searchHistory));
updateHistoryDisplay();
}
// お気に入り追加
function addToFavorites(city) {
if (!city) return;
// 重複チェック
const exists = favorites.some(fav =>
fav.city.toLowerCase() === city.toLowerCase()
);
if (exists) {
alert('この都市は既にお気に入りに追加されています');
return;
}
favorites.push({
city: city,
addedAt: new Date().toISOString()
});
localStorage.setItem('weatherFavorites', JSON.stringify(favorites));
updateFavoritesDisplay();
}
// お気に入り削除
function removeFromFavorites(city) {
favorites = favorites.filter(fav =>
fav.city.toLowerCase() !== city.toLowerCase()
);
localStorage.setItem('weatherFavorites', JSON.stringify(favorites));
updateFavoritesDisplay();
}
// お気に入り表示更新
function updateFavoritesDisplay() {
if (favorites.length === 0) {
favoritesList.innerHTML = '<p style="color: #666; text-align: center;">お気に入りの都市がありません</p>';
return;
}
favoritesList.innerHTML = favorites.map(fav => `
<div class="favorite-item" onclick="searchWeather('${fav.city}')">
${fav.city}
<button class="remove-favorite" onclick="event.stopPropagation(); removeFromFavorites('${fav.city}')">
×
</button>
</div>
`).join('');
}
// 履歴表示更新
function updateHistoryDisplay() {
if (searchHistory.length === 0) {
historyList.innerHTML = '<p style="color: #666; text-align: center;">検索履歴がありません</p>';
return;
}
historyList.innerHTML = searchHistory.map(item => `
<div class="history-item" onclick="searchWeather('${item.city}')">
${item.city}
<small style="opacity: 0.7;">${formatDate(item.timestamp)}</small>
</div>
`).join('');
}
// 日付フォーマット
function formatDate(isoString) {
const date = new Date(isoString);
const now = new Date();
const diffMs = now - date;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours < 1) return '今';
if (diffHours < 24) return `${diffHours}時間前`;
return `${Math.floor(diffHours / 24)}日前`;
}
// UI制御関数
function showLoading() {
loading.classList.add('show');
}
function hideLoading() {
loading.classList.remove('show');
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
error.classList.add('show');
}
function hideError() {
error.classList.remove('show');
}
function showWeatherInfo() {
weatherInfo.classList.add('show');
document.getElementById('addFavoriteBtn').disabled = false;
}
function hideWeatherInfo() {
weatherInfo.classList.remove('show');
document.getElementById('addFavoriteBtn').disabled = true;
}
// イベントリスナー
searchBtn.addEventListener('click', () => {
searchWeather(cityInput.value);
});
cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchWeather(cityInput.value);
}
});
// クイック都市ボタン
document.querySelectorAll('.city-btn').forEach(btn => {
btn.addEventListener('click', () => {
searchWeather(btn.dataset.city);
});
});
// お気に入り追加ボタン
document.getElementById('addFavoriteBtn').addEventListener('click', () => {
if (currentWeatherData) {
addToFavorites(currentWeatherData.name);
}
});
// 履歴クリアボタン
document.getElementById('clearHistoryBtn').addEventListener('click', () => {
if (confirm('検索履歴をすべて削除しますか?')) {
searchHistory = [];
localStorage.removeItem('weatherHistory');
updateHistoryDisplay();
}
});
// 再試行ボタン
document.getElementById('retryBtn').addEventListener('click', () => {
const lastCity = cityInput.value || 'Tokyo';
searchWeather(lastCity);
});
// 設定の保存・復元
function saveSettings() {
const settings = {
lastSearchedCity: currentWeatherData?.name || 'Tokyo',
theme: document.body.classList.contains('dark-theme') ? 'dark' : 'light'
};
localStorage.setItem('weatherSettings', JSON.stringify(settings));
}
function loadSettings() {
const settings = JSON.parse(localStorage.getItem('weatherSettings'));
if (settings) {
// テーマ設定などの復元
if (settings.theme === 'dark') {
document.body.classList.add('dark-theme');
}
}
}
// ページ離脱時に設定を保存
window.addEventListener('beforeunload', saveSettings);
// 定期的なデータ更新(5分ごと)
setInterval(() => {
if (currentWeatherData) {
searchWeather(currentWeatherData.name);
}
}, 5 * 60 * 1000);
// エラーハンドリング
window.addEventListener('error', (e) => {
console.error('Global error:', e.error);
showError('予期しないエラーが発生しました');
});
// Promise rejection handling
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled promise rejection:', e.reason);
showError('通信エラーが発生しました');
e.preventDefault();
});📝 まとめ・質疑応答(5分)
非同期処理の重要性確認
✅ 習得したスキル
- 配列の高度な操作(map, filter, reduce等)
- オブジェクトの分割代入とスプレッド演算子
- Promise と async/await の理解
- fetch API を使ったHTTP通信
- JSON データの適切な処理
- エラーハンドリングの実装
次回予告:総合制作企画
次回から総合制作に入ります:
- これまでの学習内容の統合
- オリジナルWebアプリケーションの企画
- 技術選択と設計方針の決定
🏠 宿題
-
APIを使った別のアプリ作成
- ニュースAPI、映画API等の活用
- データの表示・フィルタリング機能
-
配列操作の練習問題
// 売上データから分析情報を抽出 const salesData = [ { date: '2024-01-01', product: 'A', amount: 1000 }, { date: '2024-01-02', product: 'B', amount: 1500 }, // ... ]; // 月別売上合計、商品別売上ランキング等を算出 -
LocalStorage を使ったデータ永続化
- 設定の保存・復元
- オフライン対応
📚 参考リソース
次回もお楽しみに! 🌐
Last updated on