news 2026/9/7 4:36:44

AJAX实战:从XMLHttpRequest到axios,蓝桥杯“组课神器”全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AJAX实战:从XMLHttpRequest到axios,蓝桥杯“组课神器”全解析

本系列第28篇,我们继续 Web 应用开发实战。很多同学在准备蓝桥杯 Web 应用开发方向时,最担心的往往不是 HTML 布局,也不是 CSS 样式,而是 AJAX 请求:按钮点击后页面没反应、后端明明有数据却渲染不出来、请求发出去了却报 404……这些问题看似零散,其实都指向同一个能力点——你能不能在前端页面中正确发起异步请求,并且拿到数据后完成页面动态更新。

本文以一道蓝桥杯真题风格的模拟题“组课神器”为例,完整拆解 AJAX 请求从理论到实战的完整链路。题目场景是:学生在前端页面选择课程方向和学习难度,页面通过 AJAX 请求向后端获取课程列表,再勾选若干课程,发送给后端进行课程时间冲突检测,并展示推荐组合结果。整个项目包含原生 XMLHttpRequest、jQuery 的$.ajax、axios 三种写法,以及 Flask 后端接口设计,覆盖了蓝桥杯 Web 应用开发中 AJAX 模块的高频考点。

无论你是刚接触 AJAX 的新手,还是准备蓝桥杯冲刺的备赛选手,这篇文章都能提供一套可以直接复制的代码模板和排查思路。

1. 题目背景与考点定位

1.1 “组课神器”是一个什么题

“组课神器”本质是一个课程组合推荐工具。学生用户打开页面后,会看到一个筛选表单,上面有“课程方向”和“难度等级”两个条件;选择并点击“查询课程”后,页面下方会动态显示符合条件的课程卡片。用户可以勾选 2 到 4 门课程,点击“智能组课”,系统会判断所选课程是否存在时间冲突,并返回组合后的总学分与推荐提示。

这个场景在真实业务里非常常见,换成“选课系统”“预约系统”“商品筛选系统”都说得通。蓝桥杯 Web 应用开发真题也经常以这类“表单查询 + 动态渲染 + 数据回传”的题目出现,考察点非常集中:

  • 前端表单数据的获取与拼接;
  • AJAX 请求的 URL、请求方法、请求参数设置;
  • 后端接口返回 JSON 数据的结构设计;
  • 前端拿到 JSON 后如何遍历渲染;
  • POST 请求中 JSON 格式数据的序列化与解析。

1.2 蓝桥杯 Web 应用开发为什么爱考 AJAX

AJAX 是现代 Web 应用前后端交互的基石。在蓝桥杯 Web 应用开发方向的题目中,静态页面只能拿基础分,真正区分水平的就是动态数据交互。真题中经常出现的“数据列表加载”“条件筛选”“异步提交”都依赖 AJAX。

同时,蓝桥杯的 Web 应用开发赛项也支持 Python Flask/Django、Java Spring 等后端技术栈,前后端分离的开发方式越来越普遍。在考试中,选手需要在规定时间内完成后端接口编写,再在前端页面中通过 AJAX 请求调用接口,并完成数据渲染。只要 AJAX 请求这一步没掌握,哪怕后端写得再完整,页面也无法展示出结果,最终得分会受到很大影响。

1.3 本文使用的技术栈与运行环境

为了让代码最小可运行,本文后端以 Python Flask 为例,Flask 2.x / 3.x 均可运行。如果你参加蓝桥杯时使用的是 Django,原理完全一致,只需要把路由和请求解析方式替换为 Django 对应写法即可。

环境说明
操作系统Windows / macOS / Linux 均可
Python3.8 及以上
Flask2.x 或 3.x
浏览器Chrome / Edge 最新版
编辑器VS Code、PyCharm 均可

版本不一定需要完全一致,重点是把 AJAX 请求的开发思路掌握清楚。

2. 先搞懂 AJAX 请求的底层逻辑

2.1 什么是 AJAX,它解决了什么问题

AJAX 全称 Asynchronous JavaScript And XML,中文叫“异步 JavaScript 和 XML”。注意,这里虽然名字里有 XML,但在实际开发中,现代接口几乎都返回 JSON 而不是 XML。

