首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Ajax 實戰:用戶名異步校驗、服務條款動態載入、驗證碼生成完整實現 (前後端分離架構)

     編輯:Java 程式設計入門

專案背景與架構

傳統表單提交 = 整頁重載 + 用戶等待。Ajax (Asynchronous JavaScript and XML) 透過瀏覽器內建 XMLHttpRequest (XHR) 實現**局部異步數據交換**,用戶無感知等待,體驗質變。本文實現註冊頁三大核心異步交互:

  • 用戶名即時校驗:離焦觸發,後端查重,即時反饋。
  • 服務條款動態載入:頁面載入時異步獲取長文本,避免首屏阻塞。
  • 驗證碼圖片生成與刷新:後端繪製干擾碼圖片,前端點擊刷新。

技術棧

  • 後端:Java Servlet (JDK 8+)、Tomcat 9、javax.imageio 繪圖
  • 前端:原生 JS (ES5 相容)、CSS3、JSP 模板
  • 部署描述符:web.xml Servlet 映射

後端核心 Servlet (UserServlet)


package com.webapp.servlet;

import com.webapp.util.CheckCodeImageUtil;
import javax.imageio.ImageIO;
import javax.servlet.*;
import javax.servlet.http.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;

public class UserServlet extends HttpServlet {
    private List userList = new ArrayList<>(); // 模擬用戶庫
    private String rulesFilePath;

    @Override
    public void init() throws ServletException {
        rulesFilePath = getInitParameter("rulesfilepath"); // /txt/item.txt
        // 預置用戶
        userList.addAll(Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        doGet(req, resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String path = req.getRequestURI().substring(req.getContextPath().length());
        // 路徑格式:/validatename.do, /rulesText.do, /checkcode.do
        String action = path.substring(1, path.indexOf('.'));

        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");

        switch (action) {
            case "validatename":
                handleValidateName(req, resp);
                break;
            case "rulesText":
                handleRulesText(req, resp);
                break;
            case "checkcode":
                handleCheckCode(req, resp);
                break;
            default:
                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }

    private void handleValidateName(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 模擬網路延遲 3 秒,體現異步優勢
        try { Thread.sleep(3000); } catch (InterruptedException ignored) {}

        String username = req.getParameter("username");
        resp.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.print(userList.contains(username) ? "exists" : "noexists");
    }

    private void handleRulesText(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        String realPath = getServletContext().getRealPath(rulesFilePath);
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(new FileInputStream(realPath), "GBK"))) { // 檔案編碼 GBK
            String line;
            StringBuilder sb = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
            out.print(sb.toString());
        } catch (Exception e) {
            out.print("服務條款載入失敗,請稍後再試。");
        }
    }

    private void handleCheckCode(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.setContentType("image/jpeg");
        resp.setHeader("Pragma", "no-cache");
        resp.setHeader("Cache-Control", "no-cache");
        resp.setDateHeader("Expires", 0);

        Map map = CheckCodeImageUtil.creatCheckImage();
        String code = map.keySet().iterator().next();
        BufferedImage image = map.get(code);

        req.getSession().setAttribute("validateCode", code); // 存入 Session 驗證
        ImageIO.write(image, "jpeg", resp.getOutputStream());
    }
}

驗證碼工具類 (CheckCodeImageUtil)


package com.webapp.util;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.*;

public class CheckCodeImageUtil {
    private static final char[] CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray(); // 排除易混淆字元
    private static final Random RANDOM = new Random();

    public static Map creatCheckImage() {
        int width = 100, height = 38, codeCount = 4;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = image.createGraphics();

        // 背景
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);

        // 邊框
        g.setColor(Color.GRAY);
        g.drawRect(0, 0, width - 1, height - 1);

        // 干擾線 (30 條)
        g.setColor(Color.LIGHT_GRAY);
        for (int i = 0; i < 30; i++) {
            int x1 = RANDOM.nextInt(width);
            int y1 = RANDOM.nextInt(height);
            int x2 = RANDOM.nextInt(20);
            int y2 = RANDOM.nextInt(20);
            g.drawLine(x1, y1, x1 + x2, y1 + y2);
        }

