# 後端體驗營 Day-2 教材 ## 認識 API API(Application Programming Interface)是一種讓「不同系統」彼此溝通的方式。 在後端開發中,最常見的是 **RESTful API**: * **GET**:取得資料 * **POST**:新增資料 * **PATCH**:更新資料 * **DELETE**:刪除資料 --- ## 初探 Spring Boot Spring Boot 可以讓我們快速建立後端專案,不用自己設定一堆複雜的設定。 只要建立一個專案,寫上 Controller,就能跑出 API。 啟動後,你就可以透過 `http://localhost:8080` 測試 API。 --- ## 建立&開啟專案 進入[Spring Initializr](https://start.spring.io/) 依照下圖會得到一個壓縮檔,解壓縮後將demo資料夾用IDEA開啟。 ![Screenshot 2025-09-15 112256](https://hackmd.io/_uploads/SJxxwWrilg.png) (目前可能最高版本為Java 25,但為了避免新版本的衝突,選擇Java 21就好) ### 資料夾結構 開啟後的資料夾結構會如下圖 開啟DemoApplication.java會出現黃色提示,點Setup JDK並選擇版本(要選與專案相同的版本,就是Spring Initializr的Java版本) ![Screenshot 2025-09-15 112649](https://hackmd.io/_uploads/H1VcwWHoxe.png) > 如果沒有版本可以選的話,點Download JDK並選擇21.x開頭的Download就好!!! > 下載完後點開最右方的Gradle大象並按重整 > ![Screenshot 2025-09-15 113050](https://hackmd.io/_uploads/BJIYOWHoeg.png) --- ## 實作 API 我們這裡要做 **註冊 / 登入 API**: 1. **註冊 API**:使用者傳送帳號、密碼 → 後端儲存資料庫 2. **登入 API**:使用者傳送帳號、密碼 → 後端檢查是否正確 → 回傳登入成功或失敗 由於不連結資料庫,我們就用 `List` 來暫存使用者。 --- ## 完整程式碼 建立一個簡單的 Spring Boot 專案,在與`DemoApplication`同層資料夾下新增: ### `User` ```java public class User { private String username; private String password; public User() {} public User(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } ``` --- ### `UserController` (Controller) ```java import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; @RestController @RequestMapping("/user") @CrossOrigin public class UserController { // 暫存使用者 (假裝是資料庫) private List<User> users = new ArrayList<>(); // 註冊 API @PostMapping("/register") public ResponseEntity<String> register(@RequestBody User user) { // 檢查帳號是否已存在 for (int i = 0; i < users.size(); i++) { if (users.get(i).getUsername().equals(user.getUsername())) { return ResponseEntity.badRequest().body("帳號已存在"); } } // 加入使用者 users.add(user); return ResponseEntity.ok("註冊成功"); } // 登入 API @PostMapping("/login") public ResponseEntity<String> login(@RequestBody User user) { for (int i = 0; i < users.size(); i++) { User userInDb = users.get(i); if (userInDb.getUsername().equals(user.getUsername()) && userInDb.getPassword().equals(user.getPassword())) { return ResponseEntity.ok("登入成功,歡迎 " + userInDb.getUsername()); } } return ResponseEntity.status(401).body("帳號或密碼錯誤"); } } ``` --- ### `DemoApplication` (啟動程式) ```java package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` --- ## 測試方式 啟動專案後,可以用 **Postman** 或其他 **http client** 測試: 1. 在API URL旁邊有一個地球,點下去後再選擇**Generate request in HTTP Client** ![Screenshot 2025-09-15 110649](https://hackmd.io/_uploads/B1LS7-rseg.png) 2. 接著會跑出一個視窗,會有一個下拉可以選擇你的端口,選擇第一個就好直接按Enter ![Screenshot 2025-09-15 110936](https://hackmd.io/_uploads/By4iX-Hjel.png) 3. 綠色箭頭是發送請求的按鈕,用`{}`包起來的是`請求體`,就是你要傳入的帳號密碼,將值輸入在`""`裡面就可以了 ![Screenshot 2025-09-15 111254](https://hackmd.io/_uploads/r1CqE-Hoxe.png) 4. 發送成功的結果 ![Screenshot 2025-09-15 111612](https://hackmd.io/_uploads/SytQSbBoeg.png) 5. 接著再試試看登入吧! ![Screenshot 2025-09-15 111728](https://hackmd.io/_uploads/H1rDSWBolx.png) *** 使用已經寫好的前端測試 在resources/static下新增`index.html`,並複製以下程式碼 透過下圖方式開啟網頁 ![image](https://hackmd.io/_uploads/SkrU5WHsee.png) <details> <summary>前端程式碼</summary> ```html <!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Instagram</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: #fafafa; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; } .main-container { display: flex; max-width: 935px; width: 100%; align-items: center; gap: 32px; } .phone-mockup { flex: 1; max-width: 380px; position: relative; display: none; } .phone-image { width: 100%; height: 580px; background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 580"><rect width="380" height="580" fill="%23000" rx="40"/><rect x="20" y="40" width="340" height="500" fill="%23fff" rx="15"/><rect x="40" y="60" width="300" height="40" fill="%23f7f7f7" rx="20"/><rect x="40" y="120" width="300" height="300" fill="%23e1306c" rx="10"/><circle cx="320" cy="80" r="15" fill="%23e1306c"/><rect x="40" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/><rect x="120" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/><rect x="200" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/><rect x="280" y="440" width="60" height="60" fill="%23f7f7f7" rx="30"/></svg>') no-repeat center; background-size: contain; } .form-container { flex: 1; max-width: 350px; } .form-box { background: white; border: 1px solid #dbdbdb; border-radius: 1px; padding: 40px 40px 20px; margin-bottom: 10px; } .instagram-logo { text-align: center; margin-bottom: 32px; } .instagram-logo h1 { font-family: 'Billabong', cursive; font-size: 51px; font-weight: normal; color: #262626; margin: 0; text-decoration: none; background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .form-group { margin-bottom: 6px; } .form-group input { width: 100%; padding: 9px 0 7px 8px; border: 1px solid #dbdbdb; border-radius: 3px; background: #fafafa; font-size: 12px; color: #262626; outline: none; transition: border-color 0.2s ease; } .form-group input:focus { border-color: #a8a8a8; background: white; } .form-group input::placeholder { color: #8e8e8e; } .submit-btn { width: 100%; padding: 5px 9px; background: #0095f6; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 600; cursor: pointer; margin: 8px 0; height: 32px; transition: background-color 0.2s ease; position: relative; overflow: hidden; } .submit-btn:hover { background: #1877f2; } .submit-btn:disabled { background: #b2dffc; cursor: not-allowed; } .submit-btn.loading::after { content: ""; position: absolute; width: 16px; height: 16px; margin: auto; border: 2px solid transparent; border-top-color: #ffffff; border-radius: 50%; animation: spin 1s linear infinite; top: 50%; left: 50%; transform: translate(-50%, -50%); } .submit-btn.loading .btn-text { opacity: 0; } @keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } .divider { display: flex; align-items: center; margin: 20px 0; color: #8e8e8e; font-size: 13px; font-weight: 600; } .divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #dbdbdb; } .divider span { padding: 0 18px; } .facebook-login { display: flex; align-items: center; justify-content: center; color: #385185; font-size: 14px; font-weight: 600; text-decoration: none; margin-bottom: 20px; } .facebook-login::before { content: "📘"; margin-right: 8px; font-size: 16px; } .forgot-password { text-align: center; margin-top: 12px; } .forgot-password a { color: #0095f6; font-size: 12px; text-decoration: none; } .signup-box { background: white; border: 1px solid #dbdbdb; border-radius: 1px; padding: 20px; text-align: center; font-size: 14px; color: #262626; } .signup-box a { color: #0095f6; font-weight: 600; text-decoration: none; } .message { padding: 10px; margin-bottom: 15px; border-radius: 3px; font-size: 12px; text-align: center; opacity: 0; transition: opacity 0.3s ease; } .message.show { opacity: 1; } .message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .app-download { text-align: center; margin-top: 20px; } .app-download p { font-size: 14px; color: #262626; margin-bottom: 20px; } .download-buttons { display: flex; justify-content: center; gap: 8px; } .download-btn { height: 40px; border-radius: 3px; overflow: hidden; } .download-btn img { height: 100%; width: auto; } .api-config { position: fixed; top: 10px; right: 10px; background: rgba(255, 255, 255, 0.95); padding: 8px 12px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); font-size: 11px; color: #666; border: 1px solid #dbdbdb; } .api-config input { border: 1px solid #dbdbdb; padding: 4px 6px; border-radius: 3px; width: 180px; font-size: 11px; margin-left: 5px; } .hidden { display: none; } @media (min-width: 768px) { .phone-mockup { display: block; } } @media (max-width: 767px) { .main-container { max-width: 350px; } .form-box { border: none; background: transparent; padding: 20px 0; } .signup-box { border: none; background: transparent; } .api-config { position: relative; top: auto; right: auto; margin-bottom: 20px; text-align: center; } } /* 載入Billabong字體 */ @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap'); .instagram-logo h1 { font-family: 'Lobster', cursive; } </style> </head> <body> <!-- API 設定 --> <div class="api-config"> API: <input type="text" id="apiUrl" value="http://localhost:8080" placeholder="http://localhost:8080"> </div> <div class="main-container"> <!-- 手機模擬圖 --> <div class="phone-mockup"> <div class="phone-image"></div> </div> <!-- 表單容器 --> <div class="form-container"> <!-- 登入表單 --> <div id="loginForm"> <div class="form-box"> <div class="instagram-logo"> <h1>Instagram</h1> </div> <!-- 訊息顯示 --> <div id="message" class="message"></div> <div class="form-group"> <input type="text" id="loginUsername" placeholder="電話號碼、使用者名稱或電子郵件" required> </div> <div class="form-group"> <input type="password" id="loginPassword" placeholder="密碼" required> </div> <button class="submit-btn" id="loginBtn" onclick="handleLogin()"> <span class="btn-text">登入</span> </button> <div class="divider"> <span>或</span> </div> <a href="#" class="facebook-login">使用 Facebook 登入</a> <div class="forgot-password"> <a href="#">忘記密碼?</a> </div> </div> <div class="signup-box"> 沒有帳號嗎?<a href="#" onclick="toggleForm()">註冊</a> </div> </div> <!-- 註冊表單 --> <div id="registerForm" class="hidden"> <div class="form-box"> <div class="instagram-logo"> <h1>Instagram</h1> </div> <p style="text-align: center; color: #8e8e8e; font-size: 17px; font-weight: 600; margin-bottom: 20px;"> 註冊即可查看朋友的相片和影片。 </p> <!-- 訊息顯示 --> <div id="registerMessage" class="message"></div> <a href="#" class="facebook-login" style="background: #0095f6; color: white; padding: 8px; border-radius: 4px; margin-bottom: 20px; text-decoration: none;"> 使用 Facebook 登入 </a> <div class="divider"> <span>或</span> </div> <div class="form-group"> <input type="text" id="registerUsername" placeholder="使用者名稱" required> </div> <div class="form-group"> <input type="password" id="registerPassword" placeholder="密碼" required> </div> <p style="font-size: 12px; color: #8e8e8e; text-align: center; margin: 15px 0;"> 註冊即表示你同意我們的<a href="#" style="color: #0095f6;">服務條款</a>、<a href="#" style="color: #0095f6;">隱私權政策</a>和<a href="#" style="color: #0095f6;">Cookie 政策</a>。 </p> <button class="submit-btn" id="registerBtn" onclick="handleRegister()"> <span class="btn-text">註冊</span> </button> </div> <div class="signup-box"> 有帳號了嗎?<a href="#" onclick="toggleForm()">登入</a> </div> </div> <!-- 下載App --> <div class="app-download"> <p>取得應用程式。</p> <div class="download-buttons"> <div class="download-btn"> <img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 135 40'><rect width='135' height='40' fill='%23000' rx='5'/><text x='67.5' y='25' fill='white' text-anchor='middle' font-size='12'>App Store</text></svg>" alt="App Store"> </div> <div class="download-btn"> <img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 135 40'><rect width='135' height='40' fill='%23000' rx='5'/><text x='67.5' y='25' fill='white' text-anchor='middle' font-size='12'>Google Play</text></svg>" alt="Google Play"> </div> </div> </div> </div> </div> <script> let isLoginForm = true; // 獲取API基礎URL function getApiUrl() { return document.getElementById('apiUrl').value.trim() || 'http://localhost:8080'; } // 切換表單 function toggleForm() { const loginForm = document.getElementById('loginForm'); const registerForm = document.getElementById('registerForm'); if (isLoginForm) { loginForm.classList.add('hidden'); registerForm.classList.remove('hidden'); } else { loginForm.classList.remove('hidden'); registerForm.classList.add('hidden'); } isLoginForm = !isLoginForm; clearMessages(); clearInputs(); } // 設定按鈕載入狀態 function setButtonLoading(buttonId, isLoading) { const button = document.getElementById(buttonId); if (isLoading) { button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; } } // 處理登入 async function handleLogin() { const username = document.getElementById('loginUsername').value.trim(); const password = document.getElementById('loginPassword').value.trim(); if (!username || !password) { showMessage('message', '請填寫所有欄位', 'error'); return; } setButtonLoading('loginBtn', true); try { const response = await fetch(`${getApiUrl()}/user/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: username, password: password }) }); const message = await response.text(); if (response.ok) { showMessage('message', message, 'success'); clearInputs(); } else { showMessage('message', message, 'error'); } } catch (error) { console.error('登入錯誤:', error); showMessage('message', '連線失敗,請稍後再試', 'error'); } finally { setButtonLoading('loginBtn', false); } } // 處理註冊 async function handleRegister() { const username = document.getElementById('registerUsername').value.trim(); const password = document.getElementById('registerPassword').value.trim(); if (!username || !password) { showMessage('registerMessage', '請填寫所有欄位', 'error'); return; } if (password.length < 6) { showMessage('registerMessage', '密碼至少需要 6 個字元', 'error'); return; } setButtonLoading('registerBtn', true); try { const response = await fetch(`${getApiUrl()}/user/register`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: username, password: password }) }); const message = await response.text(); if (response.ok) { showMessage('registerMessage', message, 'success'); clearInputs(); // 註冊成功後自動切換到登入頁面 setTimeout(() => { if (!isLoginForm) { toggleForm(); } }, 2000); } else { showMessage('registerMessage', message, 'error'); } } catch (error) { console.error('註冊錯誤:', error); showMessage('registerMessage', '連線失敗,請稍後再試', 'error'); } finally { setButtonLoading('registerBtn', false); } } // 顯示訊息 function showMessage(elementId, text, type) { const message = document.getElementById(elementId); message.textContent = text; message.className = `message ${type} show`; setTimeout(() => { message.classList.remove('show'); }, 5000); } // 清除訊息 function clearMessages() { document.getElementById('message').classList.remove('show'); document.getElementById('registerMessage').classList.remove('show'); } // 清空輸入框 function clearInputs() { document.getElementById('loginUsername').value = ''; document.getElementById('loginPassword').value = ''; document.getElementById('registerUsername').value = ''; document.getElementById('registerPassword').value = ''; } // 按Enter鍵提交表單 document.addEventListener('keypress', function(e) { if (e.key === 'Enter') { const activeBtn = isLoginForm ? document.getElementById('loginBtn') : document.getElementById('registerBtn'); if (!activeBtn.disabled) { if (isLoginForm) { handleLogin(); } else { handleRegister(); } } } }); // 防止Facebook登入連結跳轉 document.addEventListener('click', function(e) { if (e.target.classList.contains('facebook-login') || e.target.closest('.facebook-login')) { e.preventDefault(); } }); // 頁面載入完成後檢查API連線 window.addEventListener('load', async function() { try { const response = await fetch(`${getApiUrl()}/user/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: '', password: '' }) }); console.log('API 伺服器連線正常'); } catch (error) { console.warn('無法連接到API伺服器:', error); } }); </script> </body> </html> ``` </details>