JavaWeb 30 天入门:第二十一天 ——AJAX 异步交互技术

在前二十天的学习中,我们掌握了 JavaWeb 开发的核心技术,包括 Servlet、JSP、会话管理、过滤器、监听器、文件操作、数据库交互、连接池、分页与排序等。今天我们将学习一项彻底改变 Web 应用交互方式的技术 ------AJAX(Asynchronous JavaScript and XML)

传统的 Web 应用中,每次数据交互都需要刷新整个页面,用户体验较差。AJAX 通过在后台与服务器进行异步数据交换,使网页可以在不重新加载整个页面的情况下,实现部分内容的更新。这项技术是现代 Web 应用(如 Gmail、Facebook、微博等)实现流畅用户体验的基础。

AJAX 概述

什么是 AJAX

AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。

  • Asynchronous(异步):指与服务器通信时,浏览器不需要暂停等待服务器响应,可以继续执行其他操作
  • JavaScript:核心编程语言,用于发送请求、处理响应和更新页面
  • And:连接词
  • XML:早期主要用于数据交换的格式,现在 JSON 更常用

AJAX 的核心是XMLHttpRequest 对象(XHR),它允许浏览器与服务器进行异步通信。

AJAX 的工作原理

AJAX 的工作流程如下:

  1. 用户在网页上执行某个操作(如点击按钮、输入文本)
  2. JavaScript 捕获该事件,创建 XMLHttpRequest 对象
  3. XMLHttpRequest 对象向服务器发送异步请求
  4. 服务器处理请求,返回数据(通常是 JSON 或 XML 格式)
  5. JavaScript 接收服务器返回的数据
  6. JavaScript 更新网页的部分内容,而无需重新加载整个页面

AJAX 的优势

  1. 提升用户体验:无需刷新整个页面,减少等待时间和视觉干扰
  2. 减少数据传输:只传输需要更新的数据,节省带宽
  3. 提高交互性:可以实现实时验证、自动完成等高级交互功能
  4. 减轻服务器负担:部分数据处理可以在客户端完成
  5. 支持离线功能:结合现代 API 可以实现数据本地存储和离线操作

AJAX 的应用场景

  1. 表单实时验证(如用户名是否已存在)
  2. 动态加载数据(如下拉列表联动、滚动加载更多)
  3. 实时搜索建议(输入时自动提示匹配结果)
  4. 无刷新分页和排序
  5. 实时数据展示(如股票行情、在线聊天)
  6. 文件上传进度显示

XMLHttpRequest 对象

XMLHttpRequest(XHR)是 AJAX 的核心对象,用于在后台与服务器交换数据。

创建 XHR 对象

不同浏览器创建 XHR 对象的方式略有差异,标准写法如下:

复制代码
// 创建XMLHttpRequest对象
function createXHR() {
    var xhr;
    if (window.XMLHttpRequest) {
        // 现代浏览器(IE7+、Firefox、Chrome、Safari等)
        xhr = new XMLHttpRequest();
    } else {
        // 兼容IE6及以下版本
        xhr = new ActiveXObject("Microsoft.XMLHTTP");
    }
    return xhr;
}

XHR 对象的常用属性

属性 描述
readyState 请求的状态码:0 - 未初始化,1 - 服务器连接已建立,2 - 请求已接收,3 - 请求处理中,4 - 请求已完成且响应已就绪
status 服务器返回的 HTTP 状态码:200 - 成功,404 - 未找到,500 - 服务器内部错误等
statusText 服务器返回的状态文本(如 "OK"、"Not Found")
responseText 服务器返回的文本数据
responseXML 服务器返回的 XML 数据(可作为 DOM 对象处理)
onreadystatechange 每当readyState改变时触发的事件处理函数

XHR 对象的常用方法

方法 描述
open(method, url, async) 初始化请求: - method:请求方法(GET、POST 等) - url:请求地址 - async:是否异步(true - 异步,false - 同步)
send(data) 发送请求: - data:POST 请求时的参数数据
setRequestHeader(header, value) 设置请求头信息(需在open()之后、send()之前调用)
abort() 取消当前请求
getResponseHeader(header) 获取指定响应头的值
getAllResponseHeaders() 获取所有响应头信息

AJAX 的基本使用步骤

