pin_drop当前位置:知识文库 ❯ 图文

后端通信:Ajax基础与XMLHttpRequest - 从入门到实践详解

教程简介

Ajax(Asynchronous JavaScript and XML)是Web开发史上最重要的技术突破之一,它使网页能够在不重新加载整个页面的情况下与服务器交换数据并更新局部内容。虽然现代开发中Fetch API已成为首选,但XMLHttpRequest(XHR)作为Ajax的核心实现,仍然是理解前后端通信基础的重要知识点。许多第三方库(如jQuery的$.ajax)和旧项目仍在使用XHR。

本教程将全面讲解Ajax原理、XMLHttpRequest对象的使用方法、请求与响应处理、进度事件、超时处理,以及XHR的局限性。

核心概念

1. Ajax原理

Ajax不是一种单一技术,而是几种技术的组合:

  • HTML/XHTML:页面结构

  • CSS:页面样式

  • JavaScript:动态操作DOM

  • DOM:动态显示和交互

  • XMLHttpRequest:异步数据交换

  • XML/JSON:数据格式

Ajax的工作流程:

  1. 用户在页面上触发事件(点击按钮、提交表单等)

  2. JavaScript创建XMLHttpRequest对象

  3. 向服务器发送异步请求

  4. 服务器处理请求并返回数据

  5. JavaScript接收响应数据

  6. 使用DOM操作更新页面局部内容

与传统Web开发的区别:

特性 传统Web Ajax
数据请求 页面跳转/刷新 后台异步请求
用户体验 等待页面刷新 无感知更新
数据传输 整个HTML页面 仅需要的数据
服务器负载 较高 较低
交互性 有限 丰富

2. XMLHttpRequest对象

XMLHttpRequest是浏览器提供的内置对象,用于在后台与服务器通信:

代码示例

// 创建XHR对象
const xhr = new XMLHttpRequest();

XHR属性

属性 说明
readyState 请求状态(0-4)
status HTTP状态码
statusText HTTP状态描述
responseText 响应文本
responseXML 响应XML文档
response 响应体(类型取决于responseType)
responseType 响应类型设置
timeout 超时时间(毫秒)
withCredentials 是否携带Cookie

readyState状态值

状态 说明
0 UNSENT XHR对象已创建,未调用open()
1 OPENED open()已调用,未调用send()
2 HEADERS_RECEIVED send()已调用,头部已接收
3 LOADING 正在接收响应体
4 DONE 请求完成

XHR方法

方法 说明
open(method, url, async, user, password) 初始化请求
send(body) 发送请求
abort() 终止请求
setRequestHeader(name, value) 设置请求头
getResponseHeader(name) 获取响应头
getAllResponseHeaders() 获取所有响应头
overrideMimeType(mime) 覆盖响应MIME类型

XHR事件

事件 说明
readystatechange readyState变化时触发
load 请求成功完成
error 请求失败
abort 请求被终止
timeout 请求超时
loadstart 请求开始
loadend 请求结束
progress 数据传输进度

3. 基本使用流程

代码示例

// 1. 创建XHR对象
const xhr = new XMLHttpRequest();

// 2. 配置请求
xhr.open('GET', '/api/users', true);

// 3. 设置请求头(可选)
xhr.setRequestHeader('Accept', 'application/json');

// 4. 监听状态变化
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
            // 请求成功
            const data = JSON.parse(xhr.responseText);
            console.log(data);
        } else {
            // 请求失败
            console.error('请求失败:', xhr.status);
        }
    }
};

// 5. 发送请求
xhr.send();

4. 同步vs异步

代码示例

// 异步请求(推荐)
xhr.open('GET', '/api/data', true);

// 同步请求(不推荐,已废弃)
xhr.open('GET', '/api/data', false);

同步请求的问题:

  • 阻塞主线程,页面冻结

  • 用户体验极差

  • 在主线程上已被大多数浏览器废弃

  • 只能在Worker中使用

5. 请求头设置

代码示例

// 设置Content-Type
xhr.setRequestHeader('Content-Type', 'application/json');

// 设置Authorization
xhr.setRequestHeader('Authorization', 'Bearer token123');

// 注意:某些头部浏览器不允许设置
// Forbidden headers: Accept-Charset, Accept-Encoding, Cookie, Host, Origin, Referer等

6. 响应处理

代码示例

// 文本响应
const text = xhr.responseText;

// JSON响应
const data = JSON.parse(xhr.responseText);

// XML响应
const xml = xhr.responseXML;

// Blob响应
xhr.responseType = 'blob';
xhr.onload = function() {
    const blob = xhr.response;
    const url = URL.createObjectURL(blob);
};

// ArrayBuffer响应
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
    const buffer = xhr.response;
};

7. 进度事件

代码示例

// 上传进度
xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
        const percent = (e.loaded / e.total * 100).toFixed(1);
        console.log(`上传进度: ${percent}%`);
    }
};

// 下载进度
xhr.onprogress = function(e) {
    if (e.lengthComputable) {
        const percent = (e.loaded / e.total * 100).toFixed(1);
        console.log(`下载进度: ${percent}%`);
    }
};

8. 超时处理

代码示例

xhr.timeout = 10000; // 10秒超时
xhr.ontimeout = function() {
    console.error('请求超时');
};

9. XHR的局限性

局限性 说明
API设计老旧 基于事件,不符合现代Promise/async模式
回调地狱 多个异步请求需要嵌套回调
错误处理复杂 需要手动检查status和readyState
无法取消 abort()会触发error,没有优雅的取消机制
无流式读取 无法像Fetch一样流式处理响应
同步模式 同步模式已废弃

代码示例

示例1:XMLHttpRequest完整演示

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XMLHttpRequest完整演示</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', system-ui, sans-serif;
            background: #f0f2f5;
            padding: 20px;
            color: #333;
        }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card {
            background: #fff;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 20px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }
        .card h2 {
            font-size: 18px;
            color: #1a73e8;
            margin-bottom: 16px;
            padding-bottom: 8px;
            border-bottom: 2px solid #e8eaed;
        }
        .method-btns {
            display: flex;
            gap: 8px;
            margin-bottom: 16px;
            flex-wrap: wrap;
        }
        .method-btn {
            padding: 8px 16px;
            border: 2px solid #dadce0;
            border-radius: 8px;
            background: #fff;
            cursor: pointer;
            font-weight: 600;
            font-size: 13px;
            transition: all 0.2s;
        }
        .method-btn:hover { border-color: #1a73e8; }
        .method-btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
        .url-bar {
            display: flex;
            gap: 10px;
            margin-bottom: 16px;
        }
        .url-bar select {
            padding: 10px 14px;
            border: 1px solid #dadce0;
            border-radius: 8px;
            font-weight: 700;
            font-size: 14px;
        }
        .url-bar input {
            flex: 1;
            padding: 10px 14px;
            border: 1px solid #dadce0;
            border-radius: 8px;
            font-size: 14px;
        }
        .url-bar input:focus, .url-bar select:focus { outline: none; border-color: #1a73e8; }
        .send-btn {
            padding: 10px 24px;
            background: #1a73e8;
            color: #fff;
            border: none;
            border-radius: 8px;
            font-weight: 700;
            cursor: pointer;
            font-size: 14px;
        }
        .send-btn:hover { background: #1557b0; }
        .send-btn:disabled { background: #dadce0; cursor: not-allowed; }
        .state-bar {
            display: flex;
            gap: 8px;
            margin-bottom: 16px;
        }
        .state-item {
            flex: 1;
            padding: 10px;
            border-radius: 8px;
            text-align: center;
            font-size: 12px;
            font-weight: 600;
            background: #f8f9fa;
            color: #999;
            transition: all 0.3s;
        }
        .state-item.active { background: #1a73e8; color: #fff; }
        .state-item.done { background: #34a853; color: #fff; }
        .state-item.error { background: #ea4335; color: #fff; }
        .state-num { font-size: 18px; display: block; margin-bottom: 2px; }
        .progress-bar {
            height: 6px;
            background: #e8eaed;
            border-radius: 3px;
            overflow: hidden;
            margin-bottom: 16px;
        }
        .progress-fill {
            height: 100%;
            background: #1a73e8;
            border-radius: 3px;
            transition: width 0.3s;
            width: 0%;
        }
        .response-area {
            background: #1e1e1e;
            color: #d4d4d4;
            padding: 16px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 13px;
            line-height: 1.7;
            max-height: 300px;
            overflow-y: auto;
            white-space: pre-wrap;
            word-break: break-all;
        }
        .log-area {
            background: #f8f9fa;
            padding: 12px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 12px;
            max-height: 200px;
            overflow-y: auto;
            line-height: 1.8;
        }
        .log-time { color: #888; }
        .log-info { color: #1a73e8; }
        .log-success { color: #34a853; }
        .log-error { color: #ea4335; }
        .log-warn { color: #ff9800; }
        textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #dadce0;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 13px;
            resize: vertical;
            margin-bottom: 12px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>XMLHttpRequest完整演示</h1>

        <div class="card">
            <h2>请求配置</h2>
            <div class="url-bar">
                <select id="method">
                    <option value="GET">GET</option>
                    <option value="POST">POST</option>
                    <option value="PUT">PUT</option>
                    <option value="DELETE">DELETE</option>
                </select>
                <input type="text" id="url" value="https://httpbin.org/get" placeholder="请求URL">
                <button class="send-btn" id="sendBtn" onclick="sendXHR()">发送</button>
            </div>
            <div id="bodySection" style="display:none;">
                <textarea id="body" rows="3" placeholder='请求体内容,如:{"name":"张三"}'></textarea>
            </div>
        </div>

        <div class="card">
            <h2>请求状态</h2>
            <div class="state-bar">
                <div class="state-item" id="state0"><span class="state-num">0</span>UNSENT</div>
                <div class="state-item" id="state1"><span class="state-num">1</span>OPENED</div>
                <div class="state-item" id="state2"><span class="state-num">2</span>HEADERS</div>
                <div class="state-item" id="state3"><span class="state-num">3</span>LOADING</div>
                <div class="state-item" id="state4"><span class="state-num">4</span>DONE</div>
            </div>
            <div class="progress-bar">
                <div class="progress-fill" id="progressFill"></div>
            </div>
        </div>

        <div class="card">
            <h2>响应内容</h2>
            <div class="response-area" id="responseArea">等待发送请求...</div>
        </div>

        <div class="card">
            <h2>事件日志</h2>
            <div class="log-area" id="logArea">等待操作...</div>
        </div>
    </div>

    <script>
        let xhr = null;

        document.getElementById('method').addEventListener('change', function() {
            const show = ['POST', 'PUT', 'PATCH'].includes(this.value);
            document.getElementById('bodySection').style.display = show ? 'block' : 'none';
        });

        function log(msg, type = 'info') {
            const time = new Date().toLocaleTimeString();
            const logArea = document.getElementById('logArea');
            logArea.innerHTML += `<div><span class="log-time">[${time}]</span> <span class="log-${type}">${msg}</span></div>`;
            logArea.scrollTop = logArea.scrollHeight;
        }

        function resetStates() {
            for (let i = 0; i <= 4; i++) {
                document.getElementById('state' + i).className = 'state-item';
            }
        }

        function sendXHR() {
            if (xhr) {
                xhr.abort();
            }

            const method = document.getElementById('method').value;
            const url = document.getElementById('url').value;
            const body = document.getElementById('body').value;

            resetStates();
            document.getElementById('state0').classList.add('active');
            document.getElementById('progressFill').style.width = '0%';
            document.getElementById('responseArea').textContent = '请求中...';
            document.getElementById('sendBtn').disabled = true;

            xhr = new XMLHttpRequest();

            log(`创建XHR对象,readyState: ${xhr.readyState}`, 'info');

            xhr.onreadystatechange = function() {
                log(`readyState变化: ${xhr.readyState}`, 'info');

                for (let i = 0; i <= 4; i++) {
                    const el = document.getElementById('state' + i);
                    el.className = 'state-item';
                    if (i < xhr.readyState) el.classList.add('done');
                    if (i === xhr.readyState) el.classList.add('active');
                }

                if (xhr.readyState === 4) {
                    document.getElementById('sendBtn').disabled = false;
                    if (xhr.status >= 200 && xhr.status < 300) {
                        log(`请求成功: ${xhr.status} ${xhr.statusText}`, 'success');
                        document.getElementById('state4').classList.remove('active');
                        document.getElementById('state4').classList.add('done');
                        try {
                            const data = JSON.parse(xhr.responseText);
                            document.getElementById('responseArea').textContent = JSON.stringify(data, null, 2);
                        } catch {
                            document.getElementById('responseArea').textContent = xhr.responseText;
                        }
                    } else {
                        log(`请求失败: ${xhr.status} ${xhr.statusText}`, 'error');
                        document.getElementById('state4').classList.remove('active');
                        document.getElementById('state4').classList.add('error');
                        document.getElementById('responseArea').textContent = `错误: ${xhr.status} ${xhr.statusText}`;
                    }
                }
            };

            xhr.onprogress = function(e) {
                if (e.lengthComputable) {
                    const percent = (e.loaded / e.total * 100).toFixed(0);
                    document.getElementById('progressFill').style.width = percent + '%';
                    log(`下载进度: ${percent}%`, 'info');
                }
            };

            xhr.onloadstart = function() { log('请求开始', 'info'); };
            xhr.onloadend = function() { log('请求结束', 'info'); };
            xhr.onerror = function() { log('网络错误', 'error'); };
            xhr.ontimeout = function() { log('请求超时', 'warn'); };

            xhr.timeout = 30000;

            log(`发送请求: ${method} ${url}`, 'info');
            xhr.open(method, url, true);
            xhr.setRequestHeader('Accept', 'application/json');

            if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
                xhr.setRequestHeader('Content-Type', 'application/json');
                xhr.send(body);
            } else {
                xhr.send();
            }
        }
    </script>
</body>
</html>

示例2:XHR文件上传进度

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XHR文件上传进度</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', system-ui, sans-serif;
            background: #f0f2f5;
            padding: 20px;
            color: #333;
        }
        .container { max-width: 700px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card {
            background: #fff;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 20px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }
        .card h2 {
            font-size: 18px;
            color: #1a73e8;
            margin-bottom: 16px;
            padding-bottom: 8px;
            border-bottom: 2px solid #e8eaed;
        }
        .drop-zone {
            border: 2px dashed #dadce0;
            border-radius: 12px;
            padding: 40px;
            text-align: center;
            cursor: pointer;
            transition: all 0.2s;
            margin-bottom: 16px;
        }
        .drop-zone:hover, .drop-zone.dragover {
            border-color: #1a73e8;
            background: #e3f2fd;
        }
        .drop-zone-icon { font-size: 40px; margin-bottom: 10px; }
        .drop-zone-text { color: #888; font-size: 14px; }
        .file-list { list-style: none; }
        .file-item {
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 12px;
            border: 1px solid #e8eaed;
            border-radius: 8px;
            margin-bottom: 8px;
        }
        .file-icon { font-size: 24px; }
        .file-info { flex: 1; }
        .file-name { font-weight: 600; font-size: 14px; }
        .file-size { font-size: 12px; color: #888; }
        .file-progress {
            height: 6px;
            background: #e8eaed;
            border-radius: 3px;
            overflow: hidden;
            margin-top: 6px;
        }
        .file-progress-fill {
            height: 100%;
            background: #1a73e8;
            border-radius: 3px;
            transition: width 0.3s;
        }
        .file-status { font-size: 12px; font-weight: 600; }
        .status-pending { color: #888; }
        .status-uploading { color: #1a73e8; }
        .status-done { color: #34a853; }
        .status-error { color: #ea4335; }
        .upload-btn {
            width: 100%;
            padding: 14px;
            background: #1a73e8;
            color: #fff;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 700;
            cursor: pointer;
            transition: background 0.2s;
        }
        .upload-btn:hover { background: #1557b0; }
        .upload-btn:disabled { background: #dadce0; cursor: not-allowed; }
    </style>
</head>
<body>
    <div class="container">
        <h1>XHR文件上传进度</h1>

        <div class="card">
            <h2>选择文件</h2>
            <div class="drop-zone" id="dropZone">
                <div class="drop-zone-icon"></div>
                <div class="drop-zone-text">拖放文件到此处,或点击选择文件</div>
                <input type="file" id="fileInput" multiple style="display:none">
            </div>
            <ul class="file-list" id="fileList"></ul>
            <button class="upload-btn" id="uploadBtn" onclick="uploadFiles()" disabled>上传文件</button>
        </div>
    </div>

    <script>
        let files = [];

        const dropZone = document.getElementById('dropZone');
        const fileInput = document.getElementById('fileInput');

        dropZone.addEventListener('click', () => fileInput.click());
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.classList.add('dragover');
        });
        dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.classList.remove('dragover');
            addFiles(e.dataTransfer.files);
        });
        fileInput.addEventListener('change', () => addFiles(fileInput.files));

        function addFiles(fileList) {
            for (const file of fileList) {
                files.push({
                    file,
                    id: Date.now() + Math.random(),
                    progress: 0,
                    status: 'pending'
                });
            }
            renderFileList();
            document.getElementById('uploadBtn').disabled = files.length === 0;
        }

        function formatSize(bytes) {
            if (bytes < 1024) return bytes + ' B';
            if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
            return (bytes / 1048576).toFixed(1) + ' MB';
        }

        function renderFileList() {
            const list = document.getElementById('fileList');
            list.innerHTML = files.map(f => `
                <li class="file-item">
                    <div class="file-icon"></div>
                    <div class="file-info">
                        <div class="file-name">${f.file.name}</div>
                        <div class="file-size">${formatSize(f.file.size)}</div>
                        <div class="file-progress">
                            <div class="file-progress-fill" style="width:${f.progress}%"></div>
                        </div>
                    </div>
                    <span class="file-status status-${f.status}">
                        ${f.status === 'pending' ? '等待' :
                          f.status === 'uploading' ? f.progress + '%' :
                          f.status === 'done' ? '完成' : '失败'}
                    </span>
                </li>
            `).join('');
        }

        function uploadFiles() {
            document.getElementById('uploadBtn').disabled = true;

            files.forEach((f, index) => {
                if (f.status === 'done') return;

                const formData = new FormData();
                formData.append('file', f.file);

                const xhr = new XMLHttpRequest();
                f.status = 'uploading';
                renderFileList();

                xhr.upload.onprogress = (e) => {
                    if (e.lengthComputable) {
                        f.progress = Math.round(e.loaded / e.total * 100);
                        renderFileList();
                    }
                };

                xhr.onload = () => {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        f.status = 'done';
                        f.progress = 100;
                    } else {
                        f.status = 'error';
                    }
                    renderFileList();
                    checkAllDone();
                };

                xhr.onerror = () => {
                    f.status = 'error';
                    renderFileList();
                    checkAllDone();
                };

                // 模拟上传(实际使用时替换为真实URL)
                xhr.open('POST', 'https://httpbin.org/post', true);
                xhr.send(formData);
            });
        }

        function checkAllDone() {
            const allDone = files.every(f => f.status === 'done' || f.status === 'error');
            if (allDone) {
                document.getElementById('uploadBtn').disabled = false;
                document.getElementById('uploadBtn').textContent = '重新上传';
            }
        }
    </script>
</body>
</html>

示例3:XHR封装与Promise化

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XHR封装与Promise化</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', system-ui, sans-serif;
            background: #f0f2f5;
            padding: 20px;
            color: #333;
        }
        .container { max-width: 900px; margin: 0 auto; }
        h1 { text-align: center; color: #1a73e8; margin-bottom: 24px; font-size: 28px; }
        .card {
            background: #fff;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 20px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }
        .card h2 {
            font-size: 18px;
            color: #1a73e8;
            margin-bottom: 16px;
            padding-bottom: 8px;
            border-bottom: 2px solid #e8eaed;
        }
        .code-block {
            background: #1e1e1e;
            color: #d4d4d4;
            padding: 16px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 13px;
            line-height: 1.7;
            white-space: pre-wrap;
            margin-bottom: 16px;
        }
        .demo-btns {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            margin-bottom: 16px;
        }
        .demo-btn {
            padding: 10px 20px;
            border: none;
            border-radius: 8px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
        }
        .btn-blue { background: #1a73e8; color: #fff; }
        .btn-blue:hover { background: #1557b0; }
        .btn-green { background: #34a853; color: #fff; }
        .btn-green:hover { background: #2d8e47; }
        .btn-orange { background: #ff9800; color: #fff; }
        .btn-orange:hover { background: #e5a800; }
        .result-area {
            background: #f8f9fa;
            padding: 16px;
            border-radius: 8px;
            font-family: 'Consolas', monospace;
            font-size: 13px;
            line-height: 1.7;
            min-height: 100px;
            white-space: pre-wrap;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>XHR封装与Promise化</h1>

        <div class="card">
            <h2>XHR Promise封装</h2>
            <div class="code-block">// 将XHR封装为Promise
function xhrRequest(options) {
    return new Promise((resolve, reject) => {
        const {
            method = 'GET',
            url,
            headers = {},
            body = null,
            timeout = 30000,
            responseType = 'json'
        } = options;

        const xhr = new XMLHttpRequest();
        xhr.timeout = timeout;
        xhr.responseType = responseType;

        xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve({
                    status: xhr.status,
                    statusText: xhr.statusText,
                    data: xhr.response,
                    headers: parseHeaders(xhr.getAllResponseHeaders())
                });
            } else {
                reject(new HttpError(xhr.status, xhr.statusText));
            }
        };

        xhr.onerror = () => reject(new Error('网络错误'));
        xhr.ontimeout = () => reject(new Error('请求超时'));
        xhr.onabort = () => reject(new Error('请求已取消'));

        xhr.open(method, url, true);

        Object.entries(headers).forEach(([name, value]) => {
            xhr.setRequestHeader(name, value);
        });

        xhr.send(body ? JSON.stringify(body) : null);
    });
}

class HttpError extends Error {
    constructor(status, statusText) {
        super(`${status} ${statusText}`);
        this.status = status;
        this.statusText = statusText;
    }
}

function parseHeaders(headerStr) {
    return headerStr.trim().split(/\\r?\\n/).reduce((acc, line) => {
        const [name, ...value] = line.split(': ');
        acc[name.toLowerCase()] = value.join(': ');
        return acc;
    }, {});
}</div>
        </div>

        <div class="card">
            <h2>使用演示</h2>
            <div class="demo-btns">
                <button class="demo-btn btn-blue" onclick="demoGet()">GET请求</button>
                <button class="demo-btn btn-green" onclick="demoPost()">POST请求</button>
                <button class="demo-btn btn-orange" onclick="demoError()">错误处理</button>
                <button class="demo-btn btn-blue" onclick="demoChain()">链式调用</button>
            </div>
            <div class="result-area" id="resultArea">点击按钮查看演示结果</div>
        </div>
    </div>

    <script>
        // XHR Promise封装
        function xhrRequest(options) {
            return new Promise((resolve, reject) => {
                const {
                    method = 'GET', url, headers = {},
                    body = null, timeout = 30000, responseType = 'json'
                } = options;

                const xhr = new XMLHttpRequest();
                xhr.timeout = timeout;
                xhr.responseType = responseType;

                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve({
                            status: xhr.status,
                            statusText: xhr.statusText,
                            data: xhr.response,
                            headers: xhr.getAllResponseHeaders()
                        });
                    } else {
                        reject(new Error(`HTTP ${xhr.status} ${xhr.statusText}`));
                    }
                };

                xhr.onerror = () => reject(new Error('网络错误'));
                xhr.ontimeout = () => reject(new Error('请求超时'));

                xhr.open(method, url, true);
                Object.entries(headers).forEach(([name, value]) => {
                    xhr.setRequestHeader(name, value);
                });
                xhr.send(body ? JSON.stringify(body) : null);
            });
        }

        async function demoGet() {
            const area = document.getElementById('resultArea');
            area.textContent = 'GET请求中...';

            try {
                const result = await xhrRequest({
                    method: 'GET',
                    url: 'https://httpbin.org/get',
                    headers: { 'Accept': 'application/json' }
                });
                area.textContent = `GET请求成功!\n状态码: ${result.status}\n\n响应数据:\n${JSON.stringify(result.data, null, 2)}`;
            } catch (error) {
                area.textContent = `GET请求失败: ${error.message}`;
            }
        }

        async function demoPost() {
            const area = document.getElementById('resultArea');
            area.textContent = 'POST请求中...';

            try {
                const result = await xhrRequest({
                    method: 'POST',
                    url: 'https://httpbin.org/post',
                    headers: { 'Content-Type': 'application/json' },
                    body: { name: '张三', email: 'zhangsan@example.com' }
                });
                area.textContent = `POST请求成功!\n状态码: ${result.status}\n\n响应数据:\n${JSON.stringify(result.data, null, 2)}`;
            } catch (error) {
                area.textContent = `POST请求失败: ${error.message}`;
            }
        }

        async function demoError() {
            const area = document.getElementById('resultArea');
            area.textContent = '测试错误处理...';

            try {
                await xhrRequest({
                    url: 'https://httpbin.org/status/404',
                    timeout: 5000
                });
            } catch (error) {
                area.textContent = `错误被捕获!\n错误信息: ${error.message}\n\nPromise化后可以用try/catch优雅地处理错误`;
            }
        }

        async function demoChain() {
            const area = document.getElementById('resultArea');
            area.textContent = '链式调用演示...\n\n1. 先获取数据...';

            try {
                const result1 = await xhrRequest({
                    url: 'https://httpbin.org/uuid'
                });
                area.textContent += `\n2. 获取到UUID: ${result1.data.uuid}\n3. 用UUID获取详情...`;

                const result2 = await xhrRequest({
                    url: 'https://httpbin.org/get',
                    headers: { 'X-Request-ID': result1.data.uuid }
                });
                area.textContent += `\n4. 链式调用完成!\n\nPromise化后可以用async/await实现优雅的链式调用,\n避免了回调地狱。`;
            } catch (error) {
                area.textContent += `\n链式调用失败: ${error.message}`;
            }
        }
    </script>
</body>
</html>

浏览器兼容性

特性 Chrome Firefox Safari Edge 说明
XMLHttpRequest 全版本 全版本 全版本 全版本 所有浏览器支持
XHR2 (FormData, upload) 7+ 3.5+ 5+ 12+ 现代浏览器均支持
responseType='json' 31+ 10+ 7+ 79+ 自动解析JSON
responseType='blob' 20+ 13+ 6+ 12+ 二进制数据
progress事件 7+ 3.5+ 5+ 12+ 上传和下载进度
timeout 29+ 13+ 7+ 12+ 超时控制
withCredentials 4+ 3.5+ 5+ 12+ 跨域Cookie
同步XHR 已废弃 已废弃 已废弃 已废弃 主线程上已废弃

注意事项与最佳实践

1. 优先使用Fetch API

  • Fetch API更现代,基于Promise设计

  • XHR的API设计老旧,不符合现代编程范式

  • 新项目应使用Fetch,旧项目可逐步迁移

2. 避免同步请求

  • 同步XHR在主线程上已被废弃

  • 会导致页面冻结,用户体验极差

  • 如必须同步,请在Worker中使用

3. 正确处理错误

  • 同时监听onerror和ontimeout

  • 检查HTTP状态码(4xx/5xx)

  • 网络错误时responseText可能为空

4. 超时设置

  • 始终设置合理的超时时间

  • 默认无超时,可能永远等待

  • 建议API请求设置10-30秒超时

5. 内存管理

  • 及时abort不再需要的请求

  • 清除事件监听器

  • 大文件下载使用Blob避免内存溢出

代码规范示例

完整的XHR工具类

代码示例

class XhrClient {
    constructor(baseURL = '', defaultHeaders = {}) {
        this.baseURL = baseURL;
        this.defaultHeaders = {
            'Accept': 'application/json',
            ...defaultHeaders
        };
    }

    request(method, path, options = {}) {
        return new Promise((resolve, reject) => {
            const url = this.baseURL + path;
            const xhr = new XMLHttpRequest();

            xhr.timeout = options.timeout || 30000;
            xhr.responseType = options.responseType || 'json';

            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve({
                        status: xhr.status,
                        data: xhr.response,
                        headers: xhr.getAllResponseHeaders()
                    });
                } else {
                    reject(new Error(`HTTP ${xhr.status}`));
                }
            };

            xhr.onerror = () => reject(new Error('网络错误'));
            xhr.ontimeout = () => reject(new Error('请求超时'));

            if (options.onProgress) {
                xhr.onprogress = options.onProgress;
            }
            if (options.onUploadProgress) {
                xhr.upload.onprogress = options.onUploadProgress;
            }

            xhr.open(method, url, true);

            const headers = { ...this.defaultHeaders, ...options.headers };
            Object.entries(headers).forEach(([name, value]) => {
                xhr.setRequestHeader(name, value);
            });

            xhr.send(options.body || null);
        });
    }

    get(path, options) { return this.request('GET', path, options); }
    post(path, body, options) { return this.request('POST', path, { ...options, body: JSON.stringify(body) }); }
    put(path, body, options) { return this.request('PUT', path, { ...options, body: JSON.stringify(body) }); }
    delete(path, options) { return this.request('DELETE', path, options); }
}

常见问题与解决方案

问题1:CORS请求被拒绝

解决方案:确保服务器设置了正确的CORS头部,XHR跨域请求需要服务器配合。

问题2:中文乱码

解决方案:确保服务器返回正确的Content-Type头部(charset=utf-8),或使用overrideMimeType:

代码示例

xhr.overrideMimeType('text/html; charset=utf-8');

问题3:IE兼容性

解决方案:IE9及以下使用ActiveXObject:

代码示例

const xhr = window.XMLHttpRequest
    ? new XMLHttpRequest()
    : new ActiveXObject('Microsoft.XMLHTTP');

问题4:重复请求

解决方案:在发送新请求前abort上一个请求:

代码示例

let lastXHR = null;
function search(query) {
    if (lastXHR) lastXHR.abort();
    lastXHR = new XMLHttpRequest();
    // ...配置和发送
}

总结

XMLHttpRequest是Ajax技术的核心实现,虽然Fetch API已成为现代开发的首选,但理解XHR仍然重要:

  1. Ajax原理:异步通信,局部更新,无需刷新整个页面

  2. XHR对象:readyState状态机、事件驱动模型

  3. 请求配置:open/send/setRequestHeader的基本用法

  4. 响应处理:responseText/responseXML/responseType

  5. 进度事件:upload.onprogress和onprogress实现上传下载进度

  6. 超时处理:timeout属性和ontimeout事件

  7. Promise封装:将回调式XHR封装为Promise,配合async/await使用

  8. XHR局限:API老旧、回调地狱、无流式读取,已被Fetch API取代

掌握XHR的工作原理,不仅有助于维护旧项目,也能更深入理解浏览器网络请求的底层机制。

常见问题

什么是Ajax基础与XMLHttpRequest?

Ajax基础与XMLHttpRequest是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。

如何学习Ajax基础与XMLHttpRequest的实际应用?

教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。

Ajax基础与XMLHttpRequest有哪些注意事项?

常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。

Ajax基础与XMLHttpRequest适合初学者吗?

本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。

Ajax基础与XMLHttpRequest的核心要点是什么?

核心要点包括教程简介等内容,建议按教程顺序逐步学习。

标签: Promise HTTP头部 表单提交 响应码 async await 场景管理 Fetch API 游戏状态

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: 后端通信:HTTPS与安全 - 从入门到实践详解 下一篇: 后端通信:Fetch API详解 - 从入门到实践详解

poll相关推荐