第28课-AJAX与前端框架

作者:qq_26950675日期:2026/9/20

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

实现一个天气查询应用:

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

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


第28课-AJAX与前端框架》 是转载文章,点击查看原文


相关推荐


【AI大模型接入SDK】ChatGPT API
艾莉丝努力练剑2026/9/12

🎬 个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬 艾莉丝的简介: 文章目录 1 ~> OpenAI API 体系与版本演进1.1 两套核心 API 定位1.2 核心能力对比1.3 官方选型建议 2 ~> Chat Completion


最新大数据毕业设计选题推荐-基于大数据的电商与本地服务消费评论数据可视化分析-大数据-Spark-Hadoop-Bigdata
IT研究室2026/9/4

✨作者主页:IT研究室✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python项目 安卓项目 微信小程序项目 文章目录 一、前言二、开发环境三、系统界面展示四、代码参考五、系统视频结语 一、前言 系统介绍 本系统《基于大数据的电商与本地服务消费评论数据可视化分析》以Hadoop分布式文件系


MySQL DQL全面解析:从入门到精通
渣渣盟2026/8/27

​ 在数据库的广阔天地中,MySQL 凭借其开源、高效、易用等特性,成为了众多开发者的首选。而在 MySQL 的众多功能中,数据查询语言(Data Query Language,简称 DQL)无疑是最为常用且强大的部分之一。通过 DQL,我们可以从数据库中检索出所需的数据,进行各种复杂的数据分析和处理。本文将深入探讨 MySQL DQL 的各个方面,帮助你全面掌握这一重要技能。 一、DQL 基础:SELECT 语句入门 DQL 的核心是 SELECT 语句,它的基本语法如下: SELECT co


我做了一个软著 Skill,可以一键生成申请材料
IvanCodes2026/8/19

Hello,大家好,我是Ivan。 大家做完一个项目以后,可能会遇到一个问题,需要申请软著。这时候,申请表、操作手册和代码材料就都需要重新整理一遍,很麻烦,浪费时间。 所以,我把这部分做成了一个软著 Skill,名字叫 software-certificate-skill。把项目交给它以后,它会读取项目里的代码、页面和接口,然后生成申请表信息、操作手册和代码材料。 skill已经开源了,github链接是 https://github.com/IvanCodesDev/software-c


鸿蒙应用开发:V1与V2版本数据持久化实战教程
程序员黑豆2026/8/6

这是一个使用鸿蒙技术开发的本地原生记账应用,非常适合大家用来练手。相关源码已上传至 Github,点击此处查看项目。欢迎大家交流、指正,也欢迎提交 PR。 一、引言 在鸿蒙应用开发中,数据持久化是构建完整应用体验的关键一环。无论是保存用户的登录状态、个人偏好,还是缓存应用的核心数据,都需要一套可靠且高效的持久化方案。随着HarmonyOS版本的演进,数据持久化能力也从V1版本进化到了V2版本,带来了更强大的功能和更灵活的使用方式。 本文将深入对比V1(PersistentStorage + A


上线一个人静态网站需要多少钱,难不难?
五阳2026/7/28

不难,有合法身份证就能办,只需要花费 10 元,就能发布一个网站。 但是有门槛,先说门槛,如果你的网站需要中国大陆访问,那么必须要域名备案,不同省份等待时间不同。不过最多两周,最少 1 周也能审核完成。 有人问域名不备案就访问不通吗?不是的,我在阿里云申请的域名还没来得及备案,据我观察,大约有 2-3 周时间内,是允许中国大陆用户访问的,但是三周以后如果你还没有备案,那么就会被封掉,但是海外用户还能访问。 这是上线一个网站最困难和煎熬的环节了。 注意:国内个人用户备案域名,只能备案个人博客用途,


Agentic RAG 入门:从固定检索到自主决策
copyer_xyf2026/7/20

普通 RAG 的思路很直接:用户提出问题,系统从知识库检索相关片段,再让大模型根据这些片段生成回答。 这条链路解决了一个重要问题:模型不必只依赖训练时记住的知识,而是可以在回答前读取企业文档、产品手册、业务数据等外部资料,再根据资料组织答案。 但传统 RAG 的执行流程通常是固定的。这里的“固定”,不是说每次召回的文档都相同,而是说无论用户问什么、检索结果质量如何,程序都会按照上图展示的同一条路径执行。它不会在运行过程中重新判断,也不会主动改变检索策略。 但是,这种固定流程会遇到几个实际问题:


AEO 答案引擎优化:从“被搜索”到“被引用”的 AI 营销下一站
A亨日记2026/7/11

一、AI 正在改写“搜索”的默认行为 过去二十年,用户打开百度、Google 的第一件事是输入关键词,然后在十条蓝色链接里选择。但今天,越来越多的人直接向 DeepSeek、豆包、Kimi、ChatGPT 提问,并期待得到一个“直接可用”的答案。 这意味着企业的营销目标正在发生根本转移: SEO 时代:争取搜索结果页的前十排名;GEO 时代:争取被生成式引擎在回答中主动引用;AIO 时代:用结构化数据与反馈闭环持续优化 AI 对企业的“认知”;AEO 时代:直接为答案引擎生产“可被引用的高置信度


解决方案十八-企业级发音评测技术使用
Liu202605172026/7/3

在人工智能与教育深度融合的今天,语音评测技术(Speech Evaluation)已成为语言学习、在线教育、智能客服等领域的核心基础设施。无论是英语四六级口语考试、普通话水平测试,还是儿童语言启蒙应用,都离不开稳定、精准的语音评测能力。 然而,对于许多前端开发者而言,语音评测似乎总隔着一层“黑盒”——如何从前端采集音频?如何与云端AI服务建立稳定连接?如何处理流式数据并实时展示评测结果?这些问题往往让人望而却步。 本文将带你从零开始,基于Vue.js和WebSocket协议,构建一个完整的语


Claude Code上手指南:安装、部署、接入大模型
小此方2026/6/25

◆ 博主名称: 晓此方-CSDN博客 大家好,欢迎来到晓此方的博客。 ⭐️现代AI系列个人专栏: 【别传】AI应用与开发 ⭐️ “当AI能力被内化,智能就不再是成本,而是生产力。”——李彦宏 概要&序論    Hello,大家好,我是此方。 本文将带大家从零开始,完整走通 Claude Code 的安装、配置与部署流程。 一,让你的 Claude Code 动起来 1.1 安装前置软件 1

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读