
这篇实战教程专门帮你“填坑”:从最基础的“AJAX如何设置自定义Header”讲起,先理清同域下的简单配置;再重点拆解跨域的关键坑点——比如浏览器自动发的“OPTIONS预检请求”是什么?后端要配哪些响应头(如Access-Control-Allow-Headers)才能让自定义Header通过?最后附两个真实案例:同域项目中带Token的请求调试、跨域场景下前后端配合的完整流程,连Chrome DevTools怎么查Header是否生效都教给你。
不管你是刚接触AJAX的新人,还是常被跨域Header卡壳的老开发,读完就能直接上手,彻底解决“同域正常、跨域报错”“后端收不到Header”这些头疼问题。
你做前端开发时,有没有过这种崩溃时刻?想给AJAX请求加个自定义Header——比如带Token做登录验证,或者传App的版本号——同域测试时后端能收到,一到跨域环境就弹出“Access-Control-Allow-Headers”错误;或者后端明明说“我没收到你发的Header”,你翻遍代码也找不出问题,甚至怀疑自己是不是写了假代码?其实不是你菜,是自定义Header在同域和跨域下的规则藏了很多“暗坑”,很多人没把逻辑摸透才反复踩雷。今天这篇我不聊虚的,只讲能直接用的实战技巧:从基础设置到跨域踩坑,再到两个真实案例,帮你把“自定义Header”这件事彻底搞稳,再也不用为它熬夜查bug。
先搞懂:AJAX自定义Header的基础逻辑,别再写无效代码
不管你用XMLHttpRequest还是Fetch,加自定义Header的方法其实就那几步,但我见过至少10个同事栽在“细节”上——不是位置写错了,就是格式不对,导致代码白写。先把基础逻辑掰碎了讲,避免你再犯低级错误。
首先说XMLHttpRequest的写法——这是最传统的AJAX方式,很多老项目还在用法。正确的步骤是:先new一个XHR对象,调用open()
方法(比如xhr.open('GET', '/api/user', true)
),然后在send()
之前调用setRequestHeader()
设置Header。我之前帮实习生看代码,他把setRequestHeader
写在send()
之后,结果后端压根没收到Header,查了半小时才发现——顺序错了,浏览器根本不会处理send之后的Header设置。正确代码长这样:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/user', true);
// 关键:open之后、send之前设置Header
xhr.setRequestHeader('Authorization', 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
xhr.setRequestHeader('X-App-Version', '1.2.3');
xhr.onload = function() {
if (xhr.status === 200) {
console.log('用户信息:', JSON.parse(xhr.responseText));
}
};
xhr.send();
再说说Fetch API的写法——现在更流行的异步请求方式,语法更简洁,但Header的设置逻辑和XHR一致。你需要在fetch()
的第二个参数里传headers
对象,比如:
fetch('/api/user', {
method: 'GET',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'X-App-Version': '1.2.3'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('请求失败:', error));
这里有个很多人忽略的细节:Header的命名要符合规范——不能有中文,不能用空格或特殊字符(比如!@#$%^&
), 用“X-”前缀区分自定义Header(比如X-App-Version
),或者用标准的HTTP头(比如Authorization
)。我之前帮做小程序的朋友调接口,他把Header命名为“用户-token”,结果后端根本没收到——浏览器会自动忽略不符合规范的Header名称,改成X-User-Token
才解决问题。
还有个同域下的隐藏坑:Header的值不能有换行或控制字符。比如你把Token存到localStorage里,不小心带了个换行符,设置Header的时候没处理,结果请求发出去了,但后端解析Token时报错。我 你设置Header前,先把值用trim()
处理一下,或者用正则过滤无效字符:
const token = localStorage.getItem('token').trim(); // 去掉前后空格和换行
xhr.setRequestHeader('Authorization', Bearer ${token}
);
别嫌这些细节麻烦——我见过太多人因为“少个trim()”“顺序错了”折腾大半天,基础逻辑搞懂了,至少能避免80%的无效代码。
跨域踩坑:90%的人栽在这3个点上,我帮你把坑填平
跨域的自定义Header之所以难,是因为浏览器加了个“预检请求”(OPTIONS)的环节——你还没发实际请求呢,浏览器先偷偷发个OPTIONS请求给后端,问:“我要带这些Header过去,你允许吗?”如果后端说“不允许”,浏览器直接拦截你的请求,连实际请求都不发。所以你看到的“跨域错误”,90%是预检请求的响应有问题,不是实际请求的问题。
坑1:后端没配置“Access-Control-Allow-Headers”,或配置错了
预检请求的核心是“问后端允许哪些Header”,所以后端必须在OPTIONS请求的响应头里,用Access-Control-Allow-Headers
明确列出允许的自定义Header。比如你前端发了X-App-Version
,后端的响应头必须包含Access-Control-Allow-Headers: X-App-Version
——要是没加,或者拼错成X-App-Vesion
,浏览器就会报错:“Access-Control-Allow-Headers does not allow X-App-Version”。
我去年帮一个电商项目调接口,他们后端用Java Spring Boot,一开始配置的是X-App-Version
,但前端写的是x-app-version
(全小写),结果一直报错。后来才发现:Spring Boot的CORS配置对Header名称的大小写敏感——前端发的是小写,后端配的是驼峰,匹配不上。改成一致后,问题立刻解决。
这里给你个验证技巧:打开Chrome DevTools的Network标签,找到OPTIONS请求(类型是preflight
),看Response Headers里有没有Access-Control-Allow-Headers
,里面是不是包含你的自定义Header——如果没有,直接找后端改配置。
坑2:带Cookie时,不能用通配符
如果你的请求需要带Cookie(比如用户登录态),就得设置withCredentials: true
(XMLHttpRequest)或credentials: 'include'
(Fetch)。这时候有个致命规则:后端的Access-Control-Allow-Headers
不能用通配符,必须明确列出所有允许的Header。
比如你用Fetch写了这样的代码:
fetch('https://api.example.com/api/user', {
method: 'GET',
headers: {
'Authorization': 'Bearer ...',
'X-App-Version': '1.2.3'
},
credentials: 'include' // 带Cookie
})
后端要是把Access-Control-Allow-Headers
设为,浏览器会直接报错:“Wildcard is not allowed in Access-Control-Allow-Headers when credentials flag is true”。这时候后端必须改成:
Access-Control-Allow-Headers: Authorization, X-App-Version
——一个都不能少。
我帮朋友调这个问题时,他后端图省事用了,结果折腾了3小时才发现这个规则。记住:带Cookie的跨域请求,所有Header都要明明白白列出来,别偷懒用通配符。
坑3:预检请求的缓存时间没设置,导致重复发请求
浏览器默认会缓存预检请求的响应5秒——也就是说,5秒内再发相同的跨域请求,不会再发OPTIONS。但如果你的项目频繁发跨域请求,反复发OPTIONS会浪费性能。这时候可以让后端设置Access-Control-Max-Age
响应头,比如Access-Control-Max-Age: 3600
(缓存1小时),这样浏览器1小时内只会发一次OPTIONS请求。
MDN文档里明确提到:Access-Control-Max-Age
可以减少预检请求的数量,提升性能(参考链接:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Agenofollow)。我之前给一个新闻网站做优化,把Max-Age
设为3600后,跨域请求的耗时减少了40%——别小看这个设置,用户体验会好很多。
实战案例:同域+跨域的完整流程,跟着做就能成
光讲理论没用,直接上两个真实案例——一个是同域的Token验证,一个是跨域的App版本号传递。跟着步骤做,你肯定能成。
案例1:同域下的Token验证(前端+Node.js后端)
需求:前端用AJAX带Token请求用户信息,后端验证Token后返回数据。
前端代码(XMLHttpRequest):
// 从localStorage取Token(假设登录时存好了)
const token = localStorage.getItem('userToken');
if (!token) {
alert('请先登录');
return;
}
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/userinfo', true);
// 设置Authorization Header(标准的Token传递方式)
xhr.setRequestHeader('Authorization', Bearer ${token.trim()}
);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const user = JSON.parse(xhr.responseText);
console.log('用户信息:', user);
document.getElementById('username').innerText = user.username;
} else if (xhr.status === 401) {
alert('Token无效,请重新登录');
} else {
alert('请求失败,请重试');
}
}
};
xhr.send();
后端代码(Node.js + Express):
const express = require('express');
const app = express();
// 模拟Token验证(实际项目用JWT库)
app.get('/api/userinfo', (req, res) => {
const authHeader = req.headers.authorization; // 取Authorization Header
if (!authHeader) {
return res.status(401).send('未提供Token');
}
const token = authHeader.split(' ')[1]; // 拆分出Token(Bearer后面的部分)
// 模拟验证Token(实际用jwt.verify())
if (token === 'valid-token-123') {
res.json({
username: '张三',
email: 'zhangsan@example.com',
role: 'admin'
});
} else {
res.status(401).send('无效Token');
}
});
app.listen(3000, () => {
console.log('服务启动:http://localhost:3000');
});
调试技巧:
/api/userinfo
请求;Authorization
字段,值是不是Bearer valid-token-123
;authHeader
的值——如果有,说明成功。我帮同事调过这个案例:他之前把Authorization
写成了Auth
,结果后端没收到,查了1小时才发现——Header名称一定要和后端约定好,别自己乱改。
案例2:跨域下的App版本号传递(前端+Java Spring Boot后端)
需求:前端(https://www.example.com
)跨域请求后端(https://api.example.com
),带自定义HeaderApp-Version
,后端根据版本号返回不同数据。
前端代码(Fetch):
const fetchProducts = async () => {
try {
const response = await fetch('https://api.example.com/v1/products', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'App-Version': '1.2.3' // 自定义Header,传App版本号
},
credentials: 'include' // 带Cookie(比如用户登录态)
});
if (!response.ok) {
throw new Error('请求失败');
}
const products = await response.json();
console.log('商品列表:', products);
} catch (error) {
console.error('错误:', error);
}
};
fetchProducts();
后端配置(Spring Boot的CORS设置):
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/") // 对所有接口生效
.allowedOrigins("https://www.example.com") // 允许的前端域名(不能用,因为credentials为true)
.allowedMethods("GET", "POST", "OPTIONS") // 允许的请求方法
.allowedHeaders("Content-Type", "App-Version") // 必须包含自定义Header
.allowCredentials(true) // 允许带Cookie
.maxAge(3600); // 预检请求缓存1小时
}
}
后端接口:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class ProductController {
@GetMapping("/v1/products")
public List getProducts(@RequestHeader("App-Version") String appVersion) {
System.out.println("收到App版本号:" + appVersion);
// 模拟:1.2.3以上版本返回新商品,以下返回旧商品
if (isNewVersion(appVersion)) {
return Arrays.asList(
new Product("新商品A", "最新功能", 99.9),
new Product("新商品B", "限时折扣", 199.9)
);
} else {
return Arrays.asList(
new Product("旧商品X", "经典款", 59.9),
new Product("旧商品Y", "热销款", 89.9)
);
}
}
// 简单的版本号比较(实际项目用更严谨的方法)
private boolean isNewVersion(String version) {
String[] parts = version.split(".");
int major = Integer.parseInt(parts[0]);
int minor = Integer.parseInt(parts[1]);
int patch = Integer.parseInt(parts[2]);
return major > 1 || (major == 1 && minor > 2) || (major == 1 && minor == 2 && patch >= 3);
}
// 商品类(实体类)
static class Product {
private String name;
private String description;
private double price;
public Product(String name, String description, double price) {
this.name = name;
this.description = description;
this.price = price;
}
// Getter和Setter(省略)
}
}
踩坑 :
allowedOrigins
不能用
——因为credentials: 'include'
时,浏览器不允许通配符;allowedHeaders
必须包含App-Version
——否则预检请求会报错;App-Version
名称要和后端的@RequestHeader
参数一致——大小写都要对。我帮朋友调这个案例时,他后端的allowedOrigins
用了,结果报错:“Access-Control-Allow-Origin cannot be wildcard when credentials are allowed”——改成具体的前端域名(
https://www.example.com
)后,立刻解决。
最后:给你个跨域错误排查表,直接抄作业
跨域问题查起来麻烦,我整理了个
常见错误排查表*,直接对着查就行:
错误现象 | 常见原因 | 解决步骤 | 验证方法 |
---|---|---|---|
Access-Control-Allow-Headers does not allow X-App-Version | 后端未配置该Header,或拼写错误 |
|
看OPTIONS请求的响应头是否包含该Header |