第 28 课:AJAX 与前端框架
本课目标:掌握 AJAX 异步请求、Fetch API、async/await 语法,了解 Vue.js 基础用法和 SPA 单页应用概念,能够实现前后端数据交互。
一、概念讲解
1.1 什么是 AJAX
AJAX(Asynchronous JavaScript And XML)异步 JavaScript 和 XML,是一种在无需重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容的技术。
1.2 AJAX 工作流程
1┌─────────────────────────────────────────────────────┐ 2│ 传统请求流程 │ 3│ 用户操作 → 发送请求 → 等待服务器 → 返回整个页面 → 刷新 │ 4└─────────────────────────────────────────────────────┘ 5 6┌─────────────────────────────────────────────────────┐ 7│ AJAX 请求流程 │ 8│ 用户操作 → 发送异步请求 → 服务器处理 │ 9│ ↑ ↓ │ 10│ └──── 更新部分页面 ← 返回 JSON 数据 │ 11└─────────────────────────────────────────────────────┘ 12
1.3 前后端交互模型
1┌─────────────────────────────────────────────────────┐ 2│ 前端 (浏览器) │ 3│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 4│ │ HTML │ │ CSS │ │ JavaScript │ │ 5│ │ 页面结构 │ │ 页面样式 │ │ 交互逻辑 │ │ 6│ └─────────────┘ └─────────────┘ └──────┬──────┘ │ 7│ │ │ 8│ ┌──────┴──────┐ │ 9│ │ AJAX/Fetch │ │ 10│ └──────┬──────┘ │ 11└───────────────────────────────────────────┼─────────┘ 12 │ HTTP 13 ┌───────┴───────┐ 14 │ API │ 15 │ 服务器 │ 16 └───────┬───────┘ 17 │ 18 ┌───────┴───────┐ 19 │ 数据库 │ 20 └───────────────┘ 21
二、语法格式
2.1 XMLHttpRequest
1// 创建 XMLHttpRequest 对象 2const xhr = new XMLHttpRequest(); 3 4// 配置请求 5xhr.open('GET', '/api/users', true); 6 7// 设置回调 8xhr.onreadystatechange = function() { 9 if (xhr.readyState === 4) { // 请求完成 10 if (xhr.status === 200) { // 成功 11 const data = JSON.parse(xhr.responseText); 12 console.log(data); 13 } else { 14 console.error('请求失败:', xhr.status); 15 } 16 } 17}; 18 19// 发送请求 20xhr.send(); 21 22// POST 请求 23xhr.open('POST', '/api/users', true); 24xhr.setRequestHeader('Content-Type', 'application/json'); 25xhr.send(JSON.stringify({ name: '张三', age: 20 })); 26
▶ 运行结果:
1XMLHttpRequest 请求效果: 2┌─────────────────────────────────────┐ 3│ GET 请求: │ 4│ → 发送请求到 /api/users │ 5│ → readyState 变化: 0→1→2→3→4 │ 6│ → status: 200 (成功) │ 7│ → 控制台输出用户数据数组 │ 8│ │ 9│ POST 请求: │ 10│ → 发送请求到 /api/users │ 11│ → 请求体: {"name":"张三","age":20} │ 12│ → 服务器创建新用户并返回响应 │ 13└─────────────────────────────────────┘ 14
2.2 Fetch API(推荐)
1// GET 请求 2fetch('/api/users') 3 .then(response => response.json()) 4 .then(data => console.log(data)) 5 .catch(error => console.error('Error:', error)); 6 7// POST 请求 8fetch('/api/users', { 9 method: 'POST', 10 headers: { 11 'Content-Type': 'application/json' 12 }, 13 body: JSON.stringify({ name: '张三', age: 20 }) 14}) 15.then(response => response.json()) 16.then(data => console.log(data)); 17 18// DELETE 请求 19fetch('/api/users/1', { 20 method: 'DELETE' 21}) 22.then(response => response.json()); 23
▶ 运行结果:
1Fetch API 请求效果: 2┌─────────────────────────────────────┐ 3│ GET 请求: │ 4│ → fetch('/api/users') │ 5│ → 返回 Promise 对象 │ 6│ → 控制台输出用户数据数组 │ 7│ │ 8│ POST 请求: │ 9│ → fetch('/api/users', {...}) │ 10│ → 发送 JSON 数据到服务器 │ 11│ → 控制台输出创建的用户数据 │ 12│ │ 13│ DELETE 请求: │ 14│ → fetch('/api/users/1', {...}) │ 15│ → 删除 ID 为 1 的用户 │ 16│ → 控制台输出删除结果 │ 17└─────────────────────────────────────┘ 18
2.3 async/await
1// async 函数 2async function getUsers() { 3 try { 4 const response = await fetch('/api/users'); 5 if (!response.ok) { 6 throw new Error('HTTP error! status: ' + response.status); 7 } 8 const data = await response.json(); 9 return data; 10 } catch (error) { 11 console.error('获取用户失败:', error); 12 } 13} 14 15// 调用 16getUsers().then(users => console.log(users)); 17 18// POST 请求 19async function createUser(user) { 20 try { 21 const response = await fetch('/api/users', { 22 method: 'POST', 23 headers: { 'Content-Type': 'application/json' }, 24 body: JSON.stringify(user) 25 }); 26 return await response.json(); 27 } catch (error) { 28 console.error('创建用户失败:', error); 29 } 30} 31
▶ 运行结果:
1async/await 请求效果: 2┌─────────────────────────────────────┐ 3│ getUsers() 调用: │ 4│ → 等待 fetch 完成 │ 5│ → 等待 response.json() 完成 │ 6│ → 返回用户数据数组 │ 7│ → 控制台输出用户列表 │ 8│ │ 9│ createUser(user) 调用: │ 10│ → 等待 fetch POST 请求完成 │ 11│ → 等待 response.json() 完成 │ 12│ → 返回创建的用户数据 │ 13│ │ 14│ 错误处理: │ 15│ → 如果网络错误,catch 捕获错误 │ 16│ → 控制台输出 "获取用户失败: ..." │ 17└─────────────────────────────────────┘ 18
2.4 JSON 格式
1// JSON 转 JavaScript 对象 2const jsonString = '{"name": "张三", "age": 20}'; 3const obj = JSON.parse(jsonString); 4 5// JavaScript 对象转 JSON 6const user = { name: '张三', age: 20 }; 7const json = JSON.stringify(user); 8// '{"name":"张三","age":20}' 9
▶ 运行结果:
1JSON 操作效果: 2┌─────────────────────────────────────┐ 3│ JSON 转对象: │ 4│ jsonString = '{"name":"张三","age":20}'│ 5│ JSON.parse(jsonString) │ 6│ → { name: "张三", age: 20 } │ 7│ │ 8│ 对象转 JSON: │ 9│ user = { name: "张三", age: 20 } │ 10│ JSON.stringify(user) │ 11│ → '{"name":"张三","age":20}' │ 12└─────────────────────────────────────┘ 13
2.5 Vue.js 基础
1<!-- 引入 Vue.js --> 2<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> 3 4<div id="app"> 5 <h1>{{ message }}</h1> 6 <p>计数: {{ count }}</p> 7 <button @click="increment">+1</button> 8</div> 9 10<script> 11const { createApp } = Vue; 12 13createApp({ 14 // 数据 15 data() { 16 return { 17 message: 'Hello Vue!', 18 count: 0 19 } 20 }, 21 22 // 方法 23 methods: { 24 increment() { 25 this.count++; 26 } 27 }, 28 29 // 计算属性 30 computed: { 31 doubleCount() { 32 return this.count * 2; 33 } 34 }, 35 36 // 侦听器 37 watch: { 38 count(newVal, oldVal) { 39 console.log(`计数从 ${oldVal} 变为 ${newVal}`); 40 } 41 }, 42 43 // 生命周期钩子 44 mounted() { 45 console.log('组件已挂载'); 46 } 47}).mount('#app'); 48</script> 49
2.6 Vue.js 指令
1<div id="app"> 2 <!-- 文本插值 --> 3 <p>{{ message }}</p> 4 5 <!-- 双向绑定 --> 6 <input v-model="name" placeholder="请输入姓名"> 7 <p>你好, {{ name }}</p> 8 9 <!-- 条件渲染 --> 10 <p v-if="isLoggedIn">欢迎回来!</p> 11 <p v-else>请登录</p> 12 13 <!-- 列表渲染 --> 14 <ul> 15 <li v-for="item in items" :key="item.id"> 16 {{ item.text }} 17 </li> 18 </ul> 19 20 <!-- 事件绑定 --> 21 <button @click="handleClick">点击</button> 22 23 <!-- 属性绑定 --> 24 <img :src="imageUrl" :alt="imageAlt"> 25 26 <!-- 类名绑定 --> 27 <div :class="{ active: isActive, 'text-bold': isBold }"></div> 28 29 <!-- 样式绑定 --> 30 <div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div> 31</div> 32
2.7 Vue.js 组件
1<div id="app"> 2 <user-card name="张三" age="20"></user-card> 3 <user-card name="李四" age="22"></user-card> 4</div> 5 6<script> 7const { createApp } = Vue; 8 9// 定义组件 10const UserCard = { 11 props: ['name', 'age'], 12 template: ` 13 <div class="card"> 14 <h3>{{ name }}</h3> 15 <p>年龄: {{ age }}</p> 16 <button @click="sayHello">打招呼</button> 17 </div> 18 `, 19 methods: { 20 sayHello() { 21 alert(`你好, 我是${this.name}`); 22 } 23 } 24}; 25 26// 创建应用并注册组件 27const app = createApp({}); 28app.component('user-card', UserCard); 29app.mount('#app'); 30</script> 31
三、代码案例
案例 1:AJAX 登录表单
1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>AJAX 登录</title> 7 <style> 8 * { margin: 0; padding: 0; box-sizing: border-box; } 9 body { font-family: "Microsoft YaHei", sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } 10 .login-box { background: white; padding: 40px; border-radius: 12px; width: 100%; max-width: 400px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } 11 .login-box h2 { text-align: center; margin-bottom: 30px; color: #333; } 12 .form-group { margin-bottom: 20px; } 13 .form-group label { display: block; margin-bottom: 8px; color: #555; font-size: 14px; } 14 .form-group input { 15 width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; transition: border-color 0.3s; 16 } 17 .form-group input:focus { outline: none; border-color: #667eea; } 18 .login-btn { 19 width: 100%; padding: 14px; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; transition: background 0.3s; 20 } 21 .login-btn:hover { background: #5a6fd6; } 22 .login-btn:disabled { background: #ccc; cursor: not-allowed; } 23 .message { text-align: center; margin-top: 15px; padding: 10px; border-radius: 6px; display: none; } 24 .message.success { display: block; background: #d4edda; color: #155724; } 25 .message.error { display: block; background: #f8d7da; color: #721c24; } 26 .loading { display: none; text-align: center; } 27 .loading.show { display: block; } 28 </style> 29</head> 30<body> 31 <div class="login-box"> 32 <h2>用户登录</h2> 33 <form id="loginForm"> 34 <div class="form-group"> 35 <label for="username">用户名</label> 36 <input type="text" id="username" placeholder="请输入用户名" required> 37 </div> 38 <div class="form-group"> 39 <label for="password">密码</label> 40 <input type="password" id="password" placeholder="请输入密码" required> 41 </div> 42 <button type="submit" class="login-btn" id="loginBtn">登录</button> 43 </form> 44 <div class="loading" id="loading"> 45 <p>⏳ 登录中...</p> 46 </div> 47 <div class="message" id="message"></div> 48 </div> 49 50 <script> 51 const loginForm = document.getElementById('loginForm'); 52 const loginBtn = document.getElementById('loginBtn'); 53 const loading = document.getElementById('loading'); 54 const message = document.getElementById('message'); 55 56 // 方式一:使用 XMLHttpRequest 57 function loginWithXHR(username, password) { 58 return new Promise((resolve, reject) => { 59 const xhr = new XMLHttpRequest(); 60 xhr.open('POST', '/api/login', true); 61 xhr.setRequestHeader('Content-Type', 'application/json'); 62 63 xhr.onreadystatechange = function() { 64 if (xhr.readyState === 4) { 65 if (xhr.status === 200) { 66 resolve(JSON.parse(xhr.responseText)); 67 } else { 68 reject(new Error('登录失败: ' + xhr.status)); 69 } 70 } 71 }; 72 73 xhr.onerror = function() { 74 reject(new Error('网络错误')); 75 }; 76 77 xhr.send(JSON.stringify({ username, password })); 78 }); 79 } 80 81 // 方式二:使用 Fetch API(推荐) 82 async function loginWithFetch(username, password) { 83 const response = await fetch('/api/login', { 84 method: 'POST', 85 headers: { 86 'Content-Type': 'application/json' 87 }, 88 body: JSON.stringify({ username, password }) 89 }); 90 91 if (!response.ok) { 92 throw new Error('登录失败: ' + response.status); 93 } 94 95 return await response.json(); 96 } 97 98 // 显示消息 99 function showMessage(text, type) { 100 message.textContent = text; 101 message.className = 'message ' + type; 102 } 103 104 // 表单提交 105 loginForm.addEventListener('submit', async (e) => { 106 e.preventDefault(); 107 108 const username = document.getElementById('username').value; 109 const password = document.getElementById('password').value; 110 111 // 显示加载状态 112 loginBtn.disabled = true; 113 loading.classList.add('show'); 114 message.className = 'message'; 115 116 try { 117 // 使用 Fetch API 登录 118 const result = await loginWithFetch(username, password); 119 120 if (result.success) { 121 showMessage('登录成功!欢迎回来, ' + result.user.name, 'success'); 122 // 存储 token 123 localStorage.setItem('token', result.token); 124 // 跳转到主页 125 // window.location.href = '/dashboard'; 126 } else { 127 showMessage(result.message || '用户名或密码错误', 'error'); 128 } 129 } catch (error) { 130 showMessage('登录失败: ' + error.message, 'error'); 131 } finally { 132 loginBtn.disabled = false; 133 loading.classList.remove('show'); 134 } 135 }); 136 </script> 137</body> 138</html> 139
案例 2:Fetch API 数据加载
1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>用户列表</title> 7 <style> 8 * { margin: 0; padding: 0; box-sizing: border-box; } 9 body { font-family: "Microsoft YaHei", sans-serif; background: #f5f5f5; padding: 30px 20px; } 10 .container { max-width: 900px; margin: 0 auto; } 11 .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; } 12 .header h1 { color: #333; } 13 .refresh-btn { 14 padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; 15 } 16 .search-box { margin-bottom: 20px; } 17 .search-box input { 18 width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; 19 } 20 .user-list { display: grid; gap: 15px; } 21 .user-card { 22 background: white; border-radius: 10px; padding: 20px; display: flex; align-items: center; gap: 20px; box-shadow: 0 3px 10px rgba(0,0,0,0.08); transition: transform 0.2s; 23 } 24 .user-card:hover { transform: translateY(-3px); } 25 .avatar { 26 width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; font-weight: bold; 27 } 28 .user-info { flex: 1; } 29 .user-name { font-size: 18px; color: #333; margin-bottom: 5px; } 30 .user-email { color: #666; font-size: 14px; } 31 .user-phone { color: #999; font-size: 13px; margin-top: 5px; } 32 .user-actions { display: flex; gap: 10px; } 33 .user-actions button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; } 34 .edit-btn { background: #ffa502; color: white; } 35 .delete-btn { background: #ff4757; color: white; } 36 .loading { text-align: center; padding: 50px; color: #999; } 37 .error { text-align: center; padding: 50px; color: #ff4757; } 38 .empty { text-align: center; padding: 50px; color: #999; } 39 </style> 40</head> 41<body> 42 <div class="container"> 43 <div class="header"> 44 <h1>用户列表</h1> 45 <button class="refresh-btn" onclick="loadUsers()">刷新</button> 46 </div> 47 48 <div class="search-box"> 49 <input type="text" id="searchInput" placeholder="搜索用户..." oninput="filterUsers()"> 50 </div> 51 52 <div id="userList" class="user-list"> 53 <div class="loading">⏳ 加载中...</div> 54 </div> 55 </div> 56 57 <script> 58 let allUsers = []; 59 60 // 模拟 API 数据 61 const mockUsers = [ 62 { id: 1, name: '张三', email: 'zhangsan@email.com', phone: '13800138001' }, 63 { id: 2, name: '李四', email: 'lisi@email.com', phone: '13800138002' }, 64 { id: 3, name: '王五', email: 'wangwu@email.com', phone: '13800138003' }, 65 { id: 4, name: '赵六', email: 'zhaoliu@email.com', phone: '13800138004' }, 66 { id: 5, name: '钱七', email: 'qianqi@email.com', phone: '13800138005' } 67 ]; 68 69 // 模拟 API 延迟 70 function delay(ms) { 71 return new Promise(resolve => setTimeout(resolve, ms)); 72 } 73 74 // 获取用户列表 75 async function fetchUsers() { 76 await delay(800); // 模拟网络延迟 77 78 // 模拟 API 响应 79 return { 80 success: true, 81 data: mockUsers 82 }; 83 } 84 85 // 加载用户 86 async function loadUsers() { 87 const userList = document.getElementById('userList'); 88 userList.innerHTML = '<div class="loading">⏳ 加载中...</div>'; 89 90 try { 91 const result = await fetchUsers(); 92 93 if (result.success) { 94 allUsers = result.data; 95 renderUsers(allUsers); 96 } else { 97 userList.innerHTML = '<div class="error">❌ 加载失败</div>'; 98 } 99 } catch (error) { 100 userList.innerHTML = [`<div class="error">❌ 网络错误: ${error.message}</div>`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.div.md); 101 } 102 } 103 104 // 渲染用户列表 105 function renderUsers(users) { 106 const userList = document.getElementById('userList'); 107 108 if (users.length === 0) { 109 userList.innerHTML = '<div class="empty">🔍 没有找到匹配的用户</div>'; 110 return; 111 } 112 113 userList.innerHTML = users.map(user => ` 114 <div class="user-card" data-id="${user.id}"> 115 <div class="avatar">${user.name.charAt(0)}</div> 116 <div class="user-info"> 117 <div class="user-name">${user.name}</div> 118 <div class="user-email">📧 ${user.email}</div> 119 <div class="user-phone">📱 ${user.phone}</div> 120 </div> 121 <div class="user-actions"> 122 <button class="edit-btn" onclick="editUser(${user.id})">编辑</button> 123 <button class="delete-btn" onclick="deleteUser(${user.id})">删除</button> 124 </div> 125 </div> 126 `).join(''); 127 } 128 129 // 搜索过滤 130 function filterUsers() { 131 const keyword = document.getElementById('searchInput').value.toLowerCase(); 132 const filtered = allUsers.filter(user => 133 user.name.toLowerCase().includes(keyword) || 134 user.email.toLowerCase().includes(keyword) 135 ); 136 renderUsers(filtered); 137 } 138 139 // 编辑用户 140 function editUser(id) { 141 const user = allUsers.find(u => u.id === id); 142 alert(`编辑用户: ${user.name}`); 143 } 144 145 // 删除用户 146 async function deleteUser(id) { 147 if (!confirm('确定要删除该用户吗?')) return; 148 149 try { 150 // 模拟删除 API 151 await delay(300); 152 allUsers = allUsers.filter(u => u.id !== id); 153 renderUsers(allUsers); 154 alert('删除成功!'); 155 } catch (error) { 156 alert('删除失败: ' + error.message); 157 } 158 } 159 160 // 初始化加载 161 loadUsers(); 162 </script> 163</body> 164</html> 165
案例 3:Vue.js 计数器应用
1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Vue.js 计数器</title> 7 <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> 8 <style> 9 * { margin: 0; padding: 0; box-sizing: border-box; } 10 body { font-family: "Microsoft YaHei", sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; } 11 .app { background: white; border-radius: 16px; padding: 40px; width: 100%; max-width: 450px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } 12 .app h1 { text-align: center; color: #333; margin-bottom: 30px; } 13 .counter { text-align: center; margin-bottom: 30px; } 14 .counter-value { font-size: 72px; font-weight: bold; color: #667eea; transition: color 0.3s; } 15 .counter-value.positive { color: #2ed573; } 16 .counter-value.negative { color: #ff4757; } 17 .counter-btns { display: flex; gap: 15px; justify-content: center; margin-bottom: 30px; } 18 .counter-btns button { 19 padding: 15px 30px; border: none; border-radius: 10px; font-size: 18px; cursor: pointer; transition: transform 0.2s; 20 } 21 .counter-btns button:hover { transform: scale(1.05); } 22 .counter-btns .minus { background: #ff4757; color: white; } 23 .counter-btns .plus { background: #2ed573; color: white; } 24 .counter-btns .reset { background: #ffa502; color: white; } 25 .history { margin-top: 20px; } 26 .history h3 { color: #333; margin-bottom: 15px; } 27 .history-list { list-style: none; max-height: 150px; overflow-y: auto; } 28 .history-list li { padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 14px; color: #666; } 29 .stats { display: flex; justify-content: space-around; margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; } 30 .stat-item { text-align: center; } 31 .stat-value { font-size: 24px; font-weight: bold; color: #667eea; } 32 .stat-label { font-size: 12px; color: #999; margin-top: 5px; } 33 </style> 34</head> 35<body> 36 <div id="app" class="app"> 37 <h1>Vue.js 计数器</h1> 38 39 <div class="counter"> 40 <div :class="['counter-value', { positive: count > 0, negative: count < 0 }]"> 41 {{ count }} 42 </div> 43 </div> 44 45 <div class="counter-btns"> 46 <button class="minus" @click="decrement">-1</button> 47 <button class="reset" @click="reset">重置</button> 48 <button class="plus" @click="increment">+1</button> 49 </div> 50 51 <div class="counter-btns" style="margin-top: -15px;"> 52 <button class="minus" @click="decrementBy(5)">-5</button> 53 <button class="plus" @click="incrementBy(5)">+5</button> 54 </div> 55 56 <div class="stats"> 57 <div class="stat-item"> 58 <div class="stat-value">{{ count }}</div> 59 <div class="stat-label">当前值</div> 60 </div> 61 <div class="stat-item"> 62 <div class="stat-value">{{ doubleCount }}</div> 63 <div class="stat-label">双倍值</div> 64 </div> 65 <div class="stat-item"> 66 <div class="stat-value">{{ history.length }}</div> 67 <div class="stat-label">操作次数</div> 68 </div> 69 </div> 70 71 <div class="history" v-if="history.length > 0"> 72 <h3>操作历史</h3> 73 <ul class="history-list"> 74 <li v-for="(item, index) in history" :key="index"> 75 {{ item.action }}: {{ item.from }} → {{ item.to }} 76 </li> 77 </ul> 78 </div> 79 </div> 80 81 <script> 82 const { createApp } = Vue; 83 84 createApp({ 85 data() { 86 return { 87 count: 0, 88 history: [] 89 } 90 }, 91 92 computed: { 93 doubleCount() { 94 return this.count * 2; 95 } 96 }, 97 98 watch: { 99 count(newVal, oldVal) { 100 const action = newVal > oldVal ? '增加' : '减少'; 101 this.history.unshift({ 102 action, 103 from: oldVal, 104 to: newVal 105 }); 106 107 // 只保留最近 10 条记录 108 if (this.history.length > 10) { 109 this.history.pop(); 110 } 111 } 112 }, 113 114 methods: { 115 increment() { 116 this.count++; 117 }, 118 decrement() { 119 this.count--; 120 }, 121 incrementBy(value) { 122 this.count += value; 123 }, 124 decrementBy(value) { 125 this.count -= value; 126 }, 127 reset() { 128 this.count = 0; 129 } 130 }, 131 132 mounted() { 133 console.log('Vue.js 计数器已启动'); 134 } 135 }).mount('#app'); 136 </script> 137</body> 138</html> 139
案例 4:Vue.js Todo 应用
1<!DOCTYPE html> 2<html lang="zh-CN"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Vue.js Todo</title> 7 <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> 8 <style> 9 * { margin: 0; padding: 0; box-sizing: border-box; } 10 body { font-family: "Microsoft YaHei", sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; justify-content: center; padding: 40px 20px; } 11 .todo-app { background: white; border-radius: 16px; padding: 30px; width: 100%; max-width: 500px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } 12 .todo-app h1 { text-align: center; color: #333; margin-bottom: 25px; } 13 .input-group { display: flex; gap: 10px; margin-bottom: 20px; } 14 .input-group input { 15 flex: 1; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; 16 } 17 .input-group input:focus { outline: none; border-color: #667eea; } 18 .input-group button { 19 padding: 12px 24px; background: #667eea; color: white; border: none; border-radius: 8px; cursor: pointer; 20 } 21 .filters { display: flex; gap: 10px; margin-bottom: 20px; } 22 .filter-btn { 23 padding: 8px 16px; border: none; border-radius: 20px; cursor: pointer; background: #f0f0f0; color: #666; font-size: 13px; 24 } 25 .filter-btn.active { background: #667eea; color: white; } 26 .todo-list { list-style: none; } 27 .todo-item { 28 display: flex; align-items: center; gap: 12px; padding: 15px; border-bottom: 1px solid #f0f0f0; transition: background 0.2s; 29 } 30 .todo-item:hover { background: #f9f9f9; } 31 .todo-item input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; accent-color: #667eea; } 32 .todo-item span { flex: 1; font-size: 15px; color: #333; } 33 .todo-item.completed span { text-decoration: line-through; color: #999; } 34 .delete-btn { padding: 5px 10px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; } 35 .footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee; color: #999; font-size: 13px; } 36 .clear-btn { padding: 8px 16px; background: none; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; color: #666; font-size: 13px; } 37 .empty { text-align: center; padding: 40px; color: #999; } 38 </style> 39</head> 40<body> 41 <div id="app" class="todo-app"> 42 <h1>📝 Vue.js Todo</h1> 43 44 <div class="input-group"> 45 <input 46 v-model="newTodo" 47 @keyup.enter="addTodo" 48 placeholder="添加新的待办事项..." 49 > 50 <button @click="addTodo">添加</button> 51 </div> 52 53 <div class="filters"> 54 <button 55 v-for="filter in filters" 56 :key="filter.value" 57 :class="['filter-btn', { active: currentFilter === filter.value }]" 58 @click="currentFilter = filter.value" 59 > 60 {{ filter.label }} 61 </button> 62 </div> 63 64 <ul class="todo-list" v-if="filteredTodos.length > 0"> 65 <li 66 v-for="todo in filteredTodos" 67 :key="todo.id" 68 :class="['todo-item', { completed: todo.completed }]" 69 > 70 <input type="checkbox" v-model="todo.completed"> 71 <span>{{ todo.text }}</span> 72 <button class="delete-btn" @click="removeTodo(todo.id)">删除</button> 73 </li> 74 </ul> 75 76 <div v-else class="empty"> 77 🎉 没有待办事项 78 </div> 79 80 <div class="footer" v-if="todos.length > 0"> 81 <span>{{ activeCount }} 个待完成</span> 82 <button class="clear-btn" @click="clearCompleted">清除已完成</button> 83 </div> 84 </div> 85 86 <script> 87 const { createApp } = Vue; 88 89 createApp({ 90 data() { 91 return { 92 newTodo: '', 93 todos: JSON.parse(localStorage.getItem('vue-todos')) || [], 94 currentFilter: 'all', 95 filters: [ 96 { label: '全部', value: 'all' }, 97 { label: '未完成', value: 'active' }, 98 { label: '已完成', value: 'completed' } 99 ] 100 } 101 }, 102 103 computed: { 104 filteredTodos() { 105 switch (this.currentFilter) { 106 case 'active': 107 return this.todos.filter(t => !t.completed); 108 case 'completed': 109 return this.todos.filter(t => t.completed); 110 default: 111 return this.todos; 112 } 113 }, 114 115 activeCount() { 116 return this.todos.filter(t => !t.completed).length; 117 } 118 }, 119 120 watch: { 121 todos: { 122 handler(newVal) { 123 localStorage.setItem('vue-todos', JSON.stringify(newVal)); 124 }, 125 deep: true 126 } 127 }, 128 129 methods: { 130 addTodo() { 131 const text = this.newTodo.trim(); 132 if (!text) return; 133 134 this.todos.push({ 135 id: Date.now(), 136 text: text, 137 completed: false 138 }); 139 140 this.newTodo = ''; 141 }, 142 143 removeTodo(id) { 144 this.todos = this.todos.filter(t => t.id !== id); 145 }, 146 147 clearCompleted() { 148 this.todos = this.todos.filter(t => !t.completed); 149 } 150 } 151 }).mount('#app'); 152 </script> 153</body> 154</html> 155
四、常见错误
错误 1:忘记处理 Promise
1// 错误:没有处理 Promise 错误 2fetch('/api/data') 3 .then(response => response.json()) 4 .then(data => console.log(data)); 5// 如果请求失败,不会有错误提示 6 7// 正确:添加 catch 处理 8fetch('/api/data') 9 .then(response => response.json()) 10 .then(data => console.log(data)) 11 .catch(error => console.error('Error:', error)); 12
错误 2:async/await 忘记 try-catch
1// 错误:没有错误处理 2async function getData() { 3 const response = await fetch('/api/data'); 4 const data = await response.json(); 5 return data; 6} 7 8// 正确:使用 try-catch 9async function getData() { 10 try { 11 const response = await fetch('/api/data'); 12 if (!response.ok) { 13 throw new Error('HTTP error! status: ' + response.status); 14 } 15 const data = await response.json(); 16 return data; 17 } catch (error) { 18 console.error('获取数据失败:', error); 19 throw error; 20 } 21} 22
错误 3:Vue.js 响应式数据问题
1// 错误:直接修改数组索引 2this.todos[0].completed = true; // 不会触发视图更新 3 4// 正确:使用 splice 或 Vue.set 5this.todos.splice(0, 1, { ...this.todos[0], completed: true }); 6 7// 或者 8import { set } from 'vue'; 9set(this.todos[0], 'completed', true); 10
错误 4:CORS 跨域问题
1// 前端请求被浏览器阻止 2fetch('http://api.example.com/data') 3 .then(response => response.json()) 4 .catch(error => console.error('CORS error:', error)); 5 6// 解决方案: 7// 1. 后端设置 CORS 头 8// Access-Control-Allow-Origin: http://localhost:3000 9 10// 2. 使用代理 11// 在开发服务器配置 proxy 12
错误 5:Vue.js 组件通信错误
1// 错误:在子组件中直接修改 props 2props: ['count'], 3methods: { 4 increment() { 5 this.count++; // 不会生效,Vue 会警告 6 } 7} 8 9// 正确:使用 emit 触发事件 10props: ['count'], 11methods: { 12 increment() { 13 this.$emit('update:count', this.count + 1); 14 } 15} 16
五、课后练习
练习 1:AJAX 天气应用
实现一个天气查询应用:
- 使用 Fetch API 获取天气数据
- 支持城市搜索
- 显示当前天气和未来预报
- 错误处理和加载状态
练习 2:Vue.js 购物车
实现一个购物车应用:
- 商品列表展示
- 添加/移除商品
- 修改数量
- 计算总价
- 使用 Vue.js 组件化
练习 3:前后端交互
实现一个完整的用户管理系统:
- 前端使用 Fetch API
- 后端使用 Spring Boot 提供 REST API
- 实现增删改查功能
- 分页查询
练习 4:SPA 单页应用
实现一个简单的 SPA:
- 路由切换(手动实现)
- 页面组件化
- 状态管理
- 数据持久化
六、本课小结
| 知识点 | 说明 |
|---|---|
| XMLHttpRequest | 传统的 AJAX 实现方式 |
| Fetch API | 现代的网络请求 API(推荐) |
| async/await | 异步编程的语法糖 |
| JSON | 数据交换格式 |
| Vue.js | 渐进式 JavaScript 框架 |
| Vue 指令 | v-model、v-if、v-for、v-on 等 |
| Vue 组件 | 组件化开发,props 和 emit |
| SPA | 单页应用,前端路由 |
关键要点:
- 使用 Fetch API 替代 XMLHttpRequest
- 使用 async/await 简化异步代码
- Vue.js 是渐进式框架,可以按需引入
- 组件化开发提高代码复用性
- 注意处理网络请求的错误和加载状态
本课程持续更新中,欢迎关注!
《第28课-AJAX与前端框架》 是转载文章,点击查看原文。