AJAX请求方法详解GET与POST的实战区别表单提交文件上传跨域问题解决常见错误排查完整指南

嗨,朋友!欢迎来到AJAX的世界。想象一下,AJAX就是你网页上的”快递员”,它在后台悄悄地去服务器取数据,然后让你的页面在不刷新、不卡顿的情况下更新内容。这感觉就像你在看视频的时候,弹幕还在不停滚动,而页面本身纹丝不动——这就是AJAX的魔力。

今天咱们就把AJAX里最核心的GET和POST两大请求方法掰开揉碎讲清楚,顺便把表单提交、文件上传、跨域问题这些头疼的事都过一遍。别担心,我会尽量用大白话来讲,你边看边学,慢慢就明白了。


GET和POST,这两位老朋友到底有什么区别

我打赌你第一次学AJAX的时候,老师一定说”GET用来获取数据,POST用来提交数据”。这句话没错,但太浅了。咱们来点实在的。

本质上的区别

GET请求会把数据附加在URL后面,就像你在浏览器地址栏里看到的那些参数一样:

https://api.example.com/users?id=123&name=张三

而POST请求呢?数据是放在请求体(body)里的,URL干干净净:

POST https://api.example.com/users

数据在body里长这样:

{
  "id": 123,
  "name": "张三"
}

你看,这就好比GET是把东西放在明信片上寄出去,所有人都能看到;POST是把东西装进信封里寄出去,只有收信人知道里面是什么。

数据长度的限制

GET请求的数据长度是有限制的,原因很简单——数据全写在URL里了,而浏览器的URL长度是有限制的。Chrome大概支持2048个字符,Firefox支持65536个字符,但不同浏览器不一样。如果你的数据超过了这个限制,GET就炸了。

POST请求理论上没有长度限制,因为数据在body里,而body的长度主要受服务器配置的影响。你传10KB或者10MB,浏览器不会拦你,服务器才会说”哎呀太多了”。

安全性问题

很多人以为POST比GET安全,这个说法不完全对。HTTPS加密之后,GET和POST的数据在传输过程中都是加密的。区别在于:

  • GET的数据在URL里,会出现在浏览器历史记录、服务器日志、代理服务器日志里, anyonewhoever能看到
  • POST的数据在body里,不会出现在这些地方

所以如果你的请求包含敏感信息(比如密码、身份证号),用POST更合适。但记住,光靠POST不够,一定要上HTTPS。

缓存行为

GET请求是可以被浏览器缓存的。你第一次请求某个GET接口,浏览器可能会把结果存起来,下次再请求同样的URL就直接用缓存,不发送网络请求了。这个特性在某些场景下是好事,在某些场景下会让人抓狂。

POST请求不会被缓存。每次POST都是全新的请求,服务器必须重新处理。

幂等性

这个概念听起来有点高级,其实很简单。”幂等”的意思就是:同一个请求做多次,结果和做一次是一样的。

GET是幂等的。你请求100次”获取用户信息”,服务器返回的数据应该是一样的,不会因为你请求了100次就多了一个用户。

POST不是幂等的。你请求100次”提交订单”,服务器可能会创建100个订单。这就是为什么GET和POST在语义上有本质的区别。

实战代码对比

咱们写个代码来看看:

// GET请求的写法
fetch('https://api.example.com/users?id=123&name=张三')
  .then(response => response.json())
  .then(data => {
    console.log('获取到的用户数据:', data);
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

// 或者用axios
axios.get('https://api.example.com/users', {
  params: {
    id: 123,
    name: '张三'
  }
})
.then(response => {
  console.log('获取到的用户数据:', response.data);
});
// POST请求的写法
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    id: 123,
    name: '张三'
  })
})
.then(response => response.json())
.then(data => {
  console.log('提交成功:', data);
})
.catch(error => {
  console.error('提交失败:', error);
});

// 或者用axios
axios.post('https://api.example.com/users', {
  id: 123,
  name: '张三'
})
.then(response => {
  console.log('提交成功:', response.data);
});

看到了吗?GET的请求参数直接拼在URL后面,POST的请求参数放在body里。这就是最本质的区别。

什么时候用GET,什么时候用POST

