AJAX 是什么?

  1. AJAX 全称是 Asynchronous JavaScript and XML (异步 JavaScript 和 XML),核心是在不刷新整个页面的情况下,与服务器异步交换数据,实现页面局部更新 (比如网页的评论加载、搜索建议、数据分页都是典型场景)
  2. 注意:
    1. 异步:JS 代码执行时,不会等待 AJAX 请求完成,而是继续执行后续代码,请求完成后再通过回调处理结果;
    2. 现在 AJAX 不一定用 XML,更多用 JSON 传输数据 (更轻量、易解析)

AJAX 的实现方式

传统 XMLHttpRequest(原生核心)

  1. 完整示例

    // 1. 创建 XMLHttpRequest 对象(核心对象)
    const xhr = new XMLHttpRequest();
    
    // 2. 配置请求:method(请求方式)、url(接口地址)、async(是否异步,默认 true)
    xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
    
    // 3. 设置响应数据格式(可选,推荐加)
    xhr.responseType = 'json'; // 直接解析为 JSON 对象,无需手动 JSON.parse
    
    // 4. 监听请求状态变化(核心:处理响应)
    xhr.onreadystatechange = function() {
      // readyState 是请求状态:4 表示响应已完全接收;status 200 表示请求成功
      if (xhr.readyState === 4 && xhr.status === 200) {
        // 成功获取响应数据
        console.log('请求成功:', xhr.response);
        // 比如把数据渲染到页面
        document.getElementById('content').innerText = xhr.response.title;
      } else if (xhr.readyState === 4) {
        // 请求完成但失败(比如 404、500)
        console.error('请求失败:', xhr.status, xhr.statusText);
      }
    };
    
    // 5. 发送请求(GET 请求无请求体,传 null;POST 需要传数据)
    xhr.send(null);
    
    const xhr = new XMLHttpRequest();
    xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts', true);
    
    // POST 请求必须设置请求头(告诉服务器数据格式)
    xhr.setRequestHeader('Content-Type', 'application/json');
    
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          console.log('提交成功:', xhr.response);
        } else {
          console.error('提交失败:', xhr.status);
        }
      }
    };
    
    // 构造请求体(JSON 字符串)
    const postData = JSON.stringify({
      title: '我的AJAX测试',
      body: '这是POST请求的测试内容',
      userId: 1
    });
    
    xhr.send(postData);
    
  2. XHR 的属性

    属性 描述
    xhr.response 响应体数据,类型取决于 responseType 的指定
    xhr.status 响应状态码值,比如 200404 ----标识着请求成功或者失败
    xhr.statusText 响应状态文本
    xhr.timeout 指定请求超时时间,默认为 0 代表没有限制
    xhr.withCredentials 值为 true:跨域资源共享中,允许携带资源凭证
    xhr.readyState 表示请求状态:
    0 创建完 XHR
    1 已经完成 OPEN 操作
    2 服务器已经把响应头信息返回了
    3 响应主体正在返回中
    4 响应主体已经返回
  3. XHR 的方法

    方法 描述
    xhr.abort() 强制中断请求
    xhr.getAllResponseHeaders() 获取所有请求头
    xhr.getResponseHeader() 获取请求头
    xhr.setRequestHeader() 设置请求头(属性值不能是中文和特殊字符)
    xhr.open(method,url[,async]) 初始化一个请求
    xhr.overrideMimeType() 重写资源类型
    xhr.send() 发送请求
  4. XHR 的事件

    方法 描述
    load 在接收到完整的响应数据时触发
    readystatechange 每当 readyState 属性改变时,就会调用该函数
    error 在请求发生错误时触发

Fetch API(现代替代方案)

Axios(基于 Promise 封装的第三方库)

HTTP 请求

HTTP 请求:Web 通信的「消息格式」

  1. HTTP 请求的完整结构:

    请求行 → 方法 路径 协议版本
    请求头 → Key: Value(多个)
    请求体 → 可选(POST/PUT 等请求才有)
    
  2. 常用 HTTP 请求方法 (核心)

    方法 语义 常用场景 是否有请求体
    GET 读取 / 获取资源 查数据、分页、搜索
    POST 创建新资源 提交表单、新增数据
    PUT 全量更新资源 替换整个资源 (比如修改用户所有信息)
    PATCH 增量更新资源 只修改资源的某个字段 (比如只改用户名)
    DELETE 删除资源 删除数据 (参数可拼 URL)
    OPTIONS 预检请求 (跨域时) 浏览器自动发,检测服务器允许的方法 / 头
  3. 实际请求示例 (抓包视角)

    POST /api/posts HTTP/1.1
    Host: jsonplaceholder.typicode.com
    Content-Type: application/json
    User-Agent: Chrome/120.0.0.0
    Accept: application/json
    
    {
      "title": "测试POST",
      "body": "请求体数据",
      "userId": 1
    }
    

HTTP 状态码:服务器的「响应状态」

  1. 状态码分类 (按第一位数字)

    分类 数字范围 含义 典型场景
    1xx 100-101 信息性 请求已接收,继续处理 (极少用到)
    2xx 200-299 成功 请求已成功处理
    3xx 300-399 重定向 需要客户端进一步操作 (比如跳转到新地址)
    4xx 400-499 客户端错误 请求有问题 (比如参数错、没权限)
    5xx 500-599 服务器错误 服务器处理请求时出错 (比如代码 bug、数据库挂了)
  2. 最常用的状态码:

    1. 2xx 成功
      • 200 OK:最常用,请求成功 (GET/POST/PUT 都可能返回)
      • 201 Created:POST 请求创建资源成功 (比如新增用户 / 文章)
      • 204 No Content:请求成功,但无返回数据 (比如 DELETE 删除成功)
    2. 3xx 重定向
      • 301 Moved Permanently:永久重定向 (比如域名更换)
      • 302 Found:临时重定向 (比如未登录跳登录页)
      • 304 Not Modified:资源未修改 (浏览器缓存命中,无需重新下载)
    3. 4xx 客户端错误
      • 400 Bad Request:请求参数错误 (比如格式不对、缺少必填项)
      • 401 Unauthorized:未认证 (比如没登录、Token 过期)
      • 403 Forbidden:已认证,但无权限 (比如普通用户访问管理员接口)
      • 404 Not Found:请求的资源不存在 (比如 URL 输错、接口不存在)
      • 405 Method Not Allowed:请求方法不支持 (比如接口只允许 GET,却用了 POST)
      • 408 Request Timeout:请求超时 (客户端发请求太慢)
      • 429 Too Many Requests:请求过于频繁 (限流 / 防刷)
    4. 5xx 服务器错误
      • 500 Internal Server Error:服务器内部错误 (比如代码抛异常、数据库连接失败)
      • 502 Bad Gateway:网关错误 (比如反向代理服务器没收到后端响应)
      • 503 Service Unavailable:服务器暂时不可用 (比如维护、过载)
      • 504 Gateway Timeout:网关超时 (后端接口响应太慢)

Content-Type:数据的「格式标识」

  1. Content-Type (内容类型)HTTP 头字段,核心作用是告诉对方「我传递的数据是什么格式」,分为两种场景:

    1. 请求头的 Content-Type:客户端告诉服务器「请求体的数据格式」;
    2. 响应头的 Content-Type:服务器告诉客户端「响应体的数据格式」;
  2. 最常用的 Content-Type 值:

    含义 适用场景 数据示例
    application/json JSON 格式(最常用) AJAX / 前后端交互 {“name”:“张三”,“age”:20}
    application/x-www-form-urlencoded 表单默认格式(URL 编码) 普通表单提交 name=张三&age=20
    multipart/form-data 多部分表单格式 文件上传(图片 / 视频) 包含二进制文件 + 普通字段
    text/plain 纯文本格式 简单文本传输 hello world
    text/html HTML 格式 服务器返回网页
    测试
    image/jpeg/image/png 图片格式 传输图片文件 二进制图片数据
  3. 不同 Content-Type 的实际应用:

    // fetch POST 请求(JSON 格式)
    fetch('/api/user', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json' // 关键:告诉服务器是 JSON 格式
      },
      body: JSON.stringify({ name: '张三', age: 20 }) // JSON 字符串
    });
    
    // 表单格式的 POST 请求
    fetch('/api/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: 'username=zhangsan&password=123456' // URL 编码字符串
    });
    
    // 上传文件
    const formData = new FormData();
    formData.append('file', fileInput.files[0]); // 文件字段
    formData.append('name', '测试文件'); // 普通字段
    
    fetch('/api/upload', {
      method: 'POST',
      body: formData // 自动设置 Content-Type: multipart/form-data; boundary=xxx
    });
    

