目录

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

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

特性GETPOST
参数位置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. 无刷新购物车

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