这里有一个简单的判断逻辑:

  1. 查询数据 → 用GET。比如搜索、分页、筛选、获取详情
  2. 修改/创建数据 → 用POST。比如登录、注册、提交表单、创建订单
  3. 涉及敏感信息 → 用POST。比如密码、token、个人信息
  4. 数据量较大 → 用POST。GET有长度限制
  5. 需要幂等性 → 用GET。重复请求不会有副作用

当然,现实世界不是非黑即白的。有时候你会看到用GET请求来提交数据的,也有用POST请求来查询数据的。这不是绝对不可以,但违反了RESTful的规范,会让其他开发者困惑。


表单提交的那些事儿

表单提交是AJAX最常见的应用场景之一。想象一下,用户注册了一个新账号,填写完表单后点击提交。如果用传统的方式,整个页面会刷新,用户会看到一片白,然后才看到注册成功的提示。用AJAX的话,页面不动,数据悄悄提交,体验丝滑多了。

传统表单提交 vs AJAX表单提交

传统的方式是这样的:

<form action="/register" method="POST">
  <input type="text" name="username" placeholder="用户名" required>
  <input type="password" name="password" placeholder="密码" required>
  <input type="email" name="email" placeholder="邮箱" required>
  <button type="submit">注册</button>
</form>

用户点击提交后,浏览器会跳转到/register页面,整个页面刷新。这个过程慢,体验差,而且用户看不到任何反馈,不知道自己提交成功没有。

用AJAX改写之后:

<form id="registerForm">
  <input type="text" id="username" name="username" placeholder="用户名" required>
  <input type="password" id="password" name="password" placeholder="密码" required>
  <input type="email" id="email" name="email" placeholder="邮箱" required>
  <button type="submit">注册</button>
</form>

<script>
document.getElementById('registerForm').addEventListener('submit', function(e) {
  // 阻止默认的表单提交行为(防止页面刷新)
  e.preventDefault();

  // 获取表单数据
  const formData = new FormData(this);

  // 发送AJAX请求
  fetch('/register', {
    method: 'POST',
    body: formData
  })
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      alert('注册成功!');
      this.reset(); // 清空表单
    } else {
      alert('注册失败:' + data.message);
    }
  })
  .catch(error => {
    alert('网络错误,请稍后重试');
    console.error('注册失败:', error);
  });
});
</script>

代码看起来有点多,但逻辑很简单:

  1. e.preventDefault():阻止表单的默认提交行为,不让页面刷新
  2. new FormData(this):把表单数据打包成一个FormData对象
  3. fetch:发送POST请求,把FormData作为body传过去

用FormData对象处理表单数据

FormData是一个非常有用的API,它能帮你轻松处理表单数据。而且它不仅能处理文本输入,还能处理文件上传——这一点后面会详细讲。

// 创建FormData对象
const formData = new FormData();

// 添加普通字段
formData.append('username', '张三');
formData.append('age', 25);
formData.append('email', 'zhangsan@example.com');

// 添加多个值(比如多选框)
formData.append('hobbies', 'reading');
formData.append('hobbies', 'gaming');

// 添加文件
const fileInput = document.getElementById('avatar');
formData.append('avatar', fileInput.files[0]);

// 发送请求
fetch('/api/upload', {
  method: 'POST',
  body: formData
  // 注意:使用FormData时不要手动设置Content-Type
  // 浏览器会自动设置,并加上boundary
})
.then(response => response.json())
.then(data => console.log('成功:', data))
.catch(error => console.error('失败:', error));

这里有一个非常非常重要的点:当你使用FormData时,不要手动设置Content-Typeapplication/json。因为FormData有自己的编码方式,浏览器会自动帮你设置正确的Content-Type,并加上boundary参数。如果你手动设置了Content-Type为application/json,服务器会无法解析你的数据。

用axios处理表单提交

axios比fetch更简洁一些,特别是处理表单数据的时候:

import axios from 'axios';

const form = document.getElementById('registerForm');