同源策略-跨域解决方案

面试题

倒计时抢购

如何从服务器获取时间,以及存在的问题:

  1. 可以基于 ajax 向服务器发送请求,服务器返回的响应头中包含了服务器时间(GMT 格林威治时间,new Date([转换的时间] 转换为北京时间 ));
  2. 由于网络传送存在时差,导致客户端接收到的服务器时间和真实时间存在偏差,当响应头信息返回(AJAX 状态为 2 的时候),获取时间即可,HTTP 传输中的 HEAD 请求方式,就是只获取响应头的信息;
  3. 获取当前客户端本地的时间 (但是这个时间客户可以修改自己本地的时间):真实项目中只能做一些参考的工作,不能做严谨的校验,严格校验的情况下,需要的时间是从服务器获取的;
class CountdownTimer {
  constructor(targetTimeSelector, targetDate) {
    this.targetTime = new Date(targetDate);
    this.displayElement = document.querySelector(targetTimeSelector);
    this.serverTime = null;
    this.timer = null;
    this.isRunning = false;
    
    // 错误处理:检查DOM元素是否存在
    if (!this.displayElement) {
      console.error(`Element with selector "${targetTimeSelector}" not found`);
      throw new Error('Target element not found');
    }
  }

  // 格式化时间为 HH:MM:SS
  static formatTime(hours, minutes, seconds) {
    const pad = num => num.toString().padStart(2, '0');
    return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
  }

  // 从服务器获取时间(模拟)
  async queryServerTime() {
    try {
      // 实际项目中这里应该是真实的API调用
      const response = await fetch('/api/server-time');
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const data = await response.json();
      return new Date(data.serverTime);
    } catch (error) {
      console.warn('Failed to fetch server time, using client time:', error);
      // 降级策略:服务器时间获取失败时使用客户端时间
      return new Date();
    }
  }

  // 计算时间差并分解
  calculateTimeDifference(targetTime, currentTime) {
    const diffMs = targetTime - currentTime;
    
    if (diffMs <= 0) {
      return {
        expired: true,
        hours: 0,
        minutes: 0,
        seconds: 0,
        totalMs: 0
      };
    }

    const totalSeconds = Math.floor(diffMs / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    return {
      expired: false,
      hours,
      minutes,
      seconds,
      totalMs: diffMs
    };
  }

  // 更新显示
  updateDisplay(timeData) {
    if (timeData.expired) {
      this.displayElement.textContent = '00:00:00';
      this.displayElement.classList.add('expired');
    } else {
      this.displayElement.textContent = CountdownTimer.formatTime(
        timeData.hours,
        timeData.minutes,
        timeData.seconds
      );
      this.displayElement.classList.remove('expired');
    }
  }

  // 单次计算
  compute() {
    const timeData = this.calculateTimeDifference(this.targetTime, this.serverTime);
    this.updateDisplay(timeData);
    return timeData;
  }

  // 定时更新
  async updateTimer() {
    if (!this.isRunning) return;

    // 更新服务器时间(每秒+1000ms)
    this.serverTime = new Date(this.serverTime.getTime() + 1000);
    
    const timeData = this.compute();
    
    // 如果时间已到,停止定时器
    if (timeData.expired) {
      this.stop();
      this.onExpired?.();
    }
  }

  // 开始倒计时
  async start() {
    if (this.isRunning) {
      console.warn('Timer is already running');
      return;
    }

    try {
      // 获取初始服务器时间
      this.serverTime = await this.queryServerTime();
      
      // 初始计算
      const initialTimeData = this.compute();
      
      // 如果已经过期,直接返回
      if (initialTimeData.expired) {
        this.onExpired?.();
        return;
      }

      // 启动定时器
      this.isRunning = true;
      this.timer = setInterval(() => {
        this.updateTimer();
      }, 1000);
      
      this.onStart?.();
      
    } catch (error) {
      console.error('Failed to start countdown timer:', error);
      throw error;
    }
  }

  // 停止倒计时
  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
    this.isRunning = false;
    this.onStop?.();
  }