        // 字體
        Font font = new Font("Consolas", Font.BOLD, 28);
        g.setFont(font);

        // 生成驗證碼
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < codeCount; i++) {
            char c = CHARS[RANDOM.nextInt(CHARS.length)];
            code.append(c);
            // 隨機顏色
            g.setColor(new Color(RANDOM.nextInt(100), RANDOM.nextInt(100), RANDOM.nextInt(100)));
            g.drawString(String.valueOf(c), 20 * i + 10, 30);
        }

        g.dispose();
        Map map = new HashMap<>();
        map.put(code.toString(), image);
        return map;
    }
}

前端頁面 (regist.jsp)


<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>



    用戶註冊 - Ajax 異步演示
    
    


>> 用戶註冊 [用戶名異步校驗、條款異步載入、驗證碼後端生成]
用戶名: * 用戶名由字母、數字、下劃線組成
密碼: * 長度 6-12 位,建議混合大小寫數字符號
確認密碼: * 請再次輸入密碼
郵箱: * 用於找回密碼、接收通知
驗證碼: 驗證碼 看不清,換一張
服務條款:

前端 Ajax 核心邏輯 (regist_ajax.js)


// 跨瀏覽器創建 XHR
function createXHR() {
    if (typeof XMLHttpRequest !== 'undefined') return new XMLHttpRequest();
    try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {}
    throw new Error('不支援 XMLHttpRequest');
}

var xhr = createXHR(); // 單例複用 (簡單頁面足夠,複雜場景請每請求創建)

// 防緩存時間戳
function addTimestamp(url) {
    return url + (url.indexOf('?') === -1 ? '?' : '&') + '_t=' + Date.now();
}

// 通用 GET 請求
function get(url, callback) {
    xhr.open('GET', addTimestamp(encodeURI(url)), true);
    xhr.onreadystatechange = function() { handleResponse(callback); };
    xhr.send(null);
}

// 通用 POST 請求
function post(url, data, callback) {
    xhr.open('POST', url, true);
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function() { handleResponse(callback); };
    xhr.send(data);
}

function handleResponse(callback) {
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            callback(xhr.responseText);
        } else {
            callback('error');
        }
    }
}

// 1. 用戶名校驗 (離焦觸發 POST)
function validateUsername() {
    var username = document.getElementById('username').value.trim();
    if (!username) { showMsg('checkMsg', '請輸入用戶名', 'orange'); return; }

    showMsg('checkMsg', '檢查中...', '#0099aa');
    post('validatename.do', 'username=' + encodeURIComponent(username), function(result) {
        if (result === 'exists') showMsg('checkMsg', '* 對不起,該用戶名已存在', 'red');
        else if (result === 'noexists') showMsg('checkMsg', '* 恭喜,該用戶名可用', 'green');
        else showMsg('checkMsg', '* 系統故障,請稍後再試', 'red');
    });
}

// 2. 載入服務條款 (頁面載入時 GET)
function loadRulesText() {
    var ta = document.getElementById('rulesText');
    get('rulesText.do', function(text) {
        ta.value = text || '條款載入失敗';
    });
}

// 3. 載入/刷新驗證碼 (頁面載入 + 點擊圖片)
function loadCheckCode() {
    var img = document.getElementById('checkCodeImg');
    img.src = addTimestamp('checkcode.do'); // 修改 src 觸發新請求
}

// 顯示提示訊息
function showMsg(id, text, color) {
    var el = document.getElementById(id);
    el.innerHTML = '' + text + '';
}

// 表單提交前綜合驗證
function validateForm(form) {
    var user = form.username.value.trim();
    var pwd = form.password.value;
    var repwd = form.repassword.value;
    var email = form.email.value.trim();
    var code = form.checkcode.value.trim();

    if (!user) return alertMsg('checkMsg', '請輸入用戶名');
    if (!/^\w+$/.test(user)) return alertMsg('checkMsg', '用戶名只能含字母數字下劃線');
    if (pwd.length < 6 || pwd.length > 12) return alertMsg('pwdMsg', '密碼長度 6-12 位');
    if (pwd !== repwd) return alertMsg('repwdMsg', '兩次密碼不一致');
    if (!/^[\w.-]+@[\w.-]+\.\w+$/.test(email)) return alertMsg('emailMsg', '郵箱格式不正確');
    if (code.length !== 4) return alertMsg('ckcodeMsg', '請輸入 4 位驗證碼');

    // 可在此增加驗證碼與 Session 比對的 Ajax 同步驗證
    return true; // 通過提交
}