使用 AJAX 与服务器交互的基本步骤:

  1. 创建 XMLHttpRequest 对象
  2. 注册onreadystatechange事件处理函数
  3. 使用open()方法初始化请求
  4. (可选)设置请求头信息
  5. 使用send()方法发送请求
  6. 在事件处理函数中处理服务器响应

1. GET 请求示例

GET 请求通常用于从服务器获取数据,参数通过 URL 的查询字符串传递:

复制代码
// 发送GET请求
function sendGetRequest() {
    // 1. 创建XHR对象
    var xhr = createXHR();
    
    // 2. 注册事件处理函数
    xhr.onreadystatechange = function() {
        // 当请求完成且响应就绪
        if (xhr.readyState === 4) {
            // 当HTTP状态码为200(成功)
            if (xhr.status === 200) {
                // 处理响应数据
                var response = xhr.responseText;
                console.log("服务器响应:", response);
                document.getElementById("result").innerHTML = response;
            } else {
                // 处理错误
                console.error("请求失败,状态码:", xhr.status);
                document.getElementById("result").innerHTML = "请求失败:" + xhr.statusText;
            }
        }
    };
    
    // 3. 初始化请求(带参数)
    var username = document.getElementById("username").value;
    // 对参数进行编码,防止特殊字符问题
    var url = "GetDataServlet?username=" + encodeURIComponent(username) + "&t=" + new Date().getTime();
    xhr.open("GET", url, true);
    
    // 4. 发送请求(GET请求参数在URL中,send()方法参数为null)
    xhr.send(null);
}

注意

  • GET 请求的参数会显示在 URL 中,安全性较低
  • GET 请求有长度限制(不同浏览器限制不同,通常 2KB-8KB)
  • 添加时间戳(t=new Date().getTime())是为了避免浏览器缓存

2. POST 请求示例

POST 请求通常用于向服务器提交数据,参数在请求体中传递:

复制代码
// 发送POST请求
function sendPostRequest() {
    // 1. 创建XHR对象
    var xhr = createXHR();
    
    // 2. 注册事件处理函数
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                var response = xhr.responseText;
                console.log("服务器响应:", response);
                document.getElementById("result").innerHTML = response;
            } else {
                console.error("请求失败,状态码:", xhr.status);
                document.getElementById("result").innerHTML = "请求失败:" + xhr.statusText;
            }
        }
    };
    
    // 3. 初始化请求
    var url = "PostDataServlet";
    xhr.open("POST", url, true);
    
    // 4. 设置请求头(POST请求需要设置Content-Type)
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    
    // 5. 准备请求参数
    var username = document.getElementById("username").value;
    var email = document.getElementById("email").value;
    // 对参数进行编码
    var data = "username=" + encodeURIComponent(username) + 
               "&email=" + encodeURIComponent(email);
    
    // 6. 发送请求
    xhr.send(data);
}

POST vs GET

特性 GET POST
参数位置 URL 查询字符串 请求体
长度限制 无(由服务器配置决定)
缓存 可被缓存 通常不被缓存
安全性 低(参数可见) 较高(参数在请求体)
用途 获取数据 提交数据
幂等性 是(多次请求结果相同) 否(可能产生副作用)

处理 JSON 数据

现代 Web 应用中,JSON(JavaScript Object Notation)已成为 AJAX 数据交换的首选格式,它比 XML 更轻量、更易解析。

1. 服务器返回 JSON 数据

在 Servlet 中返回 JSON 数据:

复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet("/JsonDataServlet")
public class JsonDataServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 设置响应内容类型为JSON
        response.setContentType("application/json;charset=UTF-8");
        
        // 创建返回数据对象
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 获取请求参数
            String username = request.getParameter("username");
            
            // 模拟数据库查询
            boolean exists = "admin".equals(username);
            
            // 构建响应数据
            result.put("success", true);
            result.put("message", exists ? "用户名已存在" : "用户名可用");
            result.put("exists", exists);
            
        } catch (Exception e) {
            result.put("success", false);
            result.put("message", "服务器错误:" + e.getMessage());
        }
        
        // 使用Jackson库将Java对象转换为JSON字符串
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(result);
        
        // 发送JSON响应
        response.getWriter().write(json);
    }
}

添加 Jackson 依赖(Maven):

复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

2. 客户端解析 JSON 数据

客户端使用JSON.parse()方法解析 JSON 字符串:

复制代码
// 发送请求并处理JSON响应
function checkUsername() {
    var xhr = createXHR();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
            // 解析JSON响应
            try {
                var result = JSON.parse(xhr.responseText);
                var messageElement = document.getElementById("message");
                
                if (result.success) {
                    // 处理成功响应
                    messageElement.textContent = result.message;
                    messageElement.style.color = result.exists ? "red" : "green";
                } else {
                    // 处理错误响应
                    messageElement.textContent = "错误:" + result.message;
                    messageElement.style.color = "red";
                }
            } catch (e) {
                console.error("JSON解析错误:", e);
                document.getElementById("message").textContent = "数据格式错误";
            }
        }
    };
    
    var username = document.getElementById("username").value;
    var url = "JsonDataServlet?username=" + encodeURIComponent(username) + "&t=" + new Date().getTime();
    xhr.open("GET", url, true);
    xhr.send(null);
}

3. 用户名实时验证示例

结合上述代码,实现一个用户名实时验证功能:

复制代码
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <title>用户名实时验证</title>
    <style>
        .container { width: 500px; margin: 100px auto; }
        .form-group { margin: 20px 0; }
        label { display: inline-block; width: 100px; }
        input { padding: 8px; width: 250px; }
        #message { margin-left: 105px; height: 20px; }
    </style>
</head>
<body>
    <div class="container">
        <h2>注册</h2>
        <div class="form-group">
            <label for="username">用户名:</label>
            <input type="text" id="username" onblur="checkUsername()" onkeyup="debounceCheckUsername()">
        </div>
        <div id="message"></div>
        <div class="form-group">
            <label for="password">密码:</label>
            <input type="password" id="password">
        </div>
        <div class="form-group">
            <input type="button" value="注册" onclick="register()">
        </div>
    </div>

    <script>
        // 创建XHR对象的函数
        function createXHR() {
            var xhr;
            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            } else {
                xhr = new ActiveXObject("Microsoft.XMLHTTP");
            }
            return xhr;
        }
        
        // 防抖函数(避免输入时频繁请求)
        var timeout = null;
        function debounceCheckUsername() {
            clearTimeout(timeout);
            // 延迟500毫秒执行,避免输入过程中频繁请求
            timeout = setTimeout(checkUsername, 500);
        }
        
        // 检查用户名函数(前面已定义)
        function checkUsername() {
            // ... 实现代码同上 ...
        }
        
        // 注册函数
        function register() {
            // ... 实现注册逻辑 ...
        }
    </script>
</body>
</html>

AJAX 与表单提交

使用 AJAX 提交表单可以避免页面刷新,同时提供更灵活的错误处理和用户反馈。

1. 基本表单提交

复制代码
// 使用AJAX提交表单
function submitForm() {
    // 获取表单数据
    var username = document.getElementById("username").value;
    var password = document.getElementById("password").value;
    var email = document.getElementById("email").value;
    
    // 简单验证
    if (!username || !password || !email) {
        alert("请填写完整信息");
        return;
    }
    
    // 创建XHR对象
    var xhr = createXHR();
    
    // 处理响应
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                try {
                    var result = JSON.parse(xhr.responseText);
                    if (result.success) {
                        // 注册成功
                        alert("注册成功!");
                        // 可以跳转到登录页
                        // window.location.href = "login.jsp";
                    } else {
                        // 注册失败
                        alert("注册失败:" + result.message);
                    }
                } catch (e) {
                    alert("服务器响应格式错误");
                }
            } else {
                alert("请求失败,状态码:" + xhr.status);
            }
        }
    };
    
    // 发送POST请求
    xhr.open("POST", "RegisterServlet", true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    
    // 构建表单数据
    var data = "username=" + encodeURIComponent(username) +
               "&password=" + encodeURIComponent(password) +
               "&email=" + encodeURIComponent(email);
    
    xhr.send(data);
}

2. 处理文件上传

AJAX 也可以处理文件上传,需要使用FormData对象:

复制代码
// 使用AJAX上传文件
function uploadFile() {
    // 获取文件输入元素
    var fileInput = document.getElementById("file");
    var file = fileInput.files[0];
    
    // 检查文件是否选择
    if (!file) {
        alert("请选择要上传的文件");
        return;
    }
    
    // 检查文件类型
    var allowedTypes = ["image/jpeg", "image/png", "image/gif"];
    if (!allowedTypes.includes(file.type)) {
        alert("只允许上传JPG、PNG、GIF格式的图片");
        return;
    }
    
    // 检查文件大小(限制5MB)
    if (file.size > 5 * 1024 * 1024) {
        alert("文件大小不能超过5MB");
        return;
    }
    
    // 创建FormData对象
    var formData = new FormData();
    formData.append("file", file);
    formData.append("description", document.getElementById("description").value);
    
    // 创建XHR对象
    var xhr = createXHR();
    
    // 处理上传进度
    xhr.upload.onprogress = function(event) {
        if (event.lengthComputable) {
            var percent = (event.loaded / event.total) * 100;
            document.getElementById("progressBar").style.width = percent + "%";
            document.getElementById("progressText").textContent = Math.round(percent) + "%";
        }
    };
    
    // 处理响应
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                var result = JSON.parse(xhr.responseText);
                if (result.success) {
                    alert("上传成功!");
                    document.getElementById("result").innerHTML = 
                        "文件路径:" + result.filePath + "<br>" +
                        "预览:<img src='" + result.filePath + "' style='max-width: 300px;'>";
                } else {
                    alert("上传失败:" + result.message);
                }
            } else {
                alert("上传失败,状态码:" + xhr.status);
            }
        }
    };
    
    // 发送请求
    xhr.open("POST", "FileUploadServlet", true);
    // 上传文件时不要设置Content-Type,浏览器会自动处理
    xhr.send(formData);
}

对应的 JSP 页面:

复制代码
<div class="form-group">
    <label for="file">选择文件:</label>
    <input type="file" id="file" accept="image/*">
</div>
<div class="form-group">
    <label for="description">描述:</label>
    <input type="text" id="description" placeholder="请输入文件描述">
</div>
<div class="progress" style="width: 360px; height: 20px; border: 1px solid #ccc; margin-left: 105px;">
    <div id="progressBar" style="width: 0%; height: 100%; background-color: #4CAF50;"></div>
</div>
<div id="progressText" style="margin-left: 105px; margin-top: 5px;">0%</div>
<div class="form-group">
    <input type="button" value="上传" onclick="uploadFile()">
</div>
<div id="result" style="margin-left: 105px; margin-top: 10px;"></div>

AJAX 异步分页案例

结合之前的分页技术,使用 AJAX 实现无刷新分页:

1. 分页 Servlet

复制代码
@WebServlet("/AjaxUserPageServlet")
public class AjaxUserPageServlet extends HttpServlet {
    private UserDAO userDAO = new UserDAO();
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("application/json;charset=UTF-8");
        
        // 获取分页参数
        int currentPage = 1;
        int pageSize = 10;
        try {
            currentPage = Integer.parseInt(request.getParameter("currentPage"));
            pageSize = Integer.parseInt(request.getParameter("pageSize"));
        } catch (NumberFormatException e) {
            // 使用默认值
        }
        
        // 获取查询条件
        String username = request.getParameter("username");
        
        // 获取排序参数
        String sortField = request.getParameter("sortField");
        String sortOrder = request.getParameter("sortOrder");
        
        // 查询分页数据
        PageBean<User> pageBean = new PageBean<>(pageSize, currentPage);
        pageBean.setSortField(sortField);
        pageBean.setSortOrder(sortOrder);
        
        if (username != null && !username.trim().isEmpty()) {
            pageBean = userDAO.getUsersByConditionSortAndPage(username.trim(), pageBean);
        } else {
            pageBean = userDAO.getUsersByConditionSortAndPage(null, pageBean);
        }
        
        // 转换为JSON并响应
        ObjectMapper mapper = new ObjectMapper();
        // 处理日期格式
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        String json = mapper.writeValueAsString(pageBean);
        
        response.getWriter().write(json);
    }
}

2. 客户端分页实现

复制代码
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <title>AJAX分页示例</title>
    <style>
        /* 样式省略,参考之前的分页页面 */
    </style>
</head>
<body>
    <div class="container">
        <h2>用户列表(AJAX分页)</h2>
        
        <!-- 搜索栏 -->
        <div class="search-bar">
            <input type="text" id="username" placeholder="请输入用户名搜索">
            <input type="button" value="搜索" onclick="loadPage(1)">
            <select id="pageSize" onchange="loadPage(1)">
                <option value="5">5条/页</option>
                <option value="10" selected>10条/页</option>
                <option value="20">20条/页</option>
            </select>
        </div>
        
        <!-- 数据表格 -->
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th><a href="javascript:sortBy('username')">用户名</a></th>
                    <th><a href="javascript:sortBy('email')">邮箱</a></th>
                    <th><a href="javascript:sortBy('createTime')">注册时间</a></th>
                    <th><a href="javascript:sortBy('status')">状态</a></th>
                </tr>
            </thead>
            <tbody id="userTableBody">
                <!-- 数据将通过AJAX动态加载 -->
                <tr><td colspan="5" style="text-align: center;">加载中...</td></tr>
            </tbody>
        </table>
        
        <!-- 分页导航 -->
        <div id="pagination" class="page-nav">
            <!-- 分页导航将通过AJAX动态生成 -->
        </div>
        
        <!-- 加载状态提示 -->
        <div id="loading" style="display: none; text-align: center; padding: 20px;">
            加载中...
        </div>
    </div>

    <script>
        // 当前页码和分页参数
        var currentPage = 1;
        var pageSize = 10;
        var sortField = "createTime";
        var sortOrder = "DESC";
        
        // 页面加载完成后加载第一页数据
        window.onload = function() {
            loadPage(1);
        };
        
        // 加载指定页数据
        function loadPage(pageNum) {
            // 显示加载状态
            document.getElementById("loading").style.display = "block";
            
            // 更新当前页码
            currentPage = pageNum;
            
            // 获取查询条件
            var username = document.getElementById("username").value.trim();
            pageSize = document.getElementById("pageSize").value;
            
            // 创建XHR对象
            var xhr = createXHR();
            
            // 处理响应
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    // 隐藏加载状态
                    document.getElementById("loading").style.display = "none";
                    
                    if (xhr.status === 200) {
                        try {
                            var pageBean = JSON.parse(xhr.responseText);
                            // 更新表格数据
                            updateTable(pageBean.dataList);
                            // 更新分页导航
                            updatePagination(pageBean);
                        } catch (e) {
                            console.error("解析JSON失败:", e);
                            document.getElementById("userTableBody").innerHTML = 
                                "<tr><td colspan='5' style='text-align: center; color: red;'>数据格式错误</td></tr>";
                        }
                    } else {
                        document.getElementById("userTableBody").innerHTML = 
                            "<tr><td colspan='5' style='text-align: center; color: red;'>加载失败,状态码:" + xhr.status + "</td></tr>";
                    }
                }
            };
            
            // 构建请求URL
            var url = "AjaxUserPageServlet?" +
                      "currentPage=" + pageNum +
                      "&pageSize=" + pageSize +
                      "&username=" + encodeURIComponent(username) +
                      "&sortField=" + sortField +
                      "&sortOrder=" + sortOrder +
                      "&t=" + new Date().getTime();
            
            // 发送请求
            xhr.open("GET", url, true);
            xhr.send(null);
        }
        
        // 更新表格数据
        function updateTable(userList) {
            var tableBody = document.getElementById("userTableBody");
            
            if (userList.length === 0) {
                tableBody.innerHTML = "<tr><td colspan='5' style='text-align: center;'>暂无数据</td></tr>";
                return;
            }
            
            var html = "";
            for (var i = 0; i < userList.length; i++) {
                var user = userList[i];
                html += "<tr>";
                html += "<td>" + user.id + "</td>";
                html += "<td>" + user.username + "</td>";
                html += "<td>" + user.email + "</td>";
                html += "<td>" + user.createdTime + "</td>";
                html += "<td>" + (user.status === 1 ? "<span style='color: green;'>正常</span>" : "<span style='color: red;'>禁用</span>") + "</td>";
                html += "</tr>";
            }
            
            tableBody.innerHTML = html;
        }
        
        // 更新分页导航
        function updatePagination(pageBean) {
            var pagination = document.getElementById("pagination");
            var html = "";
            
            // 首页
            html += "<a href='javascript:loadPage(1)' " + (pageBean.currentPage === 1 ? "style='pointer-events: none; opacity: 0.5;'" : "") + ">首页</a>";
            
            // 上一页
            html += "<a href='javascript:loadPage(" + pageBean.prevPage + ")' " + (!pageBean.hasPrevPage ? "style='pointer-events: none; opacity: 0.5;'" : "") + ">上一页</a>";
            
            // 页码
            var startPage = Math.max(1, pageBean.currentPage - 3);
            var endPage = Math.min(pageBean.totalPage, pageBean.currentPage + 3);
            
            // 调整页码范围
            if (endPage - startPage < 6 && pageBean.totalPage > 6) {
                if (startPage === 1) {
                    endPage = 7;
                } else if (endPage === pageBean.totalPage) {
                    startPage = pageBean.totalPage - 6;
                }
            }
            
            for (var i = startPage; i <= endPage; i++) {
                if (i === pageBean.currentPage) {
                    html += "<span class='active'>" + i + "</span>";
                } else {
                    html += "<a href='javascript:loadPage(" + i + ")'>" + i + "</a>";
                }
            }
            
            // 下一页
            html += "<a href='javascript:loadPage(" + pageBean.nextPage + ")' " + (!pageBean.hasNextPage ? "style='pointer-events: none; opacity: 0.5;'" : "") + ">下一页</a>";
            
            // 末页
            html += "<a href='javascript:loadPage(" + pageBean.totalPage + ")' " + (pageBean.currentPage === pageBean.totalPage ? "style='pointer-events: none; opacity: 0.5;'" : "") + ">末页</a>";
            
            // 分页信息
            html += "<span>共 " + pageBean.totalCount + " 条记录,共 " + pageBean.totalPage + " 页,当前第 " + pageBean.currentPage + " 页</span>";
            
            pagination.innerHTML = html;
        }
        
        // 排序功能
        function sortBy(field) {
            if (sortField === field) {
                // 切换排序方向
                sortOrder = sortOrder === "ASC" ? "DESC" : "ASC";
            } else {
                // 新的排序字段,默认降序
                sortField = field;
                sortOrder = "DESC";
            }
            // 重新加载第一页
            loadPage(1);
        }
        
        // 创建XHR对象的函数
        function createXHR() {
            var xhr;
            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            } else {
                xhr = new ActiveXObject("Microsoft.XMLHTTP");
            }
            return xhr;
        }
    </script>
