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 请求#
// 发送 JSONasync 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 })
// 发送 FormDataasync 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) // 200console.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() // JSONconst text = await response.text() // 文本const blob = await response.blob() // Blobconst buffer = await response.arrayBuffer() // ArrayBufferconst formData = await response.formData() // FormData
// 克隆响应(可多次读取)const cloned = response.clone()const json1 = await response.json()const json2 = await cloned.json()取消请求#
// 使用 AbortControllerconst 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')
// 添加 tokenhttp.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()))))总结#
| 特性 | XMLHttpRequest | Fetch |
|---|---|---|
| API 风格 | 回调 | Promise |
| 取消请求 | xhr.abort() | AbortController |
| 进度事件 | 支持 | 不支持 |
| 超时设置 | xhr.timeout | 需要封装 |
| 请求/响应拦截 | 需要封装 | 需要封装 |
| Fetch 配置 | 说明 |
|---|---|
method | 请求方法 |
headers | 请求头 |
body | 请求体 |
mode | CORS 模式 |
credentials | 是否携带凭证 |
signal | 取消信号 |
核心要点:
- Fetch 返回 Promise,更符合现代编程风格
- 检查 response.ok 判断请求是否成功
- 响应体只能读取一次,需要时先 clone()
- 使用 AbortController 取消请求
- 封装时注意添加超时、重试、拦截器等功能