  // 重置倒计时
  async reset(newTargetDate = null) {
    this.stop();
    
    if (newTargetDate) {
      this.targetTime = new Date(newTargetDate);
    }
    
    // 重置显示
    this.displayElement.textContent = '--:--:--';
    this.displayElement.classList.remove('expired');
    
    await this.start();
  }

  // 销毁实例
  destroy() {
    this.stop();
    this.serverTime = null;
    this.displayElement = null;
  }

  // 事件回调(可选)
  onStart = null;
  onStop = null;
  onExpired = null;
}

// 使用示例
async function initCountdown() {
  try {
    const timer = new CountdownTimer('#spanBox', '2020/02/25 20:49:30');
    
    // 设置事件回调
    timer.onExpired = () => {
      console.log('倒计时结束!可以开始抢购了!');
      // 触发抢购逻辑
      startFlashSale();
    };
    
    timer.onStart = () => {
      console.log('倒计时开始');
    };
    
    // 开始倒计时
    await timer.start();
    
    // 如果需要重置
    // setTimeout(() => timer.reset('2020/02/26 10:00:00'), 5000);
    
    return timer;
  } catch (error) {
    console.error('初始化倒计时失败:', error);
    // 降级显示
    document.querySelector('#spanBox').textContent = '即将开始';
  }
}

// 启动倒计时
document.addEventListener('DOMContentLoaded', () => {
  const timerInstance = initCountdown();
  
  // 如果需要,可以在页面离开时清理
  window.addEventListener('beforeunload', () => {
    timerInstance?.then(timer => timer.destroy());
  });
});

// 模拟抢购开始函数
function startFlashSale() {
  console.log('抢购开始!');
  // 这里可以添加开始抢购的逻辑
  document.querySelector('#buyButton').disabled = false;
}

readyState 变化几次

let xhr = new XMLHttpRequest;
xhr.open('get', './js/fastclick.js', true); // readyState 变为 1(仅打开,未发送)
xhr.send(); // 异步请求:将请求任务放入宏任务队列,主线程继续执行同步代码

// 同步代码:此时请求还在宏任务队列,readyState 仍为 1,绑定回调
xhr.onreadystatechange = function () {
  console.log(xhr.readyState); //=>2.3.4
};
let xhr = new XMLHttpRequest;
xhr.open('get', './js/fastclick.js', false); // readyState 变为 1
xhr.send(); // 同步请求:阻塞主线程,直到请求完成(readyState 直接到 4)

// 同步代码:此时 readyState 已经是 4,且无后续变化,绑定回调
xhr.onreadystatechange = function () {
  console.log(xhr.readyState); // 无输出
};
let xhr = new XMLHttpRequest;
xhr.open('get', './js/fastclick.js', false); // readyState 变为 1

// 同步代码:此时 readyState 为 1,绑定回调
xhr.onreadystatechange = function () {
  console.log(xhr.readyState); //=>4
};

xhr.send(); // 同步请求:阻塞主线程,readyState 从 1 → 4(状态变化触发回调)
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. AJAX 是什么?
  2. 2. AJAX 的实现方式
    1. 2.1. 传统 XMLHttpRequest(原生核心)
    2. 2.2. Fetch API(现代替代方案)
    3. 2.3. Axios(基于 Promise 封装的第三方库)
  3. 3. HTTP 请求
    1. 3.1. HTTP 请求:Web 通信的「消息格式」
    2. 3.2. HTTP 状态码:服务器的「响应状态」
    3. 3.3. Content-Type:数据的「格式标识」
  4. 4. 同源策略-跨域解决方案
  5. 5. 面试题
    1. 5.1. 倒计时抢购
    2. 5.2. readyState 变化几次