第 28 课:AJAX 与前端框架
本课目标:掌握 AJAX 异步请求、Fetch API、async/await 语法,了解 Vue.js 基础用法和 SPA 单页应用概念,能够实现前后端数据交互。
一、概念讲解
1.1 什么是 AJAX
AJAX(Asynchronous JavaScript And XML)异步 JavaScript 和 XML,是一种在无需重新加载整个页面的情况下,与服务器交换数据并更新部分网页内容的技术。
1.2 AJAX 工作流程
┌─────────────────────────────────────────────────────┐
│ 传统请求流程 │
│ 用户操作 → 发送请求 → 等待服务器 → 返回整个页面 → 刷新 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ AJAX 请求流程 │
│ 用户操作 → 发送异步请求 → 服务器处理 │
│ ↑ ↓ │
│ └──── 更新部分页面 ← 返回 JSON 数据 │
└─────────────────────────────────────────────────────┘
1.3 前后端交互模型
┌─────────────────────────────────────────────────────┐
│ 前端 (浏览器) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HTML │ │ CSS │ │ JavaScript │ │
│ │ 页面结构 │ │ 页面样式 │ │ 交互逻辑 │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ AJAX/Fetch │ │
│ └──────┬──────┘ │
└───────────────────────────────────────────┼─────────┘
│ HTTP
┌───────┴───────┐
│ API │
│ 服务器 │
└───────┬───────┘
│
┌───────┴───────┐
│ 数据库 │
└───────────────┘
二、语法格式
2.1 XMLHttpRequest
javascript
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
// 配置请求
xhr.open('GET', '/api/users', true);
// 设置回调
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // 请求完成
if (xhr.status === 200) { // 成功
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error('请求失败:', xhr.status);
}
}
};
// 发送请求
xhr.send();
// POST 请求
xhr.open('POST', '/api/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ name: '张三', age: 20 }));
▶ 运行结果:
XMLHttpRequest 请求效果:
┌─────────────────────────────────────┐
│ GET 请求: │
│ → 发送请求到 /api/users │
│ → readyState 变化: 0→1→2→3→4 │
│ → status: 200 (成功) │
│ → 控制台输出用户数据数组 │
│ │
│ POST 请求: │
│ → 发送请求到 /api/users │
│ → 请求体: {"name":"张三","age":20} │
│ → 服务器创建新用户并返回响应 │
└─────────────────────────────────────┘
2.2 Fetch API(推荐)
javascript
// GET 请求
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// POST 请求
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: '张三', age: 20 })
})
.then(response => response.json())
.then(data => console.log(data));
// DELETE 请求
fetch('/api/users/1', {
method: 'DELETE'
})
.then(response => response.json());
▶ 运行结果:
Fetch API 请求效果:
┌─────────────────────────────────────┐
│ GET 请求: │
│ → fetch('/api/users') │
│ → 返回 Promise 对象 │
│ → 控制台输出用户数据数组 │
│ │
│ POST 请求: │
│ → fetch('/api/users', {...}) │
│ → 发送 JSON 数据到服务器 │
│ → 控制台输出创建的用户数据 │
│ │
│ DELETE 请求: │
│ → fetch('/api/users/1', {...}) │
│ → 删除 ID 为 1 的用户 │
│ → 控制台输出删除结果 │
└─────────────────────────────────────┘
2.3 async/await
javascript
// async 函数
async function getUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取用户失败:', error);
}
}
// 调用
getUsers().then(users => console.log(users));
// POST 请求
async function createUser(user) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
return await response.json();
} catch (error) {
console.error('创建用户失败:', error);
}
}
▶ 运行结果:
async/await 请求效果:
┌─────────────────────────────────────┐
│ getUsers() 调用: │
│ → 等待 fetch 完成 │
│ → 等待 response.json() 完成 │
│ → 返回用户数据数组 │
│ → 控制台输出用户列表 │
│ │
│ createUser(user) 调用: │
│ → 等待 fetch POST 请求完成 │
│ → 等待 response.json() 完成 │
│ → 返回创建的用户数据 │
│ │
│ 错误处理: │
│ → 如果网络错误,catch 捕获错误 │
│ → 控制台输出 "获取用户失败: ..." │
└─────────────────────────────────────┘
2.4 JSON 格式
javascript
// JSON 转 JavaScript 对象
const jsonString = '{"name": "张三", "age": 20}';
const obj = JSON.parse(jsonString);
// JavaScript 对象转 JSON
const user = { name: '张三', age: 20 };
const json = JSON.stringify(user);
// '{"name":"张三","age":20}'
▶ 运行结果:
JSON 操作效果:
┌─────────────────────────────────────┐
│ JSON 转对象: │
│ jsonString = '{"name":"张三","age":20}'│
│ JSON.parse(jsonString) │
│ → { name: "张三", age: 20 } │
│ │
│ 对象转 JSON: │
│ user = { name: "张三", age: 20 } │
│ JSON.stringify(user) │
│ → '{"name":"张三","age":20}' │
└─────────────────────────────────────┘
2.5 Vue.js 基础
html
<!-- 引入 Vue.js -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<div id="app">
<h1>{{ message }}</h1>
<p>计数: {{ count }}</p>
<button @click="increment">+1</button>
</div>
<script>
const { createApp } = Vue;
createApp({
// 数据
data() {
return {
message: 'Hello Vue!',
count: 0
}
},
// 方法
methods: {
increment() {
this.count++;
}
},
// 计算属性
computed: {
doubleCount() {
return this.count * 2;
}
},
// 侦听器
watch: {
count(newVal, oldVal) {
console.log(`计数从 ${oldVal} 变为 ${newVal}`);
}
},
// 生命周期钩子
mounted() {
console.log('组件已挂载');
}
}).mount('#app');
</script>
2.6 Vue.js 指令
html
<div id="app">
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- 双向绑定 -->
<input v-model="name" placeholder="请输入姓名">
<p>你好, {{ name }}</p>
<!-- 条件渲染 -->
<p v-if="isLoggedIn">欢迎回来!</p>
<p v-else>请登录</p>
<!-- 列表渲染 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
<!-- 事件绑定 -->
<button @click="handleClick">点击</button>
<!-- 属性绑定 -->
<img :src="imageUrl" :alt="imageAlt">
<!-- 类名绑定 -->
<div :class="{ active: isActive, 'text-bold': isBold }"></div>
<!-- 样式绑定 -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
</div>
2.7 Vue.js 组件
html
<div id="app">
<user-card name="张三" age="20"></user-card>
<user-card name="李四" age="22"></user-card>
</div>
<script>
const { createApp } = Vue;
// 定义组件
const UserCard = {
props: ['name', 'age'],
template: `
<div class="card">
<h3>{{ name }}</h3>
<p>年龄: {{ age }}</p>
<button @click="sayHello">打招呼</button>
</div>
`,
methods: {
sayHello() {
alert(`你好, 我是${this.name}`);
}
}
};
// 创建应用并注册组件
const app = createApp({});
app.component('user-card', UserCard);
app.mount('#app');
</script>
三、代码案例
案例 1:AJAX 登录表单
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AJAX 登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
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; }
.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); }
.login-box h2 { text-align: center; margin-bottom: 30px; color: #333; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 8px; color: #555; font-size: 14px; }
.form-group input {
width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; transition: border-color 0.3s;
}
.form-group input:focus { outline: none; border-color: #667eea; }
.login-btn {
width: 100%; padding: 14px; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; transition: background 0.3s;
}
.login-btn:hover { background: #5a6fd6; }
.login-btn:disabled { background: #ccc; cursor: not-allowed; }
.message { text-align: center; margin-top: 15px; padding: 10px; border-radius: 6px; display: none; }
.message.success { display: block; background: #d4edda; color: #155724; }
.message.error { display: block; background: #f8d7da; color: #721c24; }
.loading { display: none; text-align: center; }
.loading.show { display: block; }
</style>
</head>
<body>
<div class="login-box">
<h2>用户登录</h2>
<form id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" placeholder="请输入用户名" required>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="login-btn" id="loginBtn">登录</button>
</form>
<div class="loading" id="loading">
<p>⏳ 登录中...</p>
</div>
<div class="message" id="message"></div>
</div>
<script>
const loginForm = document.getElementById('loginForm');
const loginBtn = document.getElementById('loginBtn');
const loading = document.getElementById('loading');
const message = document.getElementById('message');
// 方式一:使用 XMLHttpRequest
function loginWithXHR(username, password) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/login', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('登录失败: ' + xhr.status));
}
}
};
xhr.onerror = function() {
reject(new Error('网络错误'));
};
xhr.send(JSON.stringify({ username, password }));
});
}
// 方式二:使用 Fetch API(推荐)
async function loginWithFetch(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error('登录失败: ' + response.status);
}
return await response.json();
}
// 显示消息
function showMessage(text, type) {
message.textContent = text;
message.className = 'message ' + type;
}
// 表单提交
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// 显示加载状态
loginBtn.disabled = true;
loading.classList.add('show');
message.className = 'message';
try {
// 使用 Fetch API 登录
const result = await loginWithFetch(username, password);
if (result.success) {
showMessage('登录成功!欢迎回来, ' + result.user.name, 'success');
// 存储 token
localStorage.setItem('token', result.token);
// 跳转到主页
// window.location.href = '/dashboard';
} else {
showMessage(result.message || '用户名或密码错误', 'error');
}
} catch (error) {
showMessage('登录失败: ' + error.message, 'error');
} finally {
loginBtn.disabled = false;
loading.classList.remove('show');
}
});
</script>
</body>
</html>
案例 2:Fetch API 数据加载
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户列表</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", sans-serif; background: #f5f5f5; padding: 30px 20px; }
.container { max-width: 900px; margin: 0 auto; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
.header h1 { color: #333; }
.refresh-btn {
padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;
}
.search-box { margin-bottom: 20px; }
.search-box input {
width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;
}
.user-list { display: grid; gap: 15px; }
.user-card {
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;
}
.user-card:hover { transform: translateY(-3px); }
.avatar {
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;
}
.user-info { flex: 1; }
.user-name { font-size: 18px; color: #333; margin-bottom: 5px; }
.user-email { color: #666; font-size: 14px; }
.user-phone { color: #999; font-size: 13px; margin-top: 5px; }
.user-actions { display: flex; gap: 10px; }
.user-actions button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
.edit-btn { background: #ffa502; color: white; }
.delete-btn { background: #ff4757; color: white; }
.loading { text-align: center; padding: 50px; color: #999; }
.error { text-align: center; padding: 50px; color: #ff4757; }
.empty { text-align: center; padding: 50px; color: #999; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>用户列表</h1>
<button class="refresh-btn" onclick="loadUsers()">刷新</button>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索用户..." oninput="filterUsers()">
</div>
<div id="userList" class="user-list">
<div class="loading">⏳ 加载中...</div>
</div>
</div>
<script>
let allUsers = [];
// 模拟 API 数据
const mockUsers = [
{ id: 1, name: '张三', email: 'zhangsan@email.com', phone: '13800138001' },
{ id: 2, name: '李四', email: 'lisi@email.com', phone: '13800138002' },
{ id: 3, name: '王五', email: 'wangwu@email.com', phone: '13800138003' },
{ id: 4, name: '赵六', email: 'zhaoliu@email.com', phone: '13800138004' },
{ id: 5, name: '钱七', email: 'qianqi@email.com', phone: '13800138005' }
];
// 模拟 API 延迟
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 获取用户列表
async function fetchUsers() {
await delay(800); // 模拟网络延迟
// 模拟 API 响应
return {
success: true,
data: mockUsers
};
}
// 加载用户
async function loadUsers() {
const userList = document.getElementById('userList');
userList.innerHTML = '<div class="loading">⏳ 加载中...</div>';
try {
const result = await fetchUsers();
if (result.success) {
allUsers = result.data;
renderUsers(allUsers);
} else {
userList.innerHTML = '<div class="error">❌ 加载失败</div>';
}
} catch (error) {
userList.innerHTML = `<div class="error">❌ 网络错误: ${error.message}</div>`;
}
}
// 渲染用户列表
function renderUsers(users) {
const userList = document.getElementById('userList');
if (users.length === 0) {
userList.innerHTML = '<div class="empty">🔍 没有找到匹配的用户</div>';
return;
}
userList.innerHTML = users.map(user => `
<div class="user-card" data-id="${user.id}">
<div class="avatar">${user.name.charAt(0)}</div>
<div class="user-info">
<div class="user-name">${user.name}</div>
<div class="user-email">📧 ${user.email}</div>
<div class="user-phone">📱 ${user.phone}</div>
</div>
<div class="user-actions">
<button class="edit-btn" onclick="editUser(${user.id})">编辑</button>
<button class="delete-btn" onclick="deleteUser(${user.id})">删除</button>
</div>
</div>
`).join('');
}
// 搜索过滤
function filterUsers() {
const keyword = document.getElementById('searchInput').value.toLowerCase();
const filtered = allUsers.filter(user =>
user.name.toLowerCase().includes(keyword) ||
user.email.toLowerCase().includes(keyword)
);
renderUsers(filtered);
}
// 编辑用户
function editUser(id) {
const user = allUsers.find(u => u.id === id);
alert(`编辑用户: ${user.name}`);
}
// 删除用户
async function deleteUser(id) {
if (!confirm('确定要删除该用户吗?')) return;
try {
// 模拟删除 API
await delay(300);
allUsers = allUsers.filter(u => u.id !== id);
renderUsers(allUsers);
alert('删除成功!');
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 初始化加载
loadUsers();
</script>
</body>
</html>
案例 3:Vue.js 计数器应用
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue.js 计数器</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
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; }
.app { background: white; border-radius: 16px; padding: 40px; width: 100%; max-width: 450px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
.app h1 { text-align: center; color: #333; margin-bottom: 30px; }
.counter { text-align: center; margin-bottom: 30px; }
.counter-value { font-size: 72px; font-weight: bold; color: #667eea; transition: color 0.3s; }
.counter-value.positive { color: #2ed573; }
.counter-value.negative { color: #ff4757; }
.counter-btns { display: flex; gap: 15px; justify-content: center; margin-bottom: 30px; }
.counter-btns button {
padding: 15px 30px; border: none; border-radius: 10px; font-size: 18px; cursor: pointer; transition: transform 0.2s;
}
.counter-btns button:hover { transform: scale(1.05); }
.counter-btns .minus { background: #ff4757; color: white; }
.counter-btns .plus { background: #2ed573; color: white; }
.counter-btns .reset { background: #ffa502; color: white; }
.history { margin-top: 20px; }
.history h3 { color: #333; margin-bottom: 15px; }
.history-list { list-style: none; max-height: 150px; overflow-y: auto; }
.history-list li { padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 14px; color: #666; }
.stats { display: flex; justify-content: space-around; margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
.stat-item { text-align: center; }
.stat-value { font-size: 24px; font-weight: bold; color: #667eea; }
.stat-label { font-size: 12px; color: #999; margin-top: 5px; }
</style>
</head>
<body>
<div id="app" class="app">
<h1>Vue.js 计数器</h1>
<div class="counter">
<div :class="['counter-value', { positive: count > 0, negative: count < 0 }]">
{{ count }}
</div>
</div>
<div class="counter-btns">
<button class="minus" @click="decrement">-1</button>
<button class="reset" @click="reset">重置</button>
<button class="plus" @click="increment">+1</button>
</div>
<div class="counter-btns" style="margin-top: -15px;">
<button class="minus" @click="decrementBy(5)">-5</button>
<button class="plus" @click="incrementBy(5)">+5</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value">{{ count }}</div>
<div class="stat-label">当前值</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ doubleCount }}</div>
<div class="stat-label">双倍值</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ history.length }}</div>
<div class="stat-label">操作次数</div>
</div>
</div>
<div class="history" v-if="history.length > 0">
<h3>操作历史</h3>
<ul class="history-list">
<li v-for="(item, index) in history" :key="index">
{{ item.action }}: {{ item.from }} → {{ item.to }}
</li>
</ul>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
count: 0,
history: []
}
},
computed: {
doubleCount() {
return this.count * 2;
}
},
watch: {
count(newVal, oldVal) {
const action = newVal > oldVal ? '增加' : '减少';
this.history.unshift({
action,
from: oldVal,
to: newVal
});
// 只保留最近 10 条记录
if (this.history.length > 10) {
this.history.pop();
}
}
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
incrementBy(value) {
this.count += value;
},
decrementBy(value) {
this.count -= value;
},
reset() {
this.count = 0;
}
},
mounted() {
console.log('Vue.js 计数器已启动');
}
}).mount('#app');
</script>
</body>
</html>
案例 4:Vue.js Todo 应用
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue.js Todo</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
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; }
.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); }
.todo-app h1 { text-align: center; color: #333; margin-bottom: 25px; }
.input-group { display: flex; gap: 10px; margin-bottom: 20px; }
.input-group input {
flex: 1; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;
}
.input-group input:focus { outline: none; border-color: #667eea; }
.input-group button {
padding: 12px 24px; background: #667eea; color: white; border: none; border-radius: 8px; cursor: pointer;
}
.filters { display: flex; gap: 10px; margin-bottom: 20px; }
.filter-btn {
padding: 8px 16px; border: none; border-radius: 20px; cursor: pointer; background: #f0f0f0; color: #666; font-size: 13px;
}
.filter-btn.active { background: #667eea; color: white; }
.todo-list { list-style: none; }
.todo-item {
display: flex; align-items: center; gap: 12px; padding: 15px; border-bottom: 1px solid #f0f0f0; transition: background 0.2s;
}
.todo-item:hover { background: #f9f9f9; }
.todo-item input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; accent-color: #667eea; }
.todo-item span { flex: 1; font-size: 15px; color: #333; }
.todo-item.completed span { text-decoration: line-through; color: #999; }
.delete-btn { padding: 5px 10px; background: #ff4757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
.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; }
.clear-btn { padding: 8px 16px; background: none; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; color: #666; font-size: 13px; }
.empty { text-align: center; padding: 40px; color: #999; }
</style>
</head>
<body>
<div id="app" class="todo-app">
<h1>📝 Vue.js Todo</h1>
<div class="input-group">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新的待办事项..."
>
<button @click="addTodo">添加</button>
</div>
<div class="filters">
<button
v-for="filter in filters"
:key="filter.value"
:class="['filter-btn', { active: currentFilter === filter.value }]"
@click="currentFilter = filter.value"
>
{{ filter.label }}
</button>
</div>
<ul class="todo-list" v-if="filteredTodos.length > 0">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="['todo-item', { completed: todo.completed }]"
>
<input type="checkbox" v-model="todo.completed">
<span>{{ todo.text }}</span>
<button class="delete-btn" @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<div v-else class="empty">
🎉 没有待办事项
</div>
<div class="footer" v-if="todos.length > 0">
<span>{{ activeCount }} 个待完成</span>
<button class="clear-btn" @click="clearCompleted">清除已完成</button>
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
newTodo: '',
todos: JSON.parse(localStorage.getItem('vue-todos')) || [],
currentFilter: 'all',
filters: [
{ label: '全部', value: 'all' },
{ label: '未完成', value: 'active' },
{ label: '已完成', value: 'completed' }
]
}
},
computed: {
filteredTodos() {
switch (this.currentFilter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
},
activeCount() {
return this.todos.filter(t => !t.completed).length;
}
},
watch: {
todos: {
handler(newVal) {
localStorage.setItem('vue-todos', JSON.stringify(newVal));
},
deep: true
}
},
methods: {
addTodo() {
const text = this.newTodo.trim();
if (!text) return;
this.todos.push({
id: Date.now(),
text: text,
completed: false
});
this.newTodo = '';
},
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
},
clearCompleted() {
this.todos = this.todos.filter(t => !t.completed);
}
}
}).mount('#app');
</script>
</body>
</html>
四、常见错误
错误 1:忘记处理 Promise
javascript
// 错误:没有处理 Promise 错误
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
// 如果请求失败,不会有错误提示
// 正确:添加 catch 处理
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
错误 2:async/await 忘记 try-catch
javascript
// 错误:没有错误处理
async function getData() {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}
// 正确:使用 try-catch
async function getData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('HTTP error! status: ' + response.status);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
错误 3:Vue.js 响应式数据问题
javascript
// 错误:直接修改数组索引
this.todos[0].completed = true; // 不会触发视图更新
// 正确:使用 splice 或 Vue.set
this.todos.splice(0, 1, { ...this.todos[0], completed: true });
// 或者
import { set } from 'vue';
set(this.todos[0], 'completed', true);
错误 4:CORS 跨域问题
javascript
// 前端请求被浏览器阻止
fetch('http://api.example.com/data')
.then(response => response.json())
.catch(error => console.error('CORS error:', error));
// 解决方案:
// 1. 后端设置 CORS 头
// Access-Control-Allow-Origin: http://localhost:3000
// 2. 使用代理
// 在开发服务器配置 proxy
错误 5:Vue.js 组件通信错误
javascript
// 错误:在子组件中直接修改 props
props: ['count'],
methods: {
increment() {
this.count++; // 不会生效,Vue 会警告
}
}
// 正确:使用 emit 触发事件
props: ['count'],
methods: {
increment() {
this.$emit('update:count', this.count + 1);
}
}
五、课后练习
练习 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 是渐进式框架,可以按需引入
- 组件化开发提高代码复用性
- 注意处理网络请求的错误和加载状态
本课程持续更新中,欢迎关注!