第28课-AJAX与前端框架

第 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 天气应用

实现一个天气查询应用:

  1. 使用 Fetch API 获取天气数据
  2. 支持城市搜索
  3. 显示当前天气和未来预报
  4. 错误处理和加载状态

练习 2:Vue.js 购物车

实现一个购物车应用:

  1. 商品列表展示
  2. 添加/移除商品
  3. 修改数量
  4. 计算总价
  5. 使用 Vue.js 组件化

练习 3:前后端交互

实现一个完整的用户管理系统:

  1. 前端使用 Fetch API
  2. 后端使用 Spring Boot 提供 REST API
  3. 实现增删改查功能
  4. 分页查询

练习 4:SPA 单页应用

实现一个简单的 SPA:

  1. 路由切换(手动实现)
  2. 页面组件化
  3. 状态管理
  4. 数据持久化

六、本课小结

知识点 说明
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 是渐进式框架,可以按需引入
  • 组件化开发提高代码复用性
  • 注意处理网络请求的错误和加载状态

本课程持续更新中,欢迎关注!

相关推荐
程序员清风1 小时前
Java 后端如何接入大语言模型
java·spring boot·架构·aigc
xieliyu.1 小时前
JVM 垃圾回收机制详解:从标记过程、回收算法到垃圾收集器
java·jvm·笔记·java-ee
JavacKaka1 小时前
Redis大Key导致生产事故复盘博客-2026-09-18
java
右耳朵猫AI1 小时前
Java周刊2026W38 | Micronaut 修补三漏洞、JDK 27 提速 54%、Jetty 修复抖动测试
java·后端·spring
事已至此先睡覺吧1 小时前
第二篇:Java 基础语法:变量、数据类型、运算符与类型转换
java
Wang's Blog2 小时前
Java 项目实战: 外卖平台-后台退出功能与首页iframe架构
java·服务器·redis
Wang's Blog2 小时前
Java 项目实战: 外卖平台-软件开发流程与项目整体介绍
java·服务器·redis
她说..2 小时前
常见设计模式-模板方法模式
java·spring·设计模式·springboot
步行cgn2 小时前
BeanFactory 与 FactoryBean 的区别:面试深度解析
java·后端·spring