Skip to content

AJAX 与 Fetch

AJAX(Asynchronous JavaScript and XML)是实现网页异步数据交互的技术。现代开发中,Fetch API 已成为首选的网络请求方式。

🎯 XMLHttpRequest#

基本用法#

// 创建 XHR 对象
const xhr = new XMLHttpRequest()
// 配置请求
xhr.open('GET', '/api/users', true) // true 表示异步
// 设置响应处理
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText)
console.log('成功:', data)
} else {
console.error('HTTP 错误:', xhr.status)
}
}
xhr.onerror = function () {
console.error('网络错误')
}
// 发送请求
xhr.send()

发送 POST 请求#

const xhr = new XMLHttpRequest()
xhr.open('POST', '/api/users')
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.onload = function () {
if (xhr.status === 201) {
console.log('创建成功')
}
}
// 发送 JSON 数据
const data = { name: '张三', age: 25 }
xhr.send(JSON.stringify(data))

请求状态#

const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
console.log('状态:', xhr.readyState)
// 0: 未初始化
// 1: 已打开(open 调用后)
// 2: 已发送(send 调用后)
// 3: 接收中(部分响应)
// 4: 完成(响应完成)
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('请求完成')
}
}
xhr.open('GET', '/api/data')
xhr.send()

超时和取消#

const xhr = new XMLHttpRequest()
xhr.open('GET', '/api/slow')
// 设置超时(毫秒)
xhr.timeout = 5000
xhr.ontimeout = function () {
console.error('请求超时')
}
xhr.send()
// 取消请求
setTimeout(() => {
xhr.abort()
console.log('请求已取消')
}, 3000)

上传进度#

const xhr = new XMLHttpRequest()
xhr.open('POST', '/api/upload')
// 上传进度
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100
console.log(`上传进度: ${percent.toFixed(2)}%`)
}
}
// 下载进度
xhr.onprogress = function (e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100
console.log(`下载进度: ${percent.toFixed(2)}%`)
}
}
const formData = new FormData()
formData.append('file', fileInput.files[0])
xhr.send(formData)

XHR 封装#

function ajax(options) {
return new Promise((resolve, reject) => {
const { method = 'GET', url, data, headers = {}, timeout = 10000 } = options
const xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.timeout = timeout
// 设置请求头
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value)
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText))
} catch {
resolve(xhr.responseText)
}
} else {
reject(new Error(`HTTP ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('网络错误'))
xhr.ontimeout = () => reject(new Error('请求超时'))
if (data) {
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify(data))
} else {
xhr.send()
}
})
}
// 使用
ajax({
method: 'POST',
url: '/api/users',
data: { name: '张三' },
}).then((data) => console.log(data))

Fetch API#

基本用法#

// GET 请求
fetch('/api/users')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
})
.then((data) => console.log(data))
.catch((error) => console.error(error))
// async/await 方式
async function getUsers() {
try {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const data = await response.json()
return data
} catch (error) {
console.error('请求失败:', error)
}
}

POST 请求#

// 发送 JSON
async function createUser(user) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
createUser({ name: '张三', age: 25 })
// 发送 FormData
async function uploadFile(file) {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData, // 不要设置 Content-Type
})
return response.json()
}

请求配置#

fetch('/api/data', {
method: 'POST', // GET, POST, PUT, DELETE, PATCH
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
body: JSON.stringify(data),
mode: 'cors', // cors, no-cors, same-origin
credentials: 'include', // omit, same-origin, include
cache: 'no-cache', // default, no-cache, reload, force-cache
redirect: 'follow', // follow, error, manual
signal: abortController.signal, // 取消信号
})

响应处理#

const response = await fetch('/api/data')
// 响应状态
console.log(response.status) // 200
console.log(response.statusText) // 'OK'
console.log(response.ok) // true (200-299)
console.log(response.url) // 最终 URL
// 响应头
console.log(response.headers.get('Content-Type'))
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`)
}
// 解析响应体(只能读取一次)
const json = await response.json() // JSON
const text = await response.text() // 文本
const blob = await response.blob() // Blob
const buffer = await response.arrayBuffer() // ArrayBuffer
const formData = await response.formData() // FormData
// 克隆响应(可多次读取)
const cloned = response.clone()
const json1 = await response.json()
const json2 = await cloned.json()

取消请求#

// 使用 AbortController
const controller = new AbortController()
fetch('/api/slow', {
signal: controller.signal,
})
.then((response) => response.json())
.catch((error) => {
if (error.name === 'AbortError') {
console.log('请求被取消')
} else {
console.error('请求失败:', error)
}
})
// 取消请求
setTimeout(() => {
controller.abort()
}, 3000)
// 超时封装
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
return fetch(url, {
...options,
signal: controller.signal,
}).finally(() => clearTimeout(timeoutId))
}

并发请求#