</body>
</html>

AJAX 最佳实践

1. 错误处理

完善的错误处理是 AJAX 应用的重要组成部分:

复制代码
// 健壮的AJAX错误处理
function safeAjaxRequest(url, method, data, successCallback, errorCallback) {
    // 参数验证
    if (!url || !method) {
        if (errorCallback) errorCallback(new Error("URL和请求方法不能为空"));
        return;
    }
    
    var xhr = createXHR();
    
    // 超时设置(5秒)
    xhr.timeout = 5000;
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                try {
                    // 尝试解析JSON
                    var response = JSON.parse(xhr.responseText);
                    if (successCallback) successCallback(response);
                } catch (e) {
                    if (errorCallback) {
                        errorCallback(new Error("响应数据格式错误: " + e.message));
                    } else {
                        console.error("响应数据格式错误: ", e);
                    }
                }
            } else {
                var errorMsg = "请求失败,状态码: " + xhr.status;
                if (xhr.status === 404) errorMsg = "请求的资源不存在";
                if (xhr.status === 500) errorMsg = "服务器内部错误";
                
                if (errorCallback) {
                    errorCallback(new Error(errorMsg));
                } else {
                    console.error(errorMsg);
                }
            }
        }
    };
    
    // 网络错误处理
    xhr.onerror = function() {
        var error = new Error("网络错误,无法连接到服务器");
        if (errorCallback) errorCallback(error);
        else console.error(error.message);
    };
    
    // 超时处理
    xhr.ontimeout = function() {
        var error = new Error("请求超时,请稍后重试");
        if (errorCallback) errorCallback(error);
        else console.error(error.message);
    };
    
    // 发送请求
    xhr.open(method, url, true);
    if (method.toUpperCase() === "POST" && !(data instanceof FormData)) {
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    }
    xhr.send(data || null);
    
    // 返回xhr对象,允许调用abort()
    return xhr;
}

2. 性能优化

  1. 请求合并:将多个小请求合并为一个大请求,减少 HTTP 请求次数
  2. 请求防抖:对于频繁触发的事件(如输入、滚动),延迟发送请求
  3. 缓存响应:对不常变化的数据进行本地缓存,减少重复请求
  4. 压缩数据:使用 gzip 压缩服务器响应,减少传输数据量
  5. 使用 HTTP/2:支持多路复用,提高并发请求效率
  6. 预加载:在空闲时预加载可能需要的数据

3. 安全性考虑

  1. 防止 XSS 攻击

    • 服务器对输出进行 HTML 转义
    • 客户端使用textContent而非innerHTML插入不可信内容
  2. 防止 CSRF 攻击

    • 使用 CSRF 令牌验证请求来源
    • 检查 Referer 请求头
  3. 数据验证

    • 客户端验证仅作为辅助,必须在服务器端进行严格验证
    • 对所有用户输入进行过滤和转义
  4. 限制请求频率

    • 服务器端实现限流机制,防止恶意请求
    • 客户端添加请求间隔限制

4. 用户体验优化

  1. 加载状态反馈

    • 显示加载动画或进度条
    • 提供取消请求的选项
  2. 错误提示友好

    • 使用用户易懂的语言描述错误
    • 提供解决问题的建议
  3. 离线支持

    • 使用 Service Worker 缓存静态资源
    • 实现离线操作和数据同步
  4. 进度指示

    • 对于耗时操作(如下载、上传),显示进度信息
    • 预估完成时间

现代 AJAX 替代方案

虽然原生 XMLHttpRequest 功能强大,但使用起来比较繁琐。现代前端开发中,有更便捷的替代方案:

1. Fetch API

Fetch API 是现代浏览器提供的用于替代 XMLHttpRequest 的 API,基于 Promise,语法更简洁:

复制代码
// 使用Fetch API发送请求
fetch('JsonDataServlet?username=' + encodeURIComponent(username))
    .then(response => {
        if (!response.ok) {
            throw new Error('HTTP error, status = ' + response.status);
        }
        return response.json();
    })
    .then(data => {
        console.log('成功:', data);
        // 处理数据
    })
    .catch(error => {
        console.error('错误:', error);
    });

2. Axios

Axios 是一个流行的第三方 AJAX 库,支持 Promise API,提供了更多功能:

复制代码
// 使用Axios发送请求
axios.get('JsonDataServlet', {
    params: {
        username: username
    }
})
.then(response => {
    console.log('成功:', response.data);
    // 处理数据
})
.catch(error => {
    console.error('错误:', error);
});

在 JavaWeb 项目中使用 Axios,只需引入 CDN:

复制代码
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

总结与实践

知识点回顾

  1. AJAX 基础

    • AJAX 允许在不刷新页面的情况下与服务器交换数据
    • 核心是 XMLHttpRequest 对象,负责异步通信
    • 支持 GET 和 POST 等 HTTP 方法
  2. 数据交互

    • 服务器通常返回 JSON 格式数据
    • 客户端使用 JSON.parse () 解析响应
    • 可以提交表单数据和上传文件
  3. 高级应用

    • 实时验证:如用户名唯一性检查
    • 异步分页:无刷新加载分页数据
    • 文件上传:带进度显示的文件上传
  4. 最佳实践

    • 完善的错误处理和超时控制
    • 性能优化:请求合并、防抖、缓存
    • 安全性考虑:防止 XSS、CSRF 攻击
    • 良好的用户体验:加载状态、友好提示

实践任务

  1. 实时聊天系统

    • 使用 AJAX 实现简单的实时聊天功能
    • 定期轮询服务器获取新消息
    • 支持发送消息和显示消息历史
  2. 动态数据仪表盘

    • 实现数据的实时刷新
    • 添加图表展示(使用 Chart.js)
    • 支持数据筛选和时间范围选择
  3. 无刷新购物车

    • 实现商品的添加、删除、数量修改
    • 实时计算总价和优惠信息
    • 支持本地存储购物车数据