XMLHttpRequest (XHR)
XMLHttpRequest 是最早用于在瀏覽器中進行異步網絡請求的 API。它允許網頁在不刷新整個頁面的情況下與服務器交換數據。
// 創建 XHR 對象
const xhr = new XMLHttpRequest();// 初始化請求
xhr.open('GET', 'https://api.example.com/data', true);// 設置請求頭(可選)
xhr.setRequestHeader('Content-Type', 'application/json');// 設置響應類型(可選)
xhr.responseType = 'json';// 監聽狀態變化
xhr.onreadystatechange = function() {if (xhr.readyState === 4) {if (xhr.status === 200) {// 請求成功console.log('Response:', xhr.response);} else {// 請求失敗console.error('Error:', xhr.status);}}
};// 可選:處理錯誤
xhr.onerror = function() {console.error('Network error occurred');
};// 發送請求
xhr.send();
- 優點:廣泛兼容、支持進度事件、功能全面
- 缺點:API 復雜、回調嵌套問題(回調地獄)、不支持 Promise
回調地獄問題
// 第一個請求
const xhr1 = new XMLHttpRequest();
xhr1.open('GET', '/api/data1', true);
xhr1.onreadystatechange = function() {if (xhr1.readyState === 4 && xhr1.status === 200) {const data1 = JSON.parse(xhr1.responseText);// 第二個請求(依賴第一個請求的結果)const xhr2 = new XMLHttpRequest();xhr2.open('GET', `/api/data2?param=${data1.id}`, true);xhr2.onreadystatechange = function() {if (xhr2.readyState === 4 && xhr2.status === 200) {const data2 = JSON.parse(xhr2.responseText);// 第三個請求(依賴第二個請求的結果)const xhr3 = new XMLHttpRequest();xhr3.open('POST', '/api/submit', true);xhr3.setRequestHeader('Content-Type', 'application/json');xhr3.onreadystatechange = function() {if (xhr3.readyState === 4 && xhr3.status === 200) {const result = JSON.parse(xhr3.responseText);console.log('最終結果:', result);}};xhr3.send(JSON.stringify({ data1, data2 }));}};xhr2.send();}
};
xhr1.send();
為什么不能按順序寫 XHR 請求?
在異步編程中,代碼的書寫順序和執行順序是兩回事。當你按順序寫 XHR 請求時,它們會立即開始執行,但不會等待前一個請求完成。這會導致嚴重問題:
// 錯誤示例:按順序寫但不嵌套
const xhr1 = new XMLHttpRequest();
xhr1.open('GET', '/api/data1', true);
xhr1.onreadystatechange = function() { /* 處理 data1 */ };
xhr1.send();const xhr2 = new XMLHttpRequest();
xhr2.open('GET', '/api/data2?param=???', true); // 這里需要 data1.id,但 data1 可能還沒返回
xhr2.onreadystatechange = function() { /* 處理 data2 */ };
xhr2.send();const xhr3 = new XMLHttpRequest();
xhr3.open('POST', '/api/submit', true);
xhr3.setRequestHeader('Content-Type', 'application/json');
xhr3.onreadystatechange = function() { /* 處理結果 */ };
xhr3.send(JSON.stringify({ data1, data2 })); // data1 和 data2 都可能不存在
為什么會出錯?
-
數據依賴問題:
-
第二個請求需要第一個請求的結果 (
data1.id
) -
第三個請求需要前兩個請求的結果 (
data1
?和?data2
)
-
-
執行順序不確定:
-
異步請求的完成時間不可預測
-
即使請求 1 先發送,請求 2 和 3 可能先完成
-
-
閉包問題:
-
回調函數中的變量會捕獲外部作用域
-
如果不嵌套,變量可能在回調執行時已被修改
-
嵌套的核心目的:確保執行順序
通過嵌套回調,你實際上是在告訴程序:
"只有當請求 1 成功完成后,才開始請求 2;只有當請求 2 成功完成后,才開始請求 3"
解決方案
? ??
Fetch API
Fetch API 是現代瀏覽器提供的用于替代 XHR 的新標準,它基于 Promise,提供了更簡潔、更強大的接口來處理網絡請求。
主要特點:
- 基于 Promise,避免回調地獄
- 支持 async/await 語法
- 提供 Request 和 Response 對象
- 支持跨域請求和 CORS
- 支持流式響應體處理
基本用法
// 簡單的 GET 請求
fetch('https://api.example.com/data').then(response => {if (!response.ok) {throw new Error(`HTTP error! Status: ${response.status}`);}return response.json(); // 解析為 JSON}).then(data => {console.log('Response:', data);}).catch(error => {console.error('Fetch error:', error);});// 使用 async/await
async function fetchData() {try {const response = await fetch('https://api.example.com/data');if (!response.ok) {throw new Error(`HTTP error! Status: ${response.status}`);}const data = await response.json();console.log('Response:', data);} catch (error) {console.error('Fetch error:', error);}
}// 帶參數的 POST 請求
fetch('https://api.example.com/submit', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ name: 'John', age: 30 }),
}).then(response => response.json()).then(data => console.log(data)).catch(error => console.error('Error:', error));
優缺點:
- 優點:現代 API、基于 Promise、更簡潔、支持流式處理
- 缺點:不支持進度事件、錯誤處理需要額外檢查狀態碼、舊瀏覽器兼容性差(需要 polyfill)
Fetch API 的局限性詳解
1. 不支持進度事件
Fetch API 的主要設計目標是提供一個現代、簡潔的網絡請求接口,但它沒有內置的進度事件支持。在上傳或下載大文件時,這是一個明顯的不足:
XHR 的進度支持:XHR 通過?onprogress
?事件提供上傳和下載進度:
xhr.upload.onprogress = function(event) {if (event.lengthComputable) {const percentComplete = (event.loaded / event.total) * 100;console.log(`上傳進度: ${percentComplete}%`);}
};
Fetch 的替代方案:Fetch 需要使用更復雜的?ReadableStream
?API 來實現進度:
fetch('large-file.zip').then(response => {const reader = response.body.getReader();const contentLength = response.headers.get('Content-Length');let receivedLength = 0;return new Response(new ReadableStream({async start(controller) {while (true) {const { done, value } = await reader.read();if (done) break;receivedLength += value.length;const percentComplete = (receivedLength / contentLength) * 100;console.log(`下載進度: ${percentComplete}%`);controller.enqueue(value);}controller.close();}}));});
2. 錯誤處理需要額外檢查狀態碼
Fetch API 的設計與傳統 HTTP 錯誤處理模式不同:
- Fetch 的錯誤處理機制:
- 只有網絡錯誤(如斷網、DNS 失敗)才會觸發?
reject
- HTTP 錯誤(如 404、500)不會觸發?
reject
,而是返回一個狀態碼非 2xx 的?Response
?對象
- 只有網絡錯誤(如斷網、DNS 失敗)才會觸發?
fetch('https://example.com/non-existent').then(response => {// 這里會執行,即使服務器返回 404if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return response.json();}).catch(error => {console.error('Fetch error:', error);});
XHR 的錯誤處理:XHR 的?onerror
?事件會捕獲網絡錯誤,而 HTTP 錯誤通過狀態碼判斷:
xhr.onerror = function() {console.error('網絡錯誤');
};xhr.onreadystatechange = function() {if (xhr.readyState === 4) {if (xhr.status >= 400) {console.error('HTTP 錯誤:', xhr.status);}}
};
3. 舊瀏覽器兼容性差(需要 polyfill)
AXIOS和fetch差別
Axios 是一個基于 Promise 的 HTTP 客戶端,專為瀏覽器和 Node.js 設計。它與原生 Fetch API 有許多區別,這些區別影響著開發者的選擇。
具體差異詳解
1 錯誤處理
Axios:HTTP 錯誤(404、500 等)會直接 reject Promise
axios.get('/api/data').then(response => {// 僅在 HTTP 狀態碼為 2xx 時執行console.log(response.data);}).catch(error => {// 處理所有錯誤(網絡錯誤和 HTTP 錯誤)console.error('請求失敗:', error.response?.status);});
Fetch:HTTP 錯誤不會 reject,需要手動檢查狀態碼
在 Fetch 請求的?then
?回調中,你需要檢查?response.ok
?屬性或?response.status
?狀態碼:
fetch('https://api.example.com/data').then(response => {// 檢查響應狀態if (!response.ok) {// 手動拋出錯誤,使 Promise 被 rejectthrow new Error(`HTTP error! status: ${response.status}`);}// 請求成功,繼續處理響應return response.json();}).then(data => console.log('數據:', data)).catch(error => console.error('請求失敗:', error));
2 數據格式處理
Axios:自動解析 JSON 響應
axios.get('/api/data').then(response => {// response.data 已經是解析后的 JSON 對象console.log(response.data.name);});
Fetch:需要手動解析響應
fetch('/api/data').then(response => response.json()) // 手動解析為 JSON.then(data => console.log(data.name));
3 請求取消
Axios:使用?CancelToken
const source = axios.CancelToken.source();axios.get('/api/data', {cancelToken: source.token
})
.then(response => console.log(response))
.catch(thrown => {if (axios.isCancel(thrown)) {console.log('請求被取消:', thrown.message);}
});// 取消請求
source.cancel('用戶取消了請求');
Fetch:使用?AbortController
(需要現代瀏覽器支持)
const controller = new AbortController();
const signal = controller.signal;fetch('/api/data', { signal }).then(response => response.json()).then(data => console.log(data)).catch(error => {if (error.name === 'AbortError') {console.log('請求被取消');}});// 取消請求
controller.abort();
4 進度事件
Axios:內置支持上傳和下載進度
// 下載進度
axios.get('/large-file', {onDownloadProgress: progressEvent => {const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);console.log(`下載進度: ${percentCompleted}%`);}
});// 上傳進度
axios.post('/upload', formData, {onUploadProgress: progressEvent => {const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);console.log(`上傳進度: ${percentCompleted}%`);}
});
Fetch:需要使用 ReadableStream 手動實現
fetch('/large-file').then(response => {const contentLength = response.headers.get('Content-Length');const reader = response.body.getReader();let receivedLength = 0;return new Response(new ReadableStream({async start(controller) {while (true) {const { done, value } = await reader.read();if (done) break;receivedLength += value.length;const percentComplete = (receivedLength / contentLength) * 100;console.log(`下載進度: ${percentComplete}%`);controller.enqueue(value);}controller.close();}}));});
5 攔截器
Axios:支持請求和響應攔截器,便于統一處理
// 添加請求攔截器
axios.interceptors.request.use(config => {// 在發送請求之前做些什么config.headers.Authorization = `Bearer ${token}`;return config;},error => {// 對請求錯誤做些什么return Promise.reject(error);}
);// 添加響應攔截器
axios.interceptors.response.use(response => {// 對響應數據做點什么return response;},error => {// 對響應錯誤做點什么if (error.response.status === 401) {// 處理未授權情況}return Promise.reject(error);}
);
Fetch:不直接支持攔截器,需要手動實現
function fetchWithInterceptor(url, options) {// 請求攔截const modifiedOptions = {...options,headers: {...options.headers,Authorization: `Bearer ${token}`}};return fetch(url, modifiedOptions).then(response => {// 響應攔截if (response.status === 401) {// 處理未授權情況}return response;});
}