在没有 AJAX 的时代,浏览器每次向服务器请求数据,都会刷新整个页面。用户体验差,服务器压力也比较大。AJAX 出现后,页面可以通过 JavaScript 在后台悄悄发出请求,拿到服务器返回的数据后,只更新页面中的某一块区域,不用刷新整个页面。

以“组课神器”为例:用户点击“查询课程”按钮后,页面不会跳到新地址,也不会整体刷新,而是课程列表区域出现新的卡片数据。这就是 AJAX 的典型应用。

2.2 一次 AJAX 请求包含哪些关键要素

无论使用哪种 AJAX 库,一次完整的请求都包含以下关键要素:

要素作用示例
url请求的接口地址/api/courses
method / type请求方法GETPOST
data请求参数{direction: '后端开发', level: '中级'}
dataType期望的返回类型json
success请求成功后的回调渲染课程列表
error请求失败后的回调弹出错误提示

其中最容易写错的就是请求参数的类型。GET 请求的参数通常是拼在 URL 后面的查询字符串,比如:

/api/courses?direction=后端开发&level=中级

而 POST 请求的参数可以放在请求体中,常见有两种格式:

  • application/x-www-form-urlencoded,表单格式;
  • application/json,JSON 字符串格式。

这个区别直接决定了后端解析参数的方式,后面实战部分会重点演示。

2.3 同步与异步:AJAX 的“异步”到底指什么

AJAX 中的 A 是 Asynchronous,异步。异步的意思是:浏览器发出请求后,不需要傻等服务器返回结果,可以继续执行后面的 JavaScript 代码。等服务器响应返回后,浏览器再触发回调函数。

理解这一点很重要。很多同学写代码时,想在发出请求的下一行直接使用返回数据,结果发现数据是undefined。原因就是请求还没完成,代码已经继续往下执行了。

$.ajax({ url: '/api/courses', success: function (res) { console.log('请求成功,拿到数据', res); } }); console.log('这行代码会在请求完成前先执行');

这种异步特性需要一段时间适应,但只要记住“数据只能在 success 回调里使用”,就不会踩坑。后续我们也会提到,可以使用async/await把异步代码写成更接近同步阅读习惯的样式。

3. 环境准备与项目结构

3.1 搭建项目目录

先在本地创建一个项目文件夹,命名为group_course,目录结构如下:

group_course/ ├── app.py └── templates/ └── index.html

Flask 默认会从templates目录下查找模板文件,所以把前端页面放在这里。如果后续代码量大,你还可以继续拆分出static/jsstatic/css目录,但本文示例为了专注于 AJAX 逻辑,将 JavaScript 内联在 HTML 页面中。

3.2 安装 Flask

在命令行进入项目目录,创建并激活虚拟环境(推荐),然后安装 Flask:

python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install flask

安装完成后,可以先测试一下 Flask 是否能正常导入:

from flask import Flask app = Flask(__name__) print("Flask 导入成功")

只要能正常输出,就说明环境准备好了。

3.3 设计后端接口

“组课神器”需要两个接口:

接口请求方式功能
/api/coursesGET根据方向、难度筛选课程列表
/api/combinePOST接收选中的课程 id 列表,检测冲突并返回组合信息

接口返回格式统一使用如下结构:

{ "code": 0, "msg": "success", "data": {} }

其中code为业务状态码,0表示成功;msg是提示信息;data是具体业务数据。这个结构在后面讲解最佳实践时会进一步说明。

4. 原生 XMLHttpRequest 实现课程查询

4.1 为什么先学 XMLHttpRequest

虽然现在很多人直接使用 jQuery 或 axios,但蓝桥杯真题中偶尔会要求“不使用第三方库”,或者直接考察原生 JS 实现。因此先掌握原生写法非常重要。

XMLHttpRequest 是浏览器内置的对象,简称 XHR。它不需要安装任何依赖,所有现代浏览器都支持。一个最简单的 GET 请求如下:

var xhr = new XMLHttpRequest(); xhr.open('GET', '/api/courses', true); xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { var res = JSON.parse(xhr.responseText); console.log(res); } }; xhr.send();

这段代码的逻辑分成三步:

  1. 创建 XHR 对象;
  2. 调用open方法指定请求方式和地址,第三个参数true表示异步请求;
  3. 调用send发送请求,并在onreadystatechange中监听状态变化。