function alertMsg(id, msg) {
    showMsg(id, '* ' + msg, 'red');
    document.getElementById(id.replace('Msg', '')).focus();
    return false;
}

CSS 樣式 (regist.css)


.reg-table {
    margin: 50px auto; width: 720px; border-collapse: collapse;
    font: 14px/1.6 "Microsoft YaHei", sans-serif; border: 1px solid #ddd;
}
.reg-table td { padding: 12px 10px; }
.label { width: 100px; text-align: right; color: #555; background: #f5faff; font-weight: 500; }
.input { border: 1px solid #b0d0f0; width: 280px; height: 32px; padding: 0 8px; border-radius: 3px; }
.input:focus { border-color: #4a90d9; outline: none; box-shadow: 0 0 4px rgba(74,144,217,0.3); }
.title { background: #e8f4fd; color: #2a6ebd; font-size: 16px; font-weight: bold; }
.btn-submit { background: #2a6ebd; color: #fff; border: none; padding: 10px 30px; font-size: 16px; border-radius: 4px; cursor: pointer; }
.btn-submit:hover { background: #1d5a9e; }
.copyright { text-align: center; margin-top: 20px; color: #888; font-size: 12px; }
#checkCodeImg { vertical-align: middle; cursor: pointer; border: 1px solid #ddd; margin-left: 5px; }

web.xml 部署配置




    
        UserServlet
        com.webapp.servlet.UserServlet
        
            rulesfilepath
            /txt/item.txt
        
    
    
        UserServlet
        *.do
    
    
        regist.jsp
    

關鍵設計亮點與避坑總結

  1. XHR 單例 vs 多實例:簡單頁面單例複用無問題;並發請求場景**每請求創建新 XHR**,避免狀態衝突。
  2. 編碼統一 UTF-8:JSP pageEncoding、Servlet request/response encoding、JS encodeURIComponent、資料庫連接、檔案讀取指定 GBK→UTF-8 轉碼,全鏈路一致。
  3. 防緩存策略:驗證碼圖片、GET 請求必須加時間戳參數 (_t=timestamp);響應頭設定 Cache-Control: no-cache。
  4. 異步體驗細節:校驗中顯示「檢查中...」、模擬 3 秒延遲體現非阻塞、成功/失敗顏色區分、輸入框聚焦引導。
  5. 驗證碼安全:Session 儲存、一次性驗證後移除、排除易混淆字元 (0/O, 1/I/l)、干擾線/噪點/隨機顏色/字體扭曲。
  6. 錯誤處理:網路異常、伺服器 500、超時統一友好提示,避免白屏/無反應。

現代化演進建議

  • 前端遷移至 fetch API / axios + async/await,代碼更線性易維護。
  • 後端遷移至 Spring Boot + @RestController + Jackson JSON 序列化,統一返回 Result<T> 結構。
  • 驗證碼升級為 Google reCAPTCHA v3 / hCaptcha / 滑塊驗證,提升安全性與用戶體驗。
  • 引入前端驗證庫 (VeeValidate, Yup) 實現聲明式表單驗證,減少手寫邏輯。
  • 服務條款內容儲存於資料庫/對象存儲,支援版本管理、多語言、富文本編輯。

總結

本專案展示了 Ajax 在傳統 Java Web 中的經典應用模式:**原生 XHR 封裝、Servlet 多動作分發、Session 驗證碼、檔案異步載入、防緩存與編碼處理**。雖然現代開發多採用前後端分離 + JSON API,但理解這套「從零構建異步交互」的完整鏈路,對排查前端請求異常、優化用戶體驗、設計通用組件庫具有根本價值。願你的每一次提交,都能享受「無感刷新」的絲滑體驗!

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved