Ajax

Ajax概述

Ajax是Asynchronous JavaScript and XML的缩写,实现无刷新数据交互。

什么是Ajax

// Ajax特点:
// 1. 无需刷新页面即可与服务器通信
// 2. 异步执行,不阻塞页面
// 3. 可以发送和接收各种数据格式

// 传统方式:页面跳转
// 用户点击 → 页面刷新 → 服务器响应 → 新页面

// Ajax方式:异步请求
// 用户点击 → Ajax请求 → 服务器响应 → 更新部分页面

Ajax应用场景

// 常见应用
// 1. 表单验证
// 2. 搜索建议
// 3. 无限滚动
// 4. 实时更新
// 5. 文件上传
// 6. 数据加载

// 示例:搜索建议
const searchInput = document.querySelector("#search");
const suggestions = document.querySelector("#suggestions");

searchInput.addEventListener("input", debounce(async function() {
    const keyword = this.value;
    if (keyword.length < 2) {
        suggestions.innerHTML = "";
        return;
    }
    
    const results = await search(keyword);
    renderSuggestions(results);
}, 300));

XMLHttpRequest对象

XMLHttpRequest是Ajax的核心对象。

创建XHR对象

// 创建XHR对象
let xhr;
if (window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
} else {
    // IE6及以下
    xhr = new ActiveXObject("Microsoft.XMLHTTP");
}

// 现代浏览器直接使用
const xhr2 = new XMLHttpRequest();

XHR属性

const xhr = new XMLHttpRequest();

// readyState: 请求状态
// 0: 未初始化
// 1: 已打开
// 2: 已发送
// 3: 接收中
// 4: 完成

// status: HTTP状态码
// 200: 成功
// 301: 永久重定向
// 302: 临时重定向
// 304: 未修改
// 400: 错误请求
// 401: 未授权
// 403: 禁止访问
// 404: 未找到
// 500: 服务器错误

// responseText: 响应文本
// responseXML: 响应XML
// statusText: 状态文本

XHR请求

发送HTTP请求。

GET请求

const xhr = new XMLHttpRequest();

// 配置请求
xhr.open("GET", "/api/users?id=1", true);

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

// 监听响应
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            console.log(xhr.responseText);
        } else {
            console.error("请求失败:", xhr.status);
        }
    }
};

POST请求

const xhr = new XMLHttpRequest();

xhr.open("POST", "/api/users", true);

// 设置请求头
xhr.setRequestHeader("Content-Type", "application/json");

// 发送数据
const data = {
    name: "东巴文",
    email: "info@db-w.cn"
};
xhr.send(JSON.stringify(data));

xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};

封装XHR

function ajax(options) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        
        const {
            method = "GET",
            url,
            data = null,
            headers = {},
            timeout = 0
        } = options;
        
        // 配置请求
        xhr.open(method, url, true);
        
        // 设置请求头
        Object.entries(headers).forEach(([key, value]) => {
            xhr.setRequestHeader(key, value);
        });
        
        // 设置超时
        xhr.timeout = timeout;
        
        // 监听响应
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    try {
                        const response = JSON.parse(xhr.responseText);
                        resolve(response);
                    } catch (e) {
                        resolve(xhr.responseText);
                    }
                } else {
                    reject(new Error(`HTTP ${xhr.status}`));
                }
            }
        };
        
        // 错误处理
        xhr.onerror = () => reject(new Error("网络错误"));
        xhr.ontimeout = () => reject(new Error("请求超时"));
        
        // 发送请求
        if (data && typeof data === "object") {
            xhr.setRequestHeader("Content-Type", "application/json");
            xhr.send(JSON.stringify(data));
        } else {
            xhr.send(data);
        }
    });
}

// 使用
ajax({
    method: "GET",
    url: "/api/users/1"
})
.then(user => console.log(user))
.catch(error => console.error(error));

XHR响应

处理服务器响应。

响应处理

const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data", true);
xhr.send();

xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
        // 获取响应头
        const contentType = xhr.getResponseHeader("Content-Type");
        const allHeaders = xhr.getAllResponseHeaders();
        
        // 根据类型处理响应
        if (contentType.includes("application/json")) {
            const data = JSON.parse(xhr.responseText);
            console.log(data);
        } else if (contentType.includes("text/html")) {
            document.getElementById("content").innerHTML = xhr.responseText;
        } else {
            console.log(xhr.responseText);
        }
    }
};

响应XML

const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data.xml", true);
xhr.send();

xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        const xml = xhr.responseXML;
        const items = xml.getElementsByTagName("item");
        
        for (const item of items) {
            console.log(item.textContent);
        }
    }
};

XHR事件

XMLHttpRequest的事件处理。

事件类型

const xhr = new XMLHttpRequest();

// readystatechange: 状态变化
xhr.onreadystatechange = function() {
    console.log("状态:", xhr.readyState);
};

// load: 请求完成
xhr.onload = function() {
    console.log("加载完成:", xhr.status);
};

// error: 请求失败
xhr.onerror = function() {
    console.error("请求失败");
};

// abort: 请求中止
xhr.onabort = function() {
    console.log("请求中止");
};

// timeout: 请求超时
xhr.ontimeout = function() {
    console.error("请求超时");
};

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

xhr.open("GET", "/api/data", true);
xhr.timeout = 5000;
xhr.send();

上传进度

function uploadFile(file) {
    const xhr = new XMLHttpRequest();
    
    // 上传进度
    xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
            const percent = (e.loaded / e.total * 100).toFixed(0);
            console.log(`上传进度: ${percent}%`);
            updateProgress(percent);
        }
    };
    
    xhr.upload.onload = function() {
        console.log("上传完成");
    };
    
    xhr.upload.onerror = function() {
        console.error("上传失败");
    };
    
    const formData = new FormData();
    formData.append("file", file);
    
    xhr.open("POST", "/api/upload", true);
    xhr.send(formData);
}

XHR跨域

处理跨域请求问题。

同源策略

// 同源:协议、域名、端口相同
// http://db-w.cn/page1
// http://db-w.cn/page2        同源
// https://db-w.cn/page1       不同源(协议不同)
// http://www.db-w.cn/page1    不同源(域名不同)
// http://db-w.cn:8080/page1   不同源(端口不同)

// 跨域请求会被浏览器阻止
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://other.com/api/data", true);
xhr.send();  // 可能被阻止

CORS

// CORS: Cross-Origin Resource Sharing
// 服务器设置响应头允许跨域

// 简单请求
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://api.db-w.cn/data", true);
xhr.withCredentials = true;  // 发送Cookie
xhr.send();

// 预检请求(非简单请求)
// 浏览器先发送OPTIONS请求
// 服务器返回允许的方法和头

// 非简单请求条件:
// 1. 方法不是GET/POST/HEAD
// 2. 头部包含自定义字段
// 3. Content-Type不是简单类型

JSONP

// JSONP: 利用script标签跨域
function jsonp(url, callback) {
    const callbackName = "jsonp_" + Date.now();
    
    window[callbackName] = function(data) {
        callback(data);
        delete window[callbackName];
        document.body.removeChild(script);
    };
    
    const script = document.createElement("script");
    script.src = `${url}?callback=${callbackName}`;
    document.body.appendChild(script);
}

// 使用
jsonp("http://api.db-w.cn/data", function(data) {
    console.log(data);
});

// 服务器返回:
// jsonp_1234567890({ "name": "东巴文" })

FormData对象

处理表单数据。

创建FormData

// 从表单创建
const form = document.querySelector("form");
const formData = new FormData(form);

// 手动创建
const formData2 = new FormData();
formData2.append("name", "东巴文");
formData2.append("email", "info@db-w.cn");
formData2.append("age", 25);

FormData方法

const formData = new FormData();
formData.append("name", "东巴文");
formData.append("tags", "js");
formData.append("tags", "css");  // 同名多值

// 获取值
console.log(formData.get("name"));      // "东巴文"
console.log(formData.getAll("tags"));   // ["js", "css"]

// 检查是否存在
console.log(formData.has("name"));      // true

// 设置值(覆盖)
formData.set("name", "新名称");

// 删除
formData.delete("age");

// 遍历
for (const [key, value] of formData) {
    console.log(key, value);
}

文件上传

// 上传文件
const input = document.querySelector('input[type="file"]');

input.addEventListener("change", function() {
    const file = this.files[0];
    
    const formData = new FormData();
    formData.append("file", file);
    formData.append("name", file.name);
    
    const xhr = new XMLHttpRequest();
    
    xhr.upload.onprogress = function(e) {
        const percent = (e.loaded / e.total * 100).toFixed(0);
        console.log(`上传: ${percent}%`);
    };
    
    xhr.onload = function() {
        console.log("上传完成");
    };
    
    xhr.open("POST", "/api/upload", true);
    xhr.send(formData);
});

下一步

掌握了Ajax后,让我们继续学习:

  1. [Fetch API](./50_Fetch API.md) - 学习现代网络请求
  2. WebSocket - 学习实时通信
  3. ES6核心特性 - 学习ES6新特性

东巴文(db-w.cn) - 让编程学习更简单

🎯 东巴文寄语:Ajax是现代Web应用的基础技术,虽然Fetch API更加现代,但理解XMLHttpRequest对于理解Web通信原理非常重要。在 db-w.cn,我们帮你掌握网络请求的核心知识!