readyState有 0 到 4 五个值,通常只需要关心4,代表请求已完成。status是 HTTP 状态码,200代表成功。

4.2 带参数发送 GET 请求

在“组课神器”中,用户选择方向和难度后,查询参数是拼接到 URL 上的。直接使用字符串拼接很容易出错,尤其当参数里包含中文时,需要调用encodeURIComponent进行编码。

我们可以封装一个工具函数来处理参数拼接:

function buildQuery(params) { var arr = []; for (var key in params) { if (params.hasOwnProperty(key)) { arr.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); } } return arr.join('&'); }

然后查询课程列表:

function loadCourses() { var params = { direction: document.getElementById('direction').value, level: document.querySelector('input[name="level"]:checked').value }; var query = buildQuery(params); var xhr = new XMLHttpRequest(); xhr.open('GET', '/api/courses?' + query, true); xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { var res = JSON.parse(xhr.responseText); renderCourses(res.data); } }; xhr.send(); }

这里有一个常见误区:页面代码里使用了document.querySelector('input[name="level"]:checked'),如果用户没有选择任何难度,会返回null,再访问.value就会报错。所以前端在发送请求之前,应该先做空值校验。

4.3 用 XHR 发送 POST 请求,参数格式是关键

POST 请求与 GET 最大的区别在于参数放入请求体,且必须设置Content-Type。如果后端使用request.get_json()解析 JSON 数据,那么前端的Content-Type必须为application/json,并且send的参数必须是 JSON 字符串,而不是对象。

function combineCourses() { var checkedBoxes = document.querySelectorAll('input[name="courseIds"]:checked'); var ids = []; checkedBoxes.forEach(function (box) { ids.push(Number(box.value)); }); if (ids.length < 2) { alert('请至少选择两门课程'); return; } var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/combine', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { var res = JSON.parse(xhr.responseText); renderCombineResult(res.data); } }; xhr.send(JSON.stringify({ ids: ids })); }

需要注意:JSON.stringify({ ids: ids })把 JavaScript 对象转成了 JSON 字符串,Content-Type也显式设置为application/json。如果这两点缺少一个,后端很可能解析不到数据。

4.4 数据渲染:拿到 JSON 之后怎么展示

loadCourses拿到课程列表后,需要把这些数据渲染成页面上的卡片。一般思路是先用innerHTML拼接 HTML 字符串,然后一次性赋值给容器节点。

function renderCourses(courses) { var container = document.getElementById('courseList'); if (courses.length === 0) { container.innerHTML = '<p class="empty">没有符合条件的课程</p>'; return; } var html = ''; courses.forEach(function (course) { html += '<div class="course-card">'; html += '<label>'; html += '<input type="checkbox" name="courseIds" value="' + course.id + '">'; html += '<span class="course-name">' + course.name + '</span>'; html += '<span class="course-meta">' + course.direction + ' / ' + course.level + '</span>'; html += '<span class="course-credit">' + course.credit + ' 学分</span>'; html += '</label>'; html += '</div>'; }); container.innerHTML = html; }

这种拼接方式简单直接,适合比赛场景。需要注意的是,课程名称如果是用户输入的数据,直接拼接到innerHTML中可能存在 XSS 注入风险。在蓝桥杯题目中,课程数据一般是写死的模拟数据,所以问题不大;但如果是在真实业务中,就需要做 HTML 转义处理。

5. 更常用的 jQuery 与 axios 写法

5.1 引入方式

jQuery 和 axios 都是第三方库。在蓝桥杯考试环境中,通常已经允许使用本地引入或 CDN 引入。你也可以直接在页面顶部用 CDN:

<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/axios.min.js"></script>

如果比赛机器不能联网,建议提前下载到本地static/js目录,再通过相对路径引入。

5.2 jQuery 的 $.ajax 写法

jQuery 的$.ajax是封装程度较高的一种写法,蓝桥杯历年真题中出现频率很高。它的参数是一个对象,优势是可以通过typedatadataType等字段快速配置请求。

GET 请求查询课程:

$.ajax({ url: '/api/courses', type: 'GET', data: { direction: $('#direction').val(), level: $('input[name="level"]:checked').val() }, dataType: 'json', success: function (res) { renderCourses(res.data); }, error: function (xhr, status, error) { console.error('请求失败', error); } });

POST 请求提交课程组合:

$.ajax({ url: '/api/combine', type: 'POST', contentType: 'application/json', data: JSON.stringify({ ids: ids }), dataType: 'json', success: function (res) { renderCombineResult(res.data); }, error: function () { alert('提交失败,请稍后重试'); } });

如果不设置contentType: 'application/json',jQuery 默认会按表单格式application/x-www-form-urlencoded提交。此时 Flask 后端应该使用request.form.get来获取参数,而不能使用request.get_json()。这是非常经典的参数解析不一致问题。

5.3 表单快速序列化

在实际题目中,如果表单字段很多,可以使用 jQuery 的serialize()方法快速把表单内容拼成查询字符串:

var query = $('#filterForm').serialize(); // 输出类似:direction=后端开发&level=中级 $.ajax({ url: '/api/courses?' + query, type: 'GET', dataType: 'json', success: function (res) { renderCourses(res.data); } });

serialize()会自动对特殊字符进行编码,对于中文和空格都能正确处理,推荐在比赛时优先使用,能减少手写参数拼接的错误。

5.4 axios 与 async/await 写法

axios 更符合现代前端项目的写法,配合async/await后,代码阅读起来接近同步逻辑:

async function loadCourses() { try { const response = await axios.get('/api/courses', { params: { direction: document.getElementById('direction').value, level: document.querySelector('input[name="level"]:checked').value } }); renderCourses(response.data.data); } catch (err) { console.error('请求失败', err); } }

POST 请求:

async function combineCourses() { const ids = getCheckedCourseIds(); if (ids.length < 2) { alert('请至少选择两门课程'); return; } try { const response = await axios.post('/api/combine', { ids: ids }); renderCombineResult(response.data.data); } catch (err) { alert('提交失败,请稍后重试'); } }

axios 与 jQuery 的$.ajax有一个明显区别:axios 请求成功后,返回对象response中的data才是后端返回的 JSON 内容,所以代码里会连续访问两次data。这一点初学者特别容易搞混,可以把后端返回结构打印到控制台确认一下。

6. 完整案例:组课神器的前后端实现

前面把知识点拆开了,这一节给出一个可以直接运行的完整项目。后端使用 Flask,前端使用原生 JavaScript + XMLHttpRequest,并同时提供 jQuery 版本的替代代码注释说明。

6.1 后端接口完整代码

文件路径:group_course/app.py

from flask import Flask, jsonify, render_template, request app = Flask(__name__) # 模拟数据库课程表 COURSES = [ {"id": 1, "name": "HTML网页基础", "direction": "前端开发", "level": "初级", "day": 1, "period": 3, "credit": 2}, {"id": 2, "name": "CSS布局实战", "direction": "前端开发", "level": "中级", "day": 1, "period": 5, "credit": 3}, {"id": 3, "name": "JavaScript核心语法", "direction": "前端开发", "level": "中级", "day": 2, "period": 3, "credit": 4}, {"id": 4, "name": "Vue组件开发", "direction": "前端开发", "level": "高级", "day": 3, "period": 3, "credit": 4}, {"id": 5, "name": "Python程序设计", "direction": "后端开发", "level": "初级", "day": 1, "period": 3, "credit": 2}, {"id": 6, "name": "Flask接口开发", "direction": "后端开发", "level": "中级", "day": 2, "period": 5, "credit": 3}, {"id": 7, "name": "Django Web开发", "direction": "后端开发", "level": "中级", "day": 4, "period": 3, "credit": 4}, {"id": 8, "name": "数据库设计", "direction": "后端开发", "level": "高级", "day": 5, "period": 3, "credit": 4}, {"id": 9, "name": "Python数据分析", "direction": "数据开发", "level": "初级", "day": 3, "period": 5, "credit": 3}, {"id": 10, "name": "SQL查询优化", "direction": "数据开发", "level": "中级", "day": 4, "period": 5, "credit": 3}, {"id": 11, "name": "数据可视化", "direction": "数据开发", "level": "高级", "day": 5, "period": 5, "credit": 2}, {"id": 12, "name": "爬虫与数据采集", "direction": "数据开发", "level": "高级", "day": 2, "period": 3, "credit": 4}, ] @app.route("/") def index(): return render_template("index.html") @app.route("/api/courses", methods=["GET"]) def get_courses(): direction = request.args.get("direction", "全部") level = request.args.get("level", "全部") result = [] for course in COURSES: if direction != "全部" and course["direction"] != direction: continue if level != "全部" and course["level"] != level: continue result.append(course) return jsonify({"code": 0, "msg": "success", "data": result}) @app.route("/api/combine", methods=["POST"]) def combine_courses(): payload = request.get_json(silent=True) if not payload or "ids" not in payload: return jsonify({"code": 1, "msg": "参数缺失", "data": None}) ids = payload.get("ids", []) # 过滤出不存在的课程 id,避免用户传入非法参数 selected = [course for course in COURSES if course["id"] in ids] # 检测时间冲突:day 和 period 都相同则冲突 conflict = False for i in range(len(selected)): for j in range(i + 1, len(selected)): if selected[i]["day"] == selected[j]["day"] and selected[i]["period"] == selected[j]["period"]: conflict = True total_credit = sum(course["credit"] for course in selected) data = { "courses": selected, "conflict": conflict, "total_credit": total_credit, } return jsonify({"code": 0, "msg": "success", "data": data}) if __name__ == "__main__": app.run(debug=True)

后端说明:

  • request.args.get用于获取 GET 查询参数;
  • request.get_json(silent=True)用于解析 JSON 请求体,加silent=True后即使解析失败也不会抛异常,而是返回None
  • jsonify会把字典转换为 JSON 响应;
  • 时间冲突检测使用了双重循环,题目数据量小,性能完全够用。

6.2 前端页面完整代码

文件路径:group_course/templates/index.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> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: "Microsoft YaHei", "PingFang SC", sans-serif; background: #f4f6f9; padding: 30px; } .container { max-width: 900px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } h1 { font-size: 24px; margin-bottom: 6px; } .subtitle { color: #888; font-size: 14px; margin-bottom: 20px; } .filter-form { display: flex; flex-wrap: wrap; gap: 20px; align-items: center; border-bottom: 1px dashed #ddd; padding-bottom: 20px; margin-bottom: 20px; } .filter-form label { font-size: 14px; margin-right: 6px; } .filter-form select, .filter-form input[type="radio"] { cursor: pointer; } .btn { padding: 8px 18px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; background: #4a7cf7; color: #fff; transition: background 0.2s; } .btn:hover { background: #3b66db; } .btn:disabled { background: #a0b4e0; cursor: not-allowed; } .course-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 20px; } .course-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; transition: border-color 0.2s; } .course-card:hover { border-color: #4a7cf7; } .course-card label { display: flex; align-items: center; cursor: pointer; } .course-card input { margin-right: 8px; } .course-name { font-weight: 600; margin-right: auto; } .course-meta { font-size: 12px; color: #999; margin-right: 8px; } .course-credit { font-size: 12px; color: #4a7cf7; background: #eef3ff; border-radius: 4px; padding: 2px 6px; } .empty { grid-column: 1 / -1; color: #999; text-align: center; padding: 30px 0; } .result-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; } .result-box h3 { margin-bottom: 10px; font-size: 16px; } .result-item { font-size: 14px; color: #333; margin-bottom: 4px; } .tip-success { color: #16a34a; margin-top: 8px; } .tip-warning { color: #dc2626; margin-top: 8px; } </style> </head> <body> <div class="container"> <h1>组课神器</h1> <p class="subtitle">选择课程方向与难度,查看推荐课程组合</p> <div class="filter-form"> <div> <label for="direction">课程方向</label> <select id="direction"> <option value="全部">全部</option> <option value="前端开发">前端开发</option> <option value="后端开发">后端开发</option> <option value="数据开发">数据开发</option> </select> </div> <div> <label>难度等级</label> <label><input type="radio" name="level" value="全部" checked> 全部</label> <label><input type="radio" name="level" value="初级"> 初级</label> <label><input type="radio" name="level" value="中级"> 中级</label> <label><input type="radio" name="level" value="高级"> 高级</label> </div> <button class="btn" id="searchBtn">查询课程</button> </div> <div id="courseList" class="course-list"></div> <button class="btn" id="combineBtn">智能组课</button> <div id="resultBox" class="result-box" style="margin-top:20px; display: none;"> <h3>推荐结果</h3> <div id="resultContent"></div> </div> </div> <script> // 1. 构建查询字符串 function buildQuery(params) { var arr = []; for (var key in params) { if (params.hasOwnProperty(key)) { arr.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); } } return arr.join('&'); } // 2. 查询课程 function loadCourses() { var direction = document.getElementById('direction').value; var levelRadio = document.querySelector('input[name="level"]:checked'); var level = levelRadio ? levelRadio.value : '全部'; var query = buildQuery({ direction: direction, level: level }); var xhr = new XMLHttpRequest(); xhr.open('GET', '/api/courses?' + query, true); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { var res = JSON.parse(xhr.responseText); renderCourses(res.data); } else { alert('课程查询失败,请检查后端服务是否启动'); } } }; xhr.send(); } // 3. 渲染课程卡片 function renderCourses(courses) { var container = document.getElementById('courseList'); if (!courses || courses.length === 0) { container.innerHTML = '<p class="empty">没有符合条件的课程</p>'; return; } var html = ''; courses.forEach(function (course) { html += '<div class="course-card">'; html += '<label>'; html += '<input type="checkbox" name="courseIds" value="' + course.id + '">'; html += '<span class="course-name">' + course.name + '</span>'; html += '<span class="course-meta">' + course.direction + ' / ' + course.level + '</span>'; html += '<span class="course-credit">' + course.credit + ' 学分</span>'; html += '</label>'; html += '</div>'; }); container.innerHTML = html; } // 4. 获取选中的课程 id 列表 function getCheckedIds() { var boxes = document.querySelectorAll('input[name="courseIds"]:checked'); var ids = []; boxes.forEach(function (box) { ids.push(Number(box.value)); }); return ids; } // 5. 提交组合 function combineCourses() { var ids = getCheckedIds(); if (ids.length < 2) { alert('请至少选择两门课程'); return; } if (ids.length > 4) { alert('最多选择四门课程'); return; } var xhr = new XMLHttpRequest(); xhr.open('POST', '/api/combine', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { var res = JSON.parse(xhr.responseText); renderCombineResult(res.data); } }; xhr.send(JSON.stringify({ ids: ids })); } // 6. 渲染组合结果 function renderCombineResult(data) { var box = document.getElementById('resultBox'); var content = document.getElementById('resultContent'); box.style.display = 'block'; var html = ''; if (!data.courses || data.courses.length === 0) { html = '<p class="result-item">未找到有效课程</p>'; } else { data.courses.forEach(function (course) { html += '<p class="result-item">' + course.name + '(' + course.direction + ')第' + course.day + '天 第' + course.period + '节</p>'; }); html += '<p class="result-item">总学分:' + data.total_credit + '</p>'; if (data.conflict) { html += '<p class="tip-warning">课程时间存在冲突,请调整选择</p>'; } else { html += '<p class="tip-success">课程时间无冲突,推荐组合成功</p>'; } } content.innerHTML = html; } // 7. 绑定事件 document.getElementById('searchBtn').addEventListener('click', loadCourses); document.getElementById('combineBtn').addEventListener('click', combineCourses); // 页面加载时默认展示全部课程 loadCourses(); </script> </body> </html>

这段代码里需要注意两个细节:

  • 使用input[name="courseIds"]:checked选择被勾选的复选框,这与 HTML 中复选框的name属性一一对应;
  • Number(box.value)把字符串类型的复选框值转换为数字,避免后续后端比对 id 时出现类型不一致的问题。

6.3 运行与验证

在项目目录下执行:

python app.py

浏览器访问http://127.0.0.1:5000,页面上会默认显示全部课程。选择“后端开发”和“中级”,点击“查询课程”,页面会立即刷新课程列表,只显示两门课程:Flask 接口开发 和 Django Web 开发。

再勾选这两门课程,点击“智能组课”,页面下方会显示推荐结果:

Flask 接口开发(后端开发)第 2 天 第 5 节 Django Web 开发(后端开发)第 4 天 第 3 节 总学分:7 课程时间无冲突,推荐组合成功

如果选择“HTML网页基础”和“Python程序设计”,两门课都是第 1 天第 3 节,就会提示“课程时间存在冲突,请调整选择”。这就是后端冲突检测逻辑生效了。

6.4 打开浏览器开发者工具观察请求

按 F12 打开开发者工具,切换到 Network 面板,再点击“查询课程”。可以看到浏览器发出了一条GET请求,地址为/api/courses?direction=后端开发&level=中级。点击该请求,在 Payload 或 Query String Parameters 中能看到完整参数。

再点击“智能组课”,可以看到一条POST /api/combine请求,请求头中带有Content-Type: application/json,请求体是:

{"ids": [6, 7]}

这个面板是排查 AJAX 问题的第一现场。如果页面没有按预期更新,优先看 Network 面板里请求的状态码、请求参数和响应内容。

7. 常见问题与排查思路

蓝桥杯比赛时间紧张,遇到问题如果反复试错会浪费大量时间。下面整理了几类 AJAX 请求的高频问题,你可以按表排查。

问题现象常见原因解决思路
点击按钮后页面无任何反应JavaScript 报错,事件未绑定F12 打开 Console 面板,查看报错行
请求返回 404URL 路径写错,或后端路由未定义对比 Network 中的请求地址与 Flask 路由
返回 JSON 字符串,前端无法渲染忘记JSON.parse,或 dataType 设置不一致确认xhr.responseText后先JSON.parse
POST 请求返回 400 / 后端收到 NoneContent-Type 不是application/json检查请求头,使用setRequestHeader设置
后端拿到参数但返回结果不对参数名不一致打印request.argspayload,逐一比对
页面中文显示乱码HTML 没有<meta charset="UTF-8">在 HTML head 中补充编码声明
请求出现 CORS 跨域报错前端页面与接口不在同源地址开发时统一通过 Flask 服务访问,或配置 CORS
按钮重复点击产生大量请求没有做防重复提交点击后禁用按钮,请求完成后恢复
异步请求拿到数据时页面已经执行后面代码不理解异步回调把数据渲染逻辑放入 success 回调中

其中“POST 请求后端收到 None”是最常见的问题,根本原因通常是前端没有设置Content-Type: application/json。你可以把请求头打印出来,或者在后端接口中临时加一行:

print(request.headers.get("Content-Type"))

如果输出不是application/json,那就可以确定是请求头的问题。

8. 蓝桥杯备考与工程实践建议

8.1 比赛时优先保证“请求能通”

蓝桥杯 Web 应用开发是限时比赛,代码规范性和性能优化不是第一优先级。拿到题目后,先保证前后端接口能连通,再考虑代码优雅性。

我建议按这个顺序检查:

  1. 后端路由能否直接通过浏览器访问;
  2. 前端 AJAX 请求地址与路由是否完全一致(注意大小写和斜杠);
  3. 请求参数名是否与后端解析字段一致;
  4. 返回 JSON 是否与前端渲染字段一致;
  5. 页面渲染是否完成预期效果。

只要这个链路跑通,题目就已经拿下了大部分分数。

8.2 封装统一的 AJAX 工具函数

在真实项目和复赛中,建议把 AJAX 请求封装成一个函数,避免每个页面重复写 XMLHttpRequest 模板代码。下面是一个简单的封装示例:

function ajax(options) { var xhr = new XMLHttpRequest(); xhr.open(options.method || 'GET', options.url, true); if (options.headers) { for (var key in options.headers) { xhr.setRequestHeader(key, options.headers[key]); } } xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { var data = JSON.parse(xhr.responseText); options.success && options.success(data); } else { options.error && options.error(xhr); } } }; var body = null; if (options.data) { if (options.contentType === 'application/json') { xhr.setRequestHeader('Content-Type', 'application/json'); body = JSON.stringify(options.data); } else { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); body = buildQuery(options.data); } } xhr.send(body); }

使用方式:

ajax({ url: '/api/courses', method: 'GET', data: { direction: '后端开发', level: '中级' }, success: function (res) { renderCourses(res.data); }, error: function () { alert('请求失败'); } });

这样页面中所有请求都能保持一致的书写风格,排查问题也更方便。

8.3 统一后端返回格式

前后端分离开发时,接口返回格式一定要统一。推荐使用{ code, msg, data }结构。前端只判断code是否为 0,再读取data渲染页面。这样即使后端内部逻辑变化,前端代码也不需要大改。

如果code非 0,比如参数缺失、未登录、数据不存在,前端可以直接根据msg提示用户,错误处理逻辑也会简单很多。

8.4 安全边界不能忘

虽然蓝桥杯题目相对简单,但写代码时仍然要养成基本的安全意识:

  • 后端接口不要信任前端传入的 id,要做存在性校验;
  • 对传入参数做类型判断,避免None类型的后续访问报错;
  • 动态拼接 HTML 时,如果是用户可控数据,要做 HTML 转义;
  • 课程组合、订单提交这类接口,涉及写操作时建议使用 POST 而不是 GET;
  • 如果接口涉及用户数据,生产环境中必须增加身份认证和权限校验。

这些习惯不一定能在比赛评分中直接体现,但能帮助你在实际项目中避免大量线上故障。

8.5 优先掌握一种写法,再扩展其他写法

原生 XMLHttpRequest、jQuery 的$.ajax、axios、fetch 这几种方式背后原理相同,区别只是封装程度。建议先把原生 XHR 写熟,再用 jQuery 和 axios 提高开发效率。考试时,优先使用自己最熟悉、最有把握的写法,不要为了展示技术栈去用不熟悉的库。

如果时间允许,可以继续学习FormData文件上传、async/await异步流程控制、JSONP 跨域原理等扩展内容。但就蓝桥杯 Web 应用开发这个方向而言,把 GET 请求和 POST 请求掌握扎实,已经能够覆盖大多数真题场景。

备赛阶段,建议手边放几个往年真题,把题目中所有的数据请求都改成 AJAX 实现,并在浏览器 Network 面板中观察请求参数和响应内容。练得多了,AJAX 的常见坑自然就心里有数了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 4:34:08

BMS接口技术需求全解析:从硬件选型到协议测试

简介&#xff1a;《BMS与其他系统接口技术需求.doc》是一份面向智能楼宇和智慧建筑项目的系统集成工程师、弱电设计师及BMS调试人员的技术需求文档&#xff0c;基于IOTOS物联网平台系统梳理了BMS与消防报警、楼宇自控、视频监控、入侵报警、出入口控制、车库管理、能源管理及一…

作者头像 李华
网站建设 2026/9/7 4:33:16

弹性容错防线全景:多维限流、双桶平滑、熔断与四级降级

弹性容错防线全景&#xff1a;多维限流、双桶平滑、熔断与四级降级在微服务与高可用分布式架构中&#xff0c;“容错与弹性&#xff08;Resilience & Fault Tolerance&#xff09;”是保障系统在极端网络抖动、流量洪峰与下游依赖瘫痪时依然能够屹立不倒的核心生命线。 然而…

作者头像 李华
网站建设 2026/9/7 4:33:05

《Turing Complete》2.1版:从NAND门到CPU的硬核通关与全成就指南

如果你在Steam上翻过“硬核编程”或者“逻辑”这类标签&#xff0c;大概率早就撞见过《Turing Complete》这个名字。它不是那种点两下鼠标就开心的休闲益智&#xff0c;而是一个让你从一枚最底层的NAND门开始&#xff0c;一步步搭出内存、ALU、寄存器、指令集&#xff0c;最后造…

作者头像 李华
网站建设 2026/9/7 4:32:42

国产开关机芯片替代选型:关键参数对比与实测验证指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 4:32:10

Tampermonkey离线安装包全攻略:无网环境安装篡改猴

简介&#xff1a;篡改猴&#xff08;Tampermonkey&#xff09;5.1.0 离线安装包是为需要使用用户脚本管理器的浏览器用户准备的离线部署工具&#xff0c;尤其适合无法访问应用商店、身处内网环境或希望固定版本的前端爱好者和普通网友。资源按 zip 压缩包分发&#xff0c;体积仅…

作者头像 李华
网站建设 2026/9/7 4:31:43

Grok Build与Quo插件实战:快速搭建短信与通话记录管理平台

前几天帮一个小团队梳理客户沟通流程&#xff0c;发现他们的通讯数据乱得惊人&#xff1a;销售手机上存着几十条未读的客户短信&#xff0c;客服的电脑上要打开三个页面才能查到一通来电的记录&#xff0c;谁给哪个客户回没回电话完全靠记忆。我本来想直接写一套后端服务接短信…

作者头像 李华