Skip to Content
Lecture第10回:JavaScript応用とAPI

第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アプリケーションの企画
  • 技術選択と設計方針の決定

🏠 宿題

  1. APIを使った別のアプリ作成

    • ニュースAPI、映画API等の活用
    • データの表示・フィルタリング機能
  2. 配列操作の練習問題

    // 売上データから分析情報を抽出 const salesData = [ { date: '2024-01-01', product: 'A', amount: 1000 }, { date: '2024-01-02', product: 'B', amount: 1500 }, // ... ]; // 月別売上合計、商品別売上ランキング等を算出
  3. LocalStorage を使ったデータ永続化

    • 設定の保存・復元
    • オフライン対応

📚 参考リソース


次回もお楽しみに! 🌐

Last updated on