// Promise.all - 全部成功才成功
async function fetchAll() {
try {
const [users, posts] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
])
return { users, posts }
} catch (error) {
console.error('至少一个请求失败')
}
}
// Promise.allSettled - 获取所有结果
async function fetchAllSettled() {
const results = await Promise.allSettled([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
])
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${index} 成功:`, result.value)
} else {
console.log(`请求 ${index} 失败:`, result.reason)
}
})
}
// Promise.race - 返回最快的结果
async function fetchFastest() {
return Promise.race([
fetch('/api/server1').then((r) => r.json()),
fetch('/api/server2').then((r) => r.json()),
])
}

Fetch 封装#

基础封装#

class Http {
constructor(baseURL = '') {
this.baseURL = baseURL
this.defaultHeaders = {
'Content-Type': 'application/json',
}
}
async request(url, options = {}) {
const { headers = {}, ...rest } = options
const response = await fetch(this.baseURL + url, {
...rest,
headers: {
...this.defaultHeaders,
...headers,
},
})
if (!response.ok) {
const error = new Error(`HTTP ${response.status}`)
error.response = response
throw error
}
const contentType = response.headers.get('Content-Type')
if (contentType?.includes('application/json')) {
return response.json()
}
return response.text()
}
get(url, options) {
return this.request(url, { ...options, method: 'GET' })
}
post(url, data, options) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data),
})
}
put(url, data, options) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data),
})
}
delete(url, options) {
return this.request(url, { ...options, method: 'DELETE' })
}
}
// 使用
const http = new Http('/api')
const users = await http.get('/users')
await http.post('/users', { name: '张三' })

拦截器#

class Http {
constructor(baseURL = '') {
this.baseURL = baseURL
this.interceptors = {
request: [],
response: [],
}
}
useRequestInterceptor(fn) {
this.interceptors.request.push(fn)
}
useResponseInterceptor(onSuccess, onError) {
this.interceptors.response.push({ onSuccess, onError })
}
async request(url, options = {}) {
// 请求拦截
let config = { url: this.baseURL + url, ...options }
for (const interceptor of this.interceptors.request) {
config = await interceptor(config)
}
try {
let response = await fetch(config.url, config)
// 响应拦截(成功)
for (const { onSuccess } of this.interceptors.response) {
if (onSuccess) {
response = await onSuccess(response)
}
}
return response
} catch (error) {
// 响应拦截(错误)
for (const { onError } of this.interceptors.response) {
if (onError) {
error = await onError(error)
}
}
throw error
}
}
}
// 使用
const http = new Http('/api')
// 添加 token
http.useRequestInterceptor((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
}
}
return config
})
// 处理���应
http.useResponseInterceptor(
async (response) => {
if (!response.ok) {
if (response.status === 401) {
// 跳转登录
location.href = '/login'
}
throw new Error(`HTTP ${response.status}`)
}
return response.json()
},
(error) => {
console.error('请求失败:', error)
throw error
}
)

请求重试#

async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options)
if (response.ok) return response
throw new Error(`HTTP ${response.status}`)
} catch (error) {
if (i === retries - 1) throw error
// 指数退避
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, i)))
}
}
}
// 使用
fetchWithRetry('/api/unstable', {}, 3)
.then((r) => r.json())
.catch((e) => console.error('所有重试失败'))

实际应用#

RESTful API 客户端#

class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
}
const response = await fetch(url, config)
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.message || `HTTP ${response.status}`)
}
return response.json()
}
// CRUD 操作
getAll(resource) {
return this.request(`/${resource}`)
}
getOne(resource, id) {
return this.request(`/${resource}/${id}`)
}
create(resource, data) {
return this.request(`/${resource}`, {
method: 'POST',
body: JSON.stringify(data),
})
}
update(resource, id, data) {
return this.request(`/${resource}/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
delete(resource, id) {
return this.request(`/${resource}/${id}`, {
method: 'DELETE',
})
}
}
// 使用
const api = new ApiClient('https://api.example.com')
const users = await api.getAll('users')
const user = await api.getOne('users', 1)
await api.create('users', { name: '张三' })
await api.update('users', 1, { name: '李四' })
await api.delete('users', 1)

带缓存的请求#

const cache = new Map()
async function cachedFetch(url, options = {}, maxAge = 60000) {
const cacheKey = `${options.method || 'GET'}-${url}`
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < maxAge) {
return cached.data
}
const response = await fetch(url, options)
const data = await response.json()
cache.set(cacheKey, {
data,
timestamp: Date.now(),
})
return data
}
// 使用
const data = await cachedFetch('/api/config', {}, 300000) // 缓存 5 分钟

请求队列#

class RequestQueue {
constructor(concurrency = 3) {
this.concurrency = concurrency
this.queue = []
this.running = 0
}
add(promiseFn) {
return new Promise((resolve, reject) => {
this.queue.push({ promiseFn, resolve, reject })
this.run()
})
}
run() {
while (this.running < this.concurrency && this.queue.length) {
const { promiseFn, resolve, reject } = this.queue.shift()
this.running++
promiseFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--
this.run()
})
}
}
}
// 使用:限制并发请求数
const queue = new RequestQueue(3)
const urls = ['/api/1', '/api/2', '/api/3', '/api/4', '/api/5']
const results = await Promise.all(
urls.map((url) => queue.add(() => fetch(url).then((r) => r.json())))
)

总结#

特性XMLHttpRequestFetch
API 风格回调Promise
取消请求xhr.abort()AbortController
进度事件支持不支持
超时设置xhr.timeout需要封装
请求/响应拦截需要封装需要封装
Fetch 配置说明
method请求方法
headers请求头
body请求体
modeCORS 模式
credentials是否携带凭证
signal取消信号

核心要点