form.addEventListener('submit', async function(e) {
  e.preventDefault();

  const formData = new FormData(this);

  try {
    const response = await axios.post('/register', formData);
    console.log('注册成功:', response.data);
    this.reset();
  } catch (error) {
    console.error('注册失败:', error.response?.data || error.message);
    alert('注册失败,请稍后重试');
  }
});

用async/await的写法更清晰,不用链式调用.then(),代码看起来更像同步的逻辑,读起来更容易理解。


文件上传——AJAX最强大的能力之一

文件上传是传统表单提交处理不了的(或者说处理起来很麻烦),但用AJAX就轻松多了。你可以上传一张头像照片、一份简历、甚至一个视频文件,全程页面不刷新,用户还能看到上传进度。

基础的文件上传

<input type="file" id="fileInput" accept="image/*">
<button id="uploadBtn">上传文件</button>
<div id="progressBar" style="display:none;">
  <progress max="100" value="0"></progress>
  <span id="progressText">0%</span>
</div>
<div id="result"></div>
document.getElementById('uploadBtn').addEventListener('click', function() {
  const fileInput = document.getElementById('fileInput');
  const file = fileInput.files[0];

  if (!file) {
    alert('请先选择一个文件');
    return;
  }

  // 限制文件大小(比如不超过5MB)
  if (file.size > 5 * 1024 * 1024) {
    alert('文件太大啦,请选择5MB以内的文件');
    return;
  }

  // 限制文件类型
  if (!file.type.startsWith('image/')) {
    alert('只能上传图片文件');
    return;
  }

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

  const xhr = new XMLHttpRequest();

  // 监听上传进度
  xhr.upload.addEventListener('progress', function(e) {
    if (e.lengthComputable) {
      const percent = Math.round((e.loaded / e.total) * 100);
      document.getElementById('progressBar').style.display = 'block';
      document.querySelector('progress').value = percent;
      document.getElementById('progressText').textContent = percent + '%';
    }
  });

  // 监听上传完成
  xhr.addEventListener('load', function() {
    if (xhr.status === 200) {
      const result = JSON.parse(xhr.responseText);
      document.getElementById('result').innerHTML =
        '<img src="' + result.url + '" style="max-width:200px;">';
      document.getElementById('progressBar').style.display = 'none';
    } else {
      alert('上传失败,状态码:' + xhr.status);
    }
  });

  // 监听错误
  xhr.addEventListener('error', function() {
    alert('网络错误,上传失败');
    document.getElementById('progressBar').style.display = 'none';
  });

  xhr.open('POST', '/api/upload');
  xhr.send(formData);
});

这段代码用了原生的XMLHttpRequest,因为它能监听上传进度。fetch和axios默认不支持上传进度的监听,如果需要监听进度,还是得用XHR。

用fetch上传文件(不带进度条)

如果你不需要显示上传进度,用fetch更简洁:

document.getElementById('uploadBtn').addEventListener('click', async function() {
  const fileInput = document.getElementById('fileInput');
  const file = fileInput.files[0];

  if (!file) {
    alert('请先选择一个文件');
    return;
  }

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

  try {
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData
      // 不要设置Content-Type,浏览器会自动设置
    });

    const data = await response.json();

    if (data.url) {
      document.getElementById('result').innerHTML =
        '<img src="' + data.url + '" style="max-width:200px;">';
    } else {
      alert('上传失败:' + data.message);
    }
  } catch (error) {
    alert('网络错误:' + error.message);
  }
});

多文件上传

有时候你需要一次上传多个文件,比如用户要上传多张头像照片:

<input type="file" id="multiFileInput" accept="image/*" multiple>
<button id="multiUploadBtn">上传多个文件</button>
<div id="multiResult"></div>
document.getElementById('multiUploadBtn').addEventListener('click', async function() {
  const fileInput = document.getElementById('multiFileInput');
  const files = fileInput.files;

  if (files.length === 0) {
    alert('请先选择文件');
    return;
  }

  const formData = new FormData();
  // 遍历所有文件,逐个添加
  for (let i = 0; i < files.length; i++) {
    formData.append('files[]', files[i]); // 注意key是files[],数组形式
  }

  try {
    const response = await fetch('/api/upload/multi', {
      method: 'POST',
      body: formData
    });
    const data = await response.json();
    console.log('上传结果:', data);
  } catch (error) {
    console.error('上传失败:', error);
  }
});

在服务器端,你需要用数组的方式接收这些文件。不同的服务器框架有不同的处理方式,这里就不展开了。

上传大文件——分片上传

如果用户上传的是一个几百MB甚至几个GB的视频文件,一次性上传会非常慢,而且一旦中断就要重新来。这时候就需要分片上传了:

async function uploadLargeFile(file) {
  const CHUNK_SIZE = 5 * 1024 * 1024; // 每片5MB
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);

  for (let i = 0; i < totalChunks; i++) {
    const start = i * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);

    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('chunkIndex', i);
    formData.append('totalChunks', totalChunks);
    formData.append('fileName', file.name);

    try {
      const response = await fetch('/api/upload/chunk', {
        method: 'POST',
        body: formData
      });
      const result = await response.json();

      if (!result.success) {
        throw new Error('分片上传失败:' + result.message);
      }

      console.log(`进度:${i + 1}/${totalChunks}`);
    } catch (error) {
      console.error('上传出错:', error);
      throw error;
    }
  }

  // 所有分片上传完成,通知服务器合并
  const formData = new FormData();
  formData.append('fileName', file.name);
  formData.append('totalChunks', totalChunks);

  const response = await fetch('/api/upload/merge', {
    method: 'POST',
    body: formData
  });

  const result = await response.json();
  return result;
}

// 使用示例
document.getElementById('largeFileInput').addEventListener('change', function(e) {
  const file = e.target.files[0];
  if (file) {
    uploadLargeFile(file)
      .then(result => alert('上传成功!'))
      .catch(error => alert('上传失败:' + error.message));
  }
});

分片上传的核心思想很简单:把大文件切成小块,一块一块地上传,最后告诉服务器”我传完了,你帮我拼起来吧”。这样即使网络中断,也可以从断开的地方继续传,不需要从头开始。


跨域问题——AJAX最让人头疼的坑

跨域问题是每一个前端开发者都会遇到的”拦路虎”。我第一次遇到的时候,控制台里报了一堆红字,完全不知道怎么回事。后来学了才明白,这是浏览器的同源策略在起作用。

什么是同源策略

浏览器的同源策略规定:只有当两个URL的协议、域名、端口完全一致时,才算是”同源”。否则就是”跨域”。

举个例子:

  • https://www.example.com/apihttps://www.example.com/users → 同源 ✅
  • https://www.example.com/apihttp://www.example.com/api → 不同源(协议不同)❌
  • https://www.example.com/apihttps://api.example.com/data → 不同源(域名不同)❌
  • https://www.example.com:8080/apihttps://www.example.com:9090/api → 不同源(端口不同)❌

当你的前端页面和API服务不在同一个源时,浏览器会拦截AJAX请求,这就是跨域问题。

跨域错误的表现

当跨域请求被拦截时,你会在控制台看到这样的错误:

Access to XMLHttpRequest at 'https://api.example.com/users' from origin
'https://www.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

翻译一下就是:你的页面在www.example.com,但请求了api.example.com的数据,服务器没有返回Access-Control-Allow-Origin这个响应头,所以浏览器拒绝了这个请求。

CORS——跨域资源共享

CORS(Cross-Origin Resource Sharing)是解决跨域问题的标准方案。它的原理很简单:服务器在响应头里加上一些特定的字段,告诉浏览器”我允许这个来源的请求”。

比如,服务器可以这样返回响应:

Access-Control-Allow-Origin: https://www.example.com

或者,如果服务器允许所有来源:

Access-Control-Allow-Origin: *

浏览器看到这些头,就会放行请求。

前端的解决方案

前端能做的事情其实不多,因为跨域问题主要是服务器端需要配置。但前端可以做这些:

1. 使用代理(开发环境)

在开发时,你可以配置一个代理,让所有API请求都走本地的开发服务器,这样就不会有跨域问题:

// webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true
      }
    }
  }
};

这样你请求/api/users,代理会帮你转发到https://api.example.com/api/users,浏览器看到的是同源请求,不会拦截。

2. JSONP(老旧方案)

JSONP是一种比较老的跨域方案,只能用于GET请求。它的原理是利用<script>标签不受同源策略限制的特点:

function jsonp(url, callback) {
  const script = document.createElement('script');
  const callbackName = 'jsonpCallback_' + Date.now();

  // 在全局暴露回调函数
  window[callbackName] = function(data) {
    callback(data);
    // 清理
    delete window[callbackName];
    document.body.removeChild(script);
  };

  // 拼接URL
  script.src = url + '?callback=' + callbackName;
  document.body.appendChild(script);
}

// 使用
jsonp('https://api.example.com/data', function(result) {
  console.log('获取到的数据:', result);
});

JSONP的原理很简单:服务器收到请求后,不会直接返回JSON数据,而是返回一段JavaScript代码,调用你提供的回调函数。比如:

jsonpCallback_1234567890({
  "name": "张三",
  "age": 25
});

浏览器执行这段代码,就拿到了数据。但JSONP有个致命的缺陷:它只能发GET请求,而且依赖服务器支持。现代开发中基本不用了。

3. Nginx反向代理(生产环境)

在生产环境,跨域问题通常由服务器或者Nginx来解决:

# Nginx配置
server {
    listen 80;
    server_name www.example.com;

    location /api/ {
        proxy_pass https://api.example.com/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

这样前端请求/api/users,Nginx会转发到https://api.example.com/api/users,浏览器看到的是同源请求。

服务器端的CORS配置

如果服务器支持CORS,配置起来很简单:

Node.js + Express

const express = require('express');
const cors = require('cors');
const app = express();

// 允许所有来源
app.use(cors());

// 或者只允许特定来源
app.use(cors({
  origin: 'https://www.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.get('/api/users', (req, res) => {
  res.json({ name: '张三', age: 25 });
});

app.listen(3000);

Python + Flask

from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "https://www.example.com"}})

@app.route('/api/users')
def get_users():
    return jsonify({"name": "张三", "age": 25})

if __name__ == '__main__':
    app.run(port=3000)

Java + Spring Boot

@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("https://www.example.com");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);

        return new CorsFilter(source);
    }
}

预检请求(Preflight Request)

有些请求比较复杂,浏览器在发送实际请求之前,会先发送一个OPTIONS请求来”试探”服务器是否允许跨域。这个OPTIONS请求就是预检请求。

什么样的请求会触发预检?

  1. 使用了GET/POST之外的方法(如PUT、DELETE、PATCH)
  2. 设置了自定义请求头(如AuthorizationX-Custom-Header
  3. Content-Type不是application/x-www-form-urlencodedmultipart/form-datatext/plain

预检请求的响应需要包含这些头:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Access-Control-Max-Age告诉浏览器,这个预检结果可以缓存多久(单位是秒)。设置一个合理的值可以减少预检请求的频率,提升性能。

带Cookie的跨域请求

有时候你的跨域请求需要携带Cookie(比如用户登录状态)。这时候不能简单地用*作为Access-Control-Allow-Origin,需要指定具体的域名,并且设置Access-Control-Allow-Credentials: true

// 前端请求
fetch('https://api.example.com/user', {
  method: 'GET',
  credentials: 'include'  // 重要:告诉浏览器带上Cookie
})
.then(response => response.json())
.then(data => console.log(data));
// 服务器响应
res.setHeader('Access-Control-Allow-Origin', 'https://www.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

注意:当Allow-Credentialstrue时,Allow-Origin不能设置为*,必须指定具体的域名。这是浏览器的安全限制。


常见错误排查——那些年踩过的坑

AJAX开发过程中,错误无处不在。下面我把常见的错误类型整理出来,每个都配上具体的例子和解决方案。

1. 404错误——接口不存在

这是最简单的错误,说明你请求的URL不对。检查一下:

  • URL路径是否正确
  • 大小写是否正确(很多服务器对大小写敏感)
  • 域名是否正确(是不是漏了http/https)
  • 是否多了一个斜杠或少了一个斜杠
// 错误示例
fetch('https://api.example.com/users/')  // 注意末尾的斜杠

// 正确示例
fetch('https://api.example.com/users')

2. 403错误——权限不足

403说明你找到了接口,但服务器拒绝你的请求。常见原因:

  • 需要登录才能访问,但你的请求没有携带token
  • IP被限制
  • 请求频率过高,被限流

解决方案:

fetch('https://api.example.com/users', {
  headers: {
    'Authorization': 'Bearer ' + localStorage.getItem('token')
  }
});

3. 401错误——未授权

401和403的区别在于:401是”你是谁?”,403是”我知道你是谁,但你不准进”。401通常意味着登录过期了,需要重新登录。

fetch('/api/users')
  .then(response => {
    if (response.status === 401) {
      // 跳转到登录页
      window.location.href = '/login';
    }
    return response.json();
  });

4. 500错误——服务器内部错误

500说明服务器端出了问题,不是你前端的问题。这时候你需要:

  • 查看服务器的错误日志
  • 联系后端开发人员
  • 检查你发送的数据格式是否正确(有时候数据格式不对会导致服务器崩溃)

5. Network Error——网络错误

Network Error是一个比较笼统的错误,可能有很多原因:

  • 服务器宕机了
  • 网络中断了
  • 跨域问题(有时候跨域错误会显示为Network Error)
  • SSL证书问题

排查方法:打开浏览器的开发者工具,查看Network面板,看看请求的详细信息。

6. CORS错误——跨域问题

前面已经讲了很多,这里再补充几个常见的CORS错误:

错误一:Access-Control-Allow-Origin缺失

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://www.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

原因:服务器没有设置Access-Control-Allow-Origin头。

解决:让后端在响应头里加上Access-Control-Allow-Origin: https://www.example.com

错误二:Credentials错误

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://www.example.com' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header in the response must
not be the wildcard '*' when the request's credentials mode is 'include'.

原因:你设置了credentials: 'include',但服务器的Access-Control-Allow-Origin*

解决:服务器把Access-Control-Allow-Origin改成具体的域名。

错误三:预检请求失败

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://www.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

原因:你的请求触发了预检(OPTIONS请求),但服务器没有正确处理。

解决:确保服务器正确处理OPTIONS请求,并返回正确的CORS头。

7. 数据解析错误

有时候请求成功了,但数据解析报错:

fetch('/api/users')
  .then(response => response.json())  // 这里可能会报错
  .then(data => console.log(data));

如果服务器返回的不是JSON格式(比如返回的是HTML错误页面),response.json()就会抛出异常。

解决方案:

fetch('/api/users')
  .then(response => {
    // 先检查Content-Type
    const contentType = response.headers.get('content-type');
    if (!contentType || !contentType.includes('application/json')) {
      throw new Error('返回的不是JSON数据');
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error('解析错误:', error));

8. 请求超时

有时候请求会卡住很久,最后超时。可以设置超时时间:

// 用AbortController设置超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时

fetch('https://api.example.com/users', {
  signal: controller.signal
})
.then(response => response.json())
.then(data => {
  clearTimeout(timeoutId);
  console.log(data);
})
.catch(error => {
  clearTimeout(timeoutId);
  if (error.name === 'AbortError') {
    console.error('请求超时了');
  } else {
    console.error('请求失败:', error);
  }
});

9. JSON序列化/反序列化错误

这是新手最容易犯的错误之一。把对象转成JSON字符串的时候漏掉了JSON.stringify,或者从JSON字符串解析的时候漏掉了JSON.parse

// 错误:没有序列化成JSON字符串
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: { name: '张三', age: 25 }  // 这是对象,不是字符串
});

// 正确:使用JSON.stringify
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: '张三', age: 25 })
});

10. 异步处理顺序错误

AJAX是异步的,很多时候新手会犯顺序错误:

// 错误示例
let userData;
fetch('/api/users/1')
  .then(response => response.json())
  .then(data => {
    userData = data;
  });

console.log(userData); // undefined!因为fetch还没完成
// 正确示例:用async/await
async function getUserData() {
  const response = await fetch('/api/users/1');
  const userData = await response.json();
  console.log(userData); // 这里才有数据
}
getUserData();

最佳实践和性能优化

写完了那么多代码,咱们最后来聊聊最佳实践。好的代码习惯能让你的AJAX请求更稳定、更快、更安全。

1. 统一封装请求方法

不要让每个页面都写一遍fetch或axios的代码。封装一个通用的请求方法:

// request.js
import axios from 'axios';

const request = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器
request.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = 'Bearer ' + token;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 响应拦截器
request.interceptors.response.use(
  response => {
    return response.data;
  },
  error => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          window.location.href = '/login';
          break;
        case 403:
          alert('你没有权限访问这个资源');
          break;
        case 404:
          alert('请求的资源不存在');
          break;
        case 500:
          alert('服务器出错了,请稍后重试');
          break;
        default:
          alert('请求失败:' + error.response.status);
      }
    } else {
      alert('网络错误,请检查网络连接');
    }
    return Promise.reject(error);
  }
);

export default request;
// 使用封装好的请求方法
import request from './request';

// 获取用户列表
async function getUsers() {
  try {
    const users = await request.get('/users');
    return users;
  } catch (error) {
    console.error('获取用户列表失败:', error);
    return [];
  }
}

// 创建用户
async function createUser(userData) {
  try {
    const result = await request.post('/users', userData);
    return result;
  } catch (error) {
    console.error('创建用户失败:', error);
    throw error;
  }
}

封装之后,你就不用每次手写重复的代码了。拦截器还能统一处理token、错误提示等逻辑。

2. 防抖和节流

有些操作会频繁触发AJAX请求,比如搜索框的输入。如果每次按键都发送请求,服务器会承受很大的压力。这时候可以用防抖(debounce)或节流(throttle):

// 防抖:延迟执行,最后一次触发后才执行
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流:固定间隔执行
function throttle(fn, delay) {
  let last = 0;
  return function(...args) {
    const now = Date.now();
    if (now - last > delay) {
      fn.apply(this, args);
      last = now;
    }
  };
}

// 使用防抖的搜索功能
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', debounce(async function(e) {
  const keyword = e.target.value.trim();
  if (keyword.length < 2) return;

  const response = await fetch('/api/search?q=' + encodeURIComponent(keyword));
  const results = await response.json();
  console.log('搜索结果:', results);
}, 300));

防抖和节流的区别:防抖是”等一等,看看还有没有新的触发”,节流是”每隔一段时间执行一次”。搜索功能用防抖,滚动加载用节流。

3. 请求取消

有时候用户触发了一个请求,但又快速触发了另一个请求,第一个请求的结果就会覆盖第二个的结果。用AbortController可以取消请求:

let currentController = null;

async function searchUsers(keyword) {
  // 取消之前的请求
  if (currentController) {
    currentController.abort();
  }

  currentController = new AbortController();

  try {
    const response = await fetch('/api/users?keyword=' + keyword, {
      signal: currentController.signal
    });
    const data = await response.json();
    console.log('搜索结果:', data);
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('搜索失败:', error);
    }
  }
}

searchInput.addEventListener('input', function(e) {
  searchUsers(e.target.value);
});

4. 缓存策略

对于不常变化的数据,可以用缓存减少请求次数:

const cache = new Map();

async function getCachedData(url, ttl = 60000) {
  const cached = cache.get(url);
  if (cached && Date.now() - cached.time < ttl) {
    return cached.data;
  }

  const response = await fetch(url);
  const data = await response.json();

  cache.set(url, {
    data: data,
    time: Date.now()
  });

  return data;
}

5. 错误重试

网络不稳定时,请求可能会失败。可以适当重试:

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) {
        throw new Error('HTTP错误:' + response.status);
      }
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;
      // 等待一段时间后重试
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

总结

AJAX的GET和POST请求虽然看起来简单,但里面藏着不少学问。GET适合查询、POST适合修改,这个原则要牢记。表单提交用FormData来处理,文件上传要分片处理大文件,跨域问题大部分靠服务器配置CORS来解决。

最重要的是,遇到问题不要慌。先看控制台报什么错,再查Network面板看请求和响应的详细信息,大多数问题都能很快定位到。

AJAX是现代网页开发的基石之一,理解它、用好它,你的网页会变得又快又好。希望这篇文章能帮到你,如果还有疑问,随时来问!