Ajax入門以及Axios的詳細使用(含Promise)

iRuriCatt發表於2024-12-30

1. 概述

1.1 是什麼

  • Ajax = Asynchronous JavaScript and XML(非同步的 JavaScript 和 XML)

  • Ajax 不是新的程式語言,而是一種用於建立快速動態網頁的技術

  • Ajax 最大的優點是在不重新載入整個頁面的情況下,可以與伺服器交換資料並更新部分網頁內容,使網頁實現非同步更新

  • 傳統的網頁(不使用 Ajax)如果需要更新內容,必需過載整個網頁

  • Ajax 不需要任何瀏覽器外掛,但需要使用者允許 JavaScript 在瀏覽器上執行

  • XMLHttpRequest只是實現 Ajax 的一種方式

1.2 為什麼

  • 以前資料都是寫在程式碼裡固定的, 無法隨時變化

  • 現在資料可以從伺服器上進行獲取,讓資料變活

1.3 入門程式

  • 需求:從伺服器獲取省份列表資料,展示到頁面上

  • 步驟:

    • 引入 axios
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    
    • 基本語法
    axios({
    	url: "目標資源地址",
    }).then(result => {
    	// 對伺服器返回的資料做後續處理
    });
    
  • 示例

<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	axios({
		url: "http://hmajax.itheima.net/api/province",
	}).then(result => {
		document.querySelector("#root").innerHTML = result.data.list.join("<br>");
	});
</script>

2. axios

2.1 URL

  • URL:統一資源定位符,簡稱網址,用於定位網路中的資源(網頁,圖片,資料,影片,音訊等)

  • 組成:協議,域名,資源路徑(比較重要的三部分)

  • http 協議:超文字傳輸協議,規定了瀏覽器和伺服器傳遞資料的格式

  • 域名:標記伺服器在網際網路當中的方位,網路中有很多伺服器,你想訪問哪一臺,需要知道它的域名

  • 資源路徑:一個伺服器內有多個資源,用於標識你要訪問的資源具體的位置

Ajax入門以及Axios的詳細使用(含Promise)
  • 查詢引數:攜帶給伺服器額外資訊,讓伺服器返回想要的某一部分資料而不是全部資料

    • 格式:http://xxxx.com/xxx/xxx?引數名1=值1&引數名2=值2

    • 引數名一般是後端規定的,值前端看情況傳遞即可

  • axios 如何攜帶查詢引數?

axios({
	url: "目標資源地址",
	params: {
		引數名: 值,
	},
}).then(result => {
	// 對伺服器返回的資料做後續處理
});
  • 示例 1:獲取“河北省”下屬的城市列表
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	axios({
		url: "http://hmajax.itheima.net/api/city",
		params: {
			pname: "河北省",
		},
	}).then(result => {
		document.querySelector("#root").innerHTML = result.data.list.join("<br>");
	});
</script>
  • 示例 2:根據輸入的省份名字和城市名字,查詢下屬地區列表

<!-- 樣式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />
<style>
	#root {
		font-size: 15px;
	}

	body {
		padding-top: 15px;
	}
</style>

<div class="container">
	<form id="editForm" class="row">
		<!-- 輸入省份名字 -->
		<div class="mb-3 col">
			<label class="form-label">省份名字</label>
			<input type="text" value="北京" name="province" class="form-control province" placeholder="請輸入省份名稱" />
		</div>
		<!-- 輸入城市名字 -->
		<div class="mb-3 col">
			<label class="form-label">城市名字</label>
			<input type="text" value="北京市" name="city" class="form-control city" placeholder="請輸入城市名稱" />
		</div>
	</form>
	<button type="button" class="btn btn-primary sel-btn">查詢</button>
	<br />
	<br />
	<p>地區列表:</p>
	<ul class="list-group">
		<!-- 示例地區 -->
		<!-- <li class="list-group-item">東城區</li> -->
	</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	/*
      獲取地區列表: http://hmajax.itheima.net/api/area
      查詢引數:
        pname: 省份或直轄市名字
        cname: 城市名字
    */
	// 繫結點選事件
	document.querySelector(".sel-btn").addEventListener("click", () => {
		// 獲取輸入框的值
		let pName = document.querySelector(".province").value;
		let cName = document.querySelector(".city").value;

		// 利用axios獲取資料
		axios({
			url: "http://hmajax.itheima.net/api/area",
			params: {
				pname: pName,
				cname: cName,
			},
		}).then(result => {
			document.querySelector(".list-group").innerHTML = result.data.list
				.map(item => `<li class="list-group-item">${item}</li>`)
				.join("");
		});
	});
</script>

2.2 資料提交

  • 常用請求方法
請求方法 操作
GET 獲取資料(預設請求方式)
POST 提交資料
PUT 修改資料(全部)
DELETE 刪除資料
PATCH 修改資料(部分)
  • axios 如何提交資料到伺服器
axios({
	url: "目標資源地址",
	method: "請求方法",
	data: {
		引數名: 值,
	},
}).then(result => {
	// 對伺服器返回的資料做後續處理
});
  • 示例:註冊賬號,提交使用者名稱和密碼到伺服器儲存
<button>點選註冊</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	document.querySelector("button").addEventListener("click", () => {
		axios({
			url: "http://hmajax.itheima.net/api/register",
			method: "post",
			data: {
				username: "itheima666",
				password: "12345678",
			},
		}).then(result => {
			console.log(result);
		});
	});
</script>
Ajax入門以及Axios的詳細使用(含Promise)

2.3 axios 錯誤處理

axios({
	// ...請求選項
})
	.then(result => {
		// 處理成功資料
	})
	.catch(error => {
		// 處理失敗錯誤
	});
  • 示例:
<button>點選註冊</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	document.querySelector("button").addEventListener("click", () => {
		axios({
			url: "http://hmajax.itheima.net/api/register",
			method: "post",
			data: {
				username: "itheima666",
				password: "12345678",
			},
		})
			.then(result => {
				console.log(result);
			})
			.catch(error => {
				alert(error.response.data.message);
			});
	});
</script>

2.4 案例-使用者登入

  • 樣式
<!-- 引入bootstrap.css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" />
<!-- 公共 -->
<style>
	html,
	body {
		background-color: #edf0f5;
		width: 100%;
		height: 100%;
		display: flex;
		justify-content: center;
		align-items: center;
	}

	.container {
		width: 520px;
		height: 540px;
		background-color: #fff;
		padding: 60px;
		box-sizing: border-box;
	}

	.container h3 {
		font-weight: 900;
	}
</style>
<!-- 表單容器和內容 -->
<style>
	.form_wrap {
		color: #8b929d !important;
	}

	.form-text {
		color: #8b929d !important;
	}
</style>
<!-- 提示框樣式 -->
<style>
	.alert {
		transition: 0.5s;
		opacity: 0;
	}

	.alert.show {
		opacity: 1;
	}
</style>
  • 框架
<div class="container">
	<h3>歡迎-登入</h3>
	<!-- 登入結果-提示框 -->
	<div class="alert alert-success" role="alert">
		<!-- 提示訊息 -->
	</div>
	<!-- 表單 -->
	<div class="form_wrap">
		<form>
			<div class="mb-3">
				<label for="username" class="form-label">賬號名</label>
				<input type="text" class="form-control username" />
			</div>
			<div class="mb-3">
				<label for="password" class="form-label">密碼</label>
				<input type="password" class="form-control password" />
			</div>
			<button type="button" class="btn btn-primary btn-login">登 錄</button>
		</form>
	</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	// 目標1:點選登入時,使用者名稱和密碼長度判斷,並提交資料和伺服器通訊
	function alertFn(msg, isSuccess) {
		// 顯示提示框
		let myAlert = document.querySelector(".alert");
		myAlert.classList.add("show");
		// 更換樣式
		let bgc = isSuccess ? "alert-success" : "alert-danger";
		myAlert.classList.add(bgc);
		// 列印資訊
		myAlert.innerText = msg;
		// 2s後自動消失
		setTimeout(() => {
			myAlert.classList.remove("show");
			// 重置背景色,避免類名衝突
			myAlert.classList.remove(bgc);
		}, 2000);
	}
	document.querySelector(".btn-login").addEventListener("click", () => {
		let username = document.querySelector(".username").value;
		let password = document.querySelector(".password").value;

		if (username.length < 8) {
			// console.log('使用者名稱不能少於8個字元')
			alertFn("使用者名稱不能少於8個字元", false);
			return;
		}
		if (password < 6) {
			// console.log('密碼不能少於6個字元')
			alertFn("密碼不能少於6個字元", false);
			return;
		}

		axios({
			url: "http://hmajax.itheima.net/api/login",
			method: "post",
			data: {
				username,
				password,
			},
		})
			.then(result => {
				// alert(result.data.message)
				alertFn(result.data.message, true);
			})
			.catch(error => {
				// alert(error.response.data.message)
				alertFn(error.response.data.message, false);
			});
	});
</script>

2.5 form-serialize 外掛

快速收集目標表單範圍內表單元素的值

  • 引入 form-serialize 外掛

  • 使用 serialize 函式

    • 引數 1:要獲取的 form 表單標籤物件(要求表單元素有 name 屬性,用來作為收集的資料中屬性名)

    • 引數 2:配置物件

      • hash:

        • true - 收集出來的是一個 JS 物件

        • false - 收集出來的是一個查詢字串

      • empty:

        • true - 收集空值

        • false - 不收集空值

  • 示例:收集登入表單裡使用者名稱和密碼

<form action="javascript:;" class="example-form">
	<input type="text" name="uname" />
	<br />
	<input type="text" name="pwd" />
	<br />
	<input type="button" class="btn" value="提交" />
</form>
<!-- 
目標:在點選提交時,使用form-serialize外掛,快速收集表單元素值
-->
<script src="./form-serialize.js"></script>
<script>
	document.querySelector(".btn").addEventListener("click", () => {
		const form = document.querySelector(".example-form");
		const data = serialize(form, { hash: true, empty: true });
		console.log(data);
	});
</script>

2.6 Bootstrap 彈框

2.6.1 屬性控制

  • 引入 bootstrap.css 和 bootstrap.js

  • 準備彈框標籤,確認結構(可以從 Bootstrap 官方文件的 Modal 裡複製基礎例子)- 執行到網頁後,逐一對應標籤和彈框每個部分對應關係

  • 透過自定義屬性,通知彈框的顯示和隱藏,語法如下:

<button data-bs-toggle="modal" data-bs-target="css選擇器">顯示彈框</button>

<button data-bs-dismiss="modal">Close</button>
  • 程式碼實現
<!-- 
目標:使用Bootstrap彈框
1. 引入bootstrap.css和bootstrap.js
2. 準備彈框標籤,確認結構
3. 透過自定義屬性,控制彈框的顯示和隱藏
-->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target=".mybox">顯示彈框</button>

<div class="modal mybox" tabindex="-1">
	<div class="modal-dialog">
		<div class="modal-content">
			<div class="modal-header">
				<h5 class="modal-title">Modal title</h5>
				<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
			</div>
			<div class="modal-body">
				<p>Modal body text goes here.</p>
			</div>
			<div class="modal-footer">
				<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
				<button type="button" class="btn btn-primary">Save changes</button>
			</div>
		</div>
	</div>
</div>

2.6.2 js 控制

  • 為什麼需要 js 方式控制?

    • 當顯示/隱藏之前,需要執行一些 JS 邏輯程式碼,就需要引入 JS 控制彈框顯示/隱藏的方式
  • 例如:

    • 點選編輯姓名按鈕,在彈框顯示之前,在輸入框填入預設姓名

    • 點選儲存按鈕,在彈框隱藏之前,獲取使用者填入的名字並列印

Ajax入門以及Axios的詳細使用(含Promise)
  • 語法
// 建立彈框物件
const modalDom = document.querySelector("css選擇器");
const modal = new bootstrap.Modal(modelDom);

// 顯示彈框
modal.show();
// 隱藏彈框
modal.hide();
  • 示例
<!-- 
目標:使用JS控制彈框,顯示和隱藏
1. 建立彈框物件
2. 呼叫彈框物件內建方法
.show() 顯示
.hide() 隱藏
-->
<button type="button" class="btn btn-primary edit-btn">編輯姓名</button>

<div class="modal name-box" tabindex="-1">
	<div class="modal-dialog">
		<div class="modal-content">
			<div class="modal-header">
				<h5 class="modal-title">請輸入姓名</h5>
				<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
			</div>
			<div class="modal-body">
				<form action="">
					<span>姓名:</span>
					<input type="text" class="username" />
				</form>
			</div>
			<div class="modal-footer">
				<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
				<button type="button" class="btn btn-primary save-btn">儲存</button>
			</div>
		</div>
	</div>
</div>

<!-- 引入bootstrap.js -->
<script src="./bootstrap.min.js"></script>
<script>
	const modalBox = document.querySelector(".name-box");
	const modal = new bootstrap.Modal(modalBox);

	document.querySelector(".edit-btn").addEventListener("click", () => {
		document.querySelector(".username").value = "預設姓名";
		modal.show();
	});

	document.querySelector(".save-btn").addEventListener("click", () => {
		const username = document.querySelector(".username").value;
		console.log("將資料提交到伺服器", username);
		modal.hide();
	});
</script>

2.7 案例-圖書管理

  • 黑馬介面文件

  • 結構

<!-- 主體區域 -->
<div class="container">
	<!-- 頭部標題和新增按鈕 -->
	<div class="top">
		<h3>圖書管理</h3>
		<button type="button" class="btn btn-primary plus-btn" data-bs-toggle="modal" data-bs-target=".add-modal">
			+ 新增
		</button>
	</div>
	<!-- 資料列表 -->
	<table class="table">
		<thead class="table-light">
			<tr>
				<th style="width: 150px;">序號</th>
				<th>書名</th>
				<th>作者</th>
				<th>出版社</th>
				<th style="width: 180px;">操作</th>
			</tr>
		</thead>
		<tbody class="list">
			<!-- <tr>
					<td>1</td>
					<td>JavaScript程式設計</td>
					<td>馬特·弗里斯比</td>
					<td>人民郵電出版社</td>
					<td>
						<span class="del">刪除</span>
						<span class="edit">編輯</span>
					</td>
			</tr> -->
		</tbody>
	</table>
</div>
<!-- 新增-彈出框 -->
<div class="modal fade add-modal">
	<!-- 中間白色區域 -->
	<div class="modal-dialog">
		<div class="modal-content">
			<div class="modal-header top">
				<span>新增圖書</span>
				<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button>
			</div>
			<div class="modal-body form-wrap">
				<!-- 新增表單 -->
				<form class="add-form">
					<div class="mb-3">
						<label for="bookname" class="form-label">書名</label>
						<input type="text" class="form-control bookname" placeholder="請輸入書籍名稱" name="bookname" />
					</div>
					<div class="mb-3">
						<label for="author" class="form-label">作者</label>
						<input type="text" class="form-control author" placeholder="請輸入作者名稱" name="author" />
					</div>
					<div class="mb-3">
						<label for="publisher" class="form-label">出版社</label>
						<input type="text" class="form-control publisher" placeholder="請輸入出版社名稱" name="publisher" />
					</div>
				</form>
			</div>
			<div class="modal-footer btn-group">
				<button type="button" class="btn btn-primary" data-bs-dismiss="modal">取消</button>
				<button type="button" class="btn btn-primary add-btn">儲存</button>
			</div>
		</div>
	</div>
</div>
<!-- 編輯-彈出框 -->
<div class="modal fade edit-modal">
	<!-- 中間白色區域 -->
	<div class="modal-dialog">
		<div class="modal-content">
			<div class="modal-header top">
				<span>編輯圖書</span>
				<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button>
			</div>
			<div class="modal-body form-wrap">
				<!-- 編輯表單 -->
				<form class="edit-form">
					<!-- 儲存正在編輯的圖書id,隱藏起來:無需讓使用者修改 -->
					<input type="hidden" class="id" name="id" />
					<div class="mb-3">
						<label for="bookname" class="form-label">書名</label>
						<input type="text" class="form-control bookname" placeholder="請輸入書籍名稱" name="bookname" />
					</div>
					<div class="mb-3">
						<label for="author" class="form-label">作者</label>
						<input type="text" class="form-control author" placeholder="請輸入作者名稱" name="author" />
					</div>
					<div class="mb-3">
						<label for="publisher" class="form-label">出版社</label>
						<input type="text" class="form-control publisher" placeholder="請輸入出版社名稱" name="publisher" />
					</div>
				</form>
			</div>
			<div class="modal-footer btn-group">
				<button type="button" class="btn btn-primary" data-bs-dismiss="modal">取消</button>
				<button type="button" class="btn btn-primary edit-btn">修改</button>
			</div>
		</div>
	</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script>
<script src="./lib/form-serialize.js"></script>
<script src="./lib/bootstrap.min.js"></script>
<!-- 核心邏輯 -->
<script src="./js/index.js"></script>
  • index.js

  • 渲染圖書列表

/**
 * 目標1:渲染圖書列表
 *  1.1 獲取資料
 *  1.2 渲染資料
 */

const creator = "老李";

function getBooksList() {
	// 1.1 獲取資料
	axios({
		url: "http://hmajax.itheima.net/api/books",
		params: {
			creator,
		},
	}).then(result => {
		// console.log(result.data.data)
		// 1.2 渲染資料
		const list = result.data.data
			.map((item, index) => {
				return `
                <tr>
                    <td>${index + 1}</td>
                    <td>${item.bookname}</td>
                    <td>${item.author}</td>
                    <td>${item.publisher}</td>
                    <td data-id=${item.id}>
                        <span class="del">刪除</span>
                        <span class="edit">編輯</span>
                    </td>
                </tr>
            `;
			})
			.join("");
		document.querySelector(".list").innerHTML = list;
	});
}

getBooksList();
  • 新增圖書
/**
 * 目標2:新增圖書
 *  2.1新增彈框->顯示和隱藏
 *  2.2收集表單資料,並提交到伺服器儲存
 *  2.3重新整理圖書列表
 */
const addModalDom = document.querySelector(".add-modal");
const addModal = new bootstrap.Modal(addModalDom);

// 2.1 新增彈框->顯示和隱藏
document.querySelector(".add-btn").addEventListener("click", () => {
	// 2.2 獲取輸入框的資料
	const addForm = document.querySelector(".add-form");
	const formData = serialize(addForm, { hash: true, empty: true });
	console.log(formData);

	// 2.3 提交到伺服器儲存
	axios({
		url: "http://hmajax.itheima.net/api/books",
		method: "post",
		data: {
			...formData,
			creator,
		},
	}).then(result => {
		console.log(result);
		// 2.4 重新渲染頁面
		getBooksList();
		// 重置表單
		addForm.reset();
		// 點選儲存,隱藏模態框
		addModal.hide();
	});
});
  • 刪除圖書
/**
 * 目標3:刪除圖書
 *  3.1刪除元素繫結點選事件->獲取圖書id
 *  3.2呼叫刪除介面
 *  3.3重新整理圖書列表
 */
// 3.1 刪除元素繫結點選事件(事件委託)->獲取圖書id
document.querySelector(".list").addEventListener("click", e => {
	if (e.target.classList.contains("del")) {
		const theId = e.target.parentNode.dataset.id;

		// 3.2 呼叫刪除介面
		axios({
			url: `http://hmajax.itheima.net/api/books/${theId}`,
			method: "delete",
		}).then(() => {
			// 3.3 重新整理圖書列表
			getBooksList();
		});
	}
});
  • 編輯圖書
/**
 * 目標4:編輯圖書
 *  4.1編輯彈框->顯示和隱藏
 *  4.2獲取當前編輯圖書資料->回顯到編輯表單中
 *  4.3提交儲存修改,並重新整理列表
 */
const editModalDom = document.querySelector(".edit-modal");
const editModal = new bootstrap.Modal(editModalDom);

document.querySelector(".list").addEventListener("click", e => {
	if (e.target.classList.contains("edit")) {
		const theId = e.target.parentNode.dataset.id;
		// 4.2 獲取當前編輯圖書資料->回顯到編輯表單中
		axios({
			url: `http://hmajax.itheima.net/api/books/${theId}`,
			method: "get",
		}).then(result => {
			// 資料物件"屬性"和標籤"類名"一致
			// 遍歷資料物件,使用屬性去獲取對應的標籤,快速賦值
			const bookObj = result.data.data;
			// console.log(bookObj)
			const keys = Object.keys(bookObj);
			keys.forEach(key => {
				document.querySelector(`.edit-form .${key}`).value = bookObj[key];
			});
		});
		// 4.1 編輯彈框->顯示和隱藏
		editModal.show();
	}
});

document.querySelector(".edit-btn").addEventListener("click", () => {
	const editForm = document.querySelector(".edit-form");
	const editData = serialize(editForm, { hash: true, empty: true });

	// 4.3 提交儲存修改,並重新整理列表
	axios({
		url: `http://hmajax.itheima.net/api/books/${editData.id}`,
		method: "put",
		data: {
			...editData,
			creator,
		},
	}).then(() => {
		getBooksList();
	});
	editModal.hide();
});
  • 總結

    • 渲染列表(查)

      • 獲取資料

      • 渲染資料

    • 新增圖書(增)

      • 彈框(顯示/隱藏)

      • 收集資料&提交儲存

      • 重新整理頁面列表

    • 刪除圖書(刪)

      • 繫結點選事件(獲取要刪除的圖書 id)

      • 呼叫刪除介面(讓伺服器刪除此資料)

      • 成功後重新獲取並重新整理列表

    • 編輯圖書(改)

      • 彈框(顯示/隱藏)

      • 表單(資料回顯)

      • 點選修改收集資料,提交到伺服器儲存

      • 重新獲取並重新整理列表

2.8 圖片上傳

把本地的圖片上傳到網頁上顯示

  • 實現步驟

    • 獲取圖片檔案物件:e.target.files[0]

    • 使用 FormData 表單資料物件裝入

      const fd = new FormData();
      fd.append(引數名, 值);
      
    • 提交表單資料物件,使用伺服器返回圖片 url 網址

<!-- 檔案選擇元素 -->
<input type="file" class="upload" />
<img src="" alt="" />

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	document.querySelector(".upload").addEventListener("change", e => {
		// 1. 獲取圖片檔案
		// console.log(e.target.files[0])

		// 2. 使用 FormData 攜帶圖片檔案
		const fd = new FormData();
		fd.append("img", e.target.files[0]);

		// 3. 提交到伺服器,獲取圖片url網址使用
		axios({
			url: "http://hmajax.itheima.net/api/uploadimg",
			method: "post",
			data: fd,
		}).then(result => {
			// console.log(result)
			document.querySelector("img").src = result.data.data.url;
		});
	});
</script>
  • 示例:更換背景圖片
<div class="container">
	<div class="nav">
		<div class="left">
			<ul>
				<li><a href="http://yun.itheima.com/?webzly" target="_blank" rel="nofollow">免費教程</a></li>
				<li><a href="http://resource.ityxb.com/booklist/?webzly" target="_blank" rel="nofollow">原創書籍</a></li>
				<li>
					<a href="http://www.itheima.com/teacher.html?webzly#ajavaee" target="_blank" rel="nofollow">教研團隊</a>
				</li>
				<li>
					<a href="http://www.itheima.com/special/hmschool/index.shtml?webzly" target="_blank" rel="nofollow">
						校區彙總
					</a>
				</li>
				<li><a href="http://www.itheima.com/flow/flow.html?webzly" target="_blank" rel="nofollow">報名流程</a></li>
				<li><a href="https://pip.itcast.cn?hmgw$webzly" target="_blank" rel="nofollow">專案資訊站</a></li>
				<li><a href="http://bbs.itheima.com/forum.php?webzly" target="_blank" rel="nofollow">技術社群</a></li>
			</ul>
		</div>
		<div class="right">
			<label for="bg">更換背景</label>
			<input class="bg-ipt" type="file" id="bg" />
		</div>
	</div>
	<div class="search-container">
		<img src="https://www.itheima.com/images/logo.png" alt="" />
		<div class="search-box">
			<input type="text" />
			<button>搜尋一下</button>
		</div>
	</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- 核心程式碼 -->
<script src="./js/index.js"></script>
  • index.js
document.querySelector(".bg-ipt").addEventListener("change", e => {
	// 1. 選擇圖片上傳,設定body背景
	const fd = new FormData();
	fd.append("img", e.target.files[0]);

	axios({
		url: "http://hmajax.itheima.net/api/uploadimg",
		method: "post",
		data: fd,
	}).then(result => {
		// console.log(result)
		const imgUrl = result.data.data.url;
		document.body.style.backgroundImage = `url(${imgUrl})`;

		// 2. 上傳成功時,"儲存"圖片url網址
		localStorage.setItem("bgImg", imgUrl);
	});
});

// 3. 網頁執行後,獲取url網址使用
const imgUrl = localStorage.getItem("bgImg");
document.body.style.backgroundImage = `url(${imgUrl})`;

2.9 案例-個人資訊設定

  • 結構
<!-- toast 提示框 -->
<div class="toast my-toast" data-bs-delay="1500">
    <div class="toast-body">
        <div class="alert alert-success info-box">
            操作成功
        </div>
    </div>
</div>
<!-- 核心內容區域 -->
<div class="container">
    <ul class="my-nav">
        <li class="active">基本設定</li>
        <li>安全設定</li>
        <li>賬號繫結</li>
        <li>新訊息通知</li>
    </ul>
    <div class="content">
        <div class="info-wrap">
            <h3 class="title">基本設定</h3>
            <form class="user-form" action="javascript:;">
                <div class="form-item">
                    <label for="email">郵箱</label>
                    <input id="email" name="email" class="email" type="text" placeholder="請輸入郵箱" autocomplete="off">
                </div>
                <div class="form-item">
                    <label for="nickname">暱稱</label>
                    <input id="nickname" name="nickname" class="nickname" type="text" placeholder="請輸入暱稱" autocomplete="off">
                </div>
                <div class="form-item">
                    <label>性別</label>
                    <label class="male-label"><input type="radio" name="gender" class="gender" value="0">男</label>
                    <label class="male-label"><input type="radio" name="gender" class="gender" value="1">女</label>
                </div>
                <div class="form-item">
                    <label for="desc">個人簡介</label>
                    <textarea id="desc" name="desc" class="desc" placeholder="請輸入個人簡介" cols="20" rows="10" autocomplete="off"></textarea>
                </div>
                <button class="submit">提交</button>
            </form>
        </div>
        <div class="avatar-box">
            <h4 class="avatar-title">頭像</h3>
            <img class="prew" src="./img/頭像.png" alt="">
            <label for="upload">更換頭像</label>
            <input id="upload" type="file" class="upload">
        </div>

    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
<script src="./lib/form-serialize.js"></script>
<!-- 核心邏輯 -->
<script src="./js/index.js"></script>
  • index.js

  • 資訊渲染

/**
 * 目標1:資訊渲染
 *  1.1 獲取使用者的資料
 *  1.2 回顯資料到標籤上
 * */

const creator = "小明";
axios({
	url: "http://hmajax.itheima.net/api/settings",
	params: {
		creator,
	},
}).then(result => {
	// console.log(result.data.data)
	const userObj = result.data.data;

	const keys = Object.keys(userObj);
	keys.forEach(key => {
		// 頭像和性別比較特殊,需要單獨處理
		if (key === "avatar") {
			document.querySelector(".prew").src = userObj[key];
		} else if (key === "gender") {
			const gRadioList = document.querySelectorAll(".gender");
			// 0男,1女,剛好與陣列下標對應
			const gNum = userObj[key];
			gRadioList[gNum].checked = true;
		} else {
			document.querySelector(`.${key}`).value = userObj[key];
		}
	});
});
  • 修改頭像
/**
 * 目標2:修改頭像
 *  2.1 獲取頭像檔案
 *  2.2 提交伺服器並更新頭像
 */

document.querySelector(".upload").addEventListener("change", e => {
	const fd = new FormData();
	fd.append("img", e.target.files[0]);
	fd.append("creator", creator);

	axios({
		url: "http://hmajax.itheima.net/api/avatar",
		method: "put",
		data: fd,
	}).then(result => {
		document.querySelector(".prew").src = result.data.data.avatar;
	});
});
  • 修改資料,並提示
/**
 * 目標3:提交表單
 *  3.1 收集表單資訊
 *  3.2 交到伺服器儲存
 */

/**
 * 目標4:結果提示
 *  4.1 建立toast物件
 *  4.2 呼叫show方法->顯示提示框
 */

document.querySelector(".submit").addEventListener("click", () => {
	// 3.1 收集表單資訊
	const form = document.querySelector(".user-form");
	const formData = serialize(form, { hash: true, empty: true });
	formData.creator = creator;
	formData.gender = +formData.gender;

	// 3.2 提交到伺服器儲存
	axios({
		url: "http://hmajax.itheima.net/api/settings",
		method: "put",
		data: formData,
	}).then(result => {
		// 4.1 建立toast物件
		const toastDom = document.querySelector(".my-toast");
		const toast = new bootstrap.Toast(toastDom);

		// 4.2 呼叫show方法->顯示提示框
		toast.show();
	});
});

2.10 請求方式別名

  • 為了方便起見,已經為所有支援的請求方法提供了別名
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
  • 注:在使用別名方法時, urlmethoddata 這些屬性都不必在配置中指定

2.11 Axios API

2.11.1 axios 例項

可以使用自定義配置新建一個例項

const instance = axios.create({
	baseURL: "https://some-domain.com/api/",
	timeout: 1000,
	headers: { "X-Custom-Header": "foobar" },
});
  • 一些例項方法

    • axios#request(config)

    • axios#get(url[, config])

    • axios#delete(url[, config])

    • axios#head(url[, config])

    • axios#options(url[, config])

    • axios#post(url[, data[, config]])

    • axios#put(url[, data[, config]])

    • axios#patch(url[, data[, config]])

    • axios#getUri([config])

2.11.2 請求配置

  • 這些是建立請求時可以用的配置選項,只有url是必需的

  • 若沒指定method,請求預設使用GET

{
  // url:用於請求的伺服器URL
  url: '/user',

  // method:建立請求時使用的方法,預設為get
  method: 'get',

  // baseURL:自動加在url前面,透過它可以傳遞相對地址
  baseURL: 'https://some-domain.com/api/',

  // transformRequest:允許在向伺服器傳送前,修改請求資料
  // 它只能用於'PUT', 'POST'和'PATCH'這幾個請求方法
  // 陣列中最後一個函式必須返回一個字串, 一個Buffer例項,ArrayBuffer,FormData或Stream
  // 可以修改請求頭
  transformRequest: [function (data, headers) {
    // 對傳送的 data 進行任意轉換處理

    return data;
  }],

  // transformResponse:在傳遞給then/catch前,允許修改響應資料
  transformResponse: [function (data) {
    // 對接收的 data 進行任意轉換處理

    return data;
  }],

  // 自定義請求頭
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // params:與請求一起傳送的URL引數
  // 必須是一個簡單物件或URLSearchParams物件
  params: {
    ID: 12345
  },

  // data:作為請求體被髮送的資料
  // 僅適用'PUT', 'POST', 'DELETE'和'PATCH'請求方法
  // 在沒有設定`transformRequest`時,則必須是以下型別之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 瀏覽器專屬: FormData, File, Blob
  // - Node 專屬: Stream, Buffer
  data: {
    firstName: 'Fred'
  },

  // 傳送請求體資料的可選語法
  // 請求方式 post
  // 只有 value 會被髮送,key 則不會
  data: 'Country=Brasil&City=Belo Horizonte',

  // timeout:指定請求超時的毫秒數
  // 如果請求時間超過`timeout`的值,則請求會被中斷,預設為0,表示永不超時
  timeout: 1000,

  // withCredentials:表示跨域請求時是否需要使用憑證,預設false
  withCredentials: false,

  // adapter:允許自定義處理請求,這使測試更加容易
  // 返回一個`promise`並提供一個有效的響應(參見 lib/adapters/README.md)
  adapter: function (config) {
    /* ... */
  },

  // auth:HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // responseType:瀏覽器將要響應的資料型別
  // 選項包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 瀏覽器專屬:'blob'
  responseType: 'json', // 預設值

  // responseEncoding:用於解碼響應的編碼 (Node.js專屬)
  // 注意:忽略`responseType`的值為 'stream',或者是客戶端請求
  responseEncoding: 'utf8', // 預設值

  // `xsrfCookieName`是`xsrf token`的值,被用作`cookie`的名稱
  xsrfCookieName: 'XSRF-TOKEN', // 預設值

  // `xsrfHeaderName`是帶有`xsrf token`值的http請求頭名稱
  xsrfHeaderName: 'X-XSRF-TOKEN', // 預設值

  // `onUploadProgress`允許為上傳處理進度事件(瀏覽器專屬)
  onUploadProgress: function (progressEvent) {
    // 處理原生進度事件
  },

  // `onDownloadProgress`允許為下載處理進度事件(瀏覽器專屬)
  onDownloadProgress: function (progressEvent) {
    // 處理原生進度事件
  },

  // `maxContentLength`定義了node.js中允許的HTTP響應內容的最大位元組數
  maxContentLength: 2000,

  // `maxBodyLength`(僅Node)定義允許的http請求內容的最大位元組數
  maxBodyLength: 2000,

  // `validateStatus`定義了對於給定的HTTP狀態碼是 resolve 還是 reject promise
  // 返回`true`(或者設定為`null`或`undefined`),則promise將會resolved,否則是rejected
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 預設值
  },

  // maxRedirects:定義了在node.js中要遵循的最大重定向數,設定為0,則不會進行重定向
  maxRedirects: 5, // 預設值

  // `socketPath` 定義了在node.js中使用的UNIX套接字
  // e.g. '/var/run/docker.sock' 傳送請求到 docker 守護程序
  // 只能指定 `socketPath` 或 `proxy`
  // 若都指定,則使用 `socketPath`
  socketPath: null, // default

  // `httpAgent`和`httpsAgent`分別定義了在 node.js 中執行 http 和 https 請求時使用的自定義代理
  // 這樣就可以新增預設情況下未啟用的選項,如`keepAlive`
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // `proxy` 定義了代理伺服器的主機名,埠和協議
  // 可以使用常規的`http_proxy`和`https_proxy` 環境變數
  // 使用`false`可以停用代理功能,同時環境變數也會被忽略
  // `auth`表示應使用HTTP Basic auth連線到代理,並且提供憑據
  // 這將設定一個`Proxy-Authorization`請求頭,它會覆蓋`headers`中已存在的自定義`Proxy-Authorization`請求頭
  // 如果代理伺服器使用 HTTPS,則必須設定 protocol 為`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // 詳見https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),

  // `decompress`:表示是否要自動解壓縮響應正文
  // 將其設定為"false",它不會解壓縮響應,並會保留原始的"Content-Encoding"標頭
  // 如果設定為“true”,將從所有解壓縮響應的響應物件中移除"Content-Encoding"標頭
  // - 僅適用於Node (XHR 無法關閉解壓縮功能)
  decompress: true // 預設值
}

2.11.3 響應結構

  • 一個請求的響應包含以下資訊
{
  // data:由伺服器提供的響應
  data: {},

  // status:來自伺服器響應的 HTTP 狀態碼
  status: 200,

  // statusText:來自伺服器響應的 HTTP 狀態資訊
  statusText: 'OK',

  // headers:伺服器響應頭
  // 所有 header 名稱都是小寫,且可以使用方括號語法訪問
  // 例如: `response.headers['content-type']`
  headers: {},

  // config:是`axios`請求的配置資訊
  config: {},

  // request:是生成此響應的請求
  // 在node.js中它是最後一個ClientRequest例項 (in redirects),
  // 在瀏覽器中則是 XMLHttpRequest 例項
  request: {}
}

2.11.4 預設配置

🛠️ 全域性 axios 預設值
axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers.common["Authorization"] = AUTH_TOKEN;
axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";
🛠️ 自定義例項預設值
// 建立例項時配置預設值
const instance = axios.create({
	baseURL: "https://api.example.com",
});

// 建立例項後修改預設值
instance.defaults.headers.common["Authorization"] = AUTH_TOKEN;
🛠️ 配置的優先順序
  • 配置將會按優先順序進行合併

  • 它的順序是:在lib/defaults.js中找到的庫預設值,然後是例項的 defaults 屬性,最後是請求的config引數,後面的優先順序要高於前面的

// 使用庫提供的預設配置建立例項
// 此時超時配置的預設值是 `0`
const instance = axios.create();

// 重寫庫的超時預設值
// 現在,所有使用此例項的請求都將等待2.5秒,然後才會超時
instance.defaults.timeout = 2500;

// 重寫此請求的超時時間,因為該請求需要很長時間
instance.get("/longRequest", {
	timeout: 5000,
});

2.11.5 攔截器

  • 在請求或響應被 then 或 catch 處理前攔截它們
// 新增請求攔截器
axios.interceptors.request.use(
	function (config) {
		// 在傳送請求之前做些什麼
		return config;
	},
	function (error) {
		// 對請求錯誤做些什麼
		return Promise.reject(error);
	}
);

// 新增響應攔截器
axios.interceptors.response.use(
	function (response) {
		// 2xx 範圍內的狀態碼都會觸發該函式
		// 對響應資料做點什麼
		return response;
	},
	function (error) {
		// 超出 2xx 範圍的狀態碼都會觸發該函式
		// 對響應錯誤做點什麼
		return Promise.reject(error);
	}
);
  • 移除攔截器
const myInterceptor = axios.interceptors.request.use(function () {
	/*...*/
});
axios.interceptors.request.eject(myInterceptor);
  • 可以給自定義的 axios 例項新增攔截器
const instance = axios.create();
instance.interceptors.request.use(function () {
	/*...*/
});

2.11.6 錯誤處理

axios.get("/user/12345").catch(function (error) {
	if (error.response) {
		// 請求成功發出且伺服器也響應了狀態碼,但狀態程式碼超出了 2xx 的範圍
		console.log(error.response.data);
		console.log(error.response.status);
		console.log(error.response.headers);
	} else if (error.request) {
		// 請求已經成功發起,但沒有收到響應
		// `error.request` 在瀏覽器中是 XMLHttpRequest 的例項,
		// 而在node.js中是 http.ClientRequest 的例項
		console.log(error.request);
	} else {
		// 傳送請求時出了點問題
		console.log("Error", error.message);
	}
	console.log(error.config);
});
  • 使用 validateStatus 配置選項,可以自定義丟擲錯誤的 HTTP code
axios.get("/user/12345", {
	validateStatus: function (status) {
		return status < 500; // 處理狀態碼小於500的情況
	},
});
  • 使用 toJSON 可以獲取更多關於 HTTP 錯誤的資訊
axios.get("/user/12345").catch(function (error) {
	console.log(error.toJSON());
});

2.11.7 取消請求

🛠️ AbortController
const controller = new AbortController();

axios
	.get("/foo/bar", {
		signal: controller.signal,
	})
	.then(function (response) {
		//...
	});
// 取消請求
controller.abort();

2.11.8 請求體編碼

  • 預設情況下,axios 將 JavaScript 物件序列化為JSON。 要以application/x-www-form-urlencoded格式傳送資料
🛠️ 瀏覽器
  • URLSearchParams
const params = new URLSearchParams();
params.append("param1", "value1");
params.append("param2", "value2");
axios.post("/foo", params);
  • 注:不是所有的瀏覽器都支援 URLSearchParams ,但是可以使用polyfill (確保 polyfill 全域性環境)

  • 可以使用qs 庫編碼資料

import qs from "qs";
const data = { bar: 123 };
const options = {
	method: "POST",
	headers: { "content-type": "application/x-www-form-urlencoded" },
	data: qs.stringify(data),
	url,
};
axios(options);
🛠️ Node.js
  • 在 node.js 中, 可以使用querystring 模組
const querystring = require("querystring");
axios.post("http://something.com/", querystring.stringify({ foo: "bar" }));
  • 或者從'url module'中使用'URLSearchParams'
const url = require("url");
const params = new url.URLSearchParams({ foo: "bar" });
axios.post("http://something.com/", params.toString());
  • 也可以使用 qs 庫

  • Form data

const FormData = require("form-data");

const form = new FormData();
form.append("my_field", "my value");
form.append("my_buffer", new Buffer(10));
form.append("my_file", fs.createReadStream("/foo/bar.jpg"));

axios.post("https://example.com", form, { headers: form.getHeaders() });
  • 或者, 使用一個攔截器
axios.interceptors.request.use(config => {
	if (config.data instanceof FormData) {
		Object.assign(config.headers, config.data.getHeaders());
	}
	return config;
});

2.11.9 axios 二次封裝

  • 為什麼

    • 方便管理和維護

    • 請求的 url 地址統一管理

    • 某些介面需要傳遞 headers

  • request.js

// 1. 引入axios
import axios from 'axios';
// 2. 建立axios物件
const service = axios.create();
// 3. 請求攔截器(前端給後端傳送資料,沒有到後端)
// 做的事:headers給後端傳遞token
service.interceptors.request.use(
    config => {
    	return config;
	}, error => {
        Promise.reject(error);
	}
});
// 4.響應攔截器(後端給前端返回資料,前端到後端了)
service.interceptors.response.use(
    (response) => {
        // 這裡是對響應的簡化 data = response.data
        const { data, meta } = response.data
        if (meta.status === 200 || meta.status === 201){
            //回傳的資料
            return data
        } else {
            ElMessage.error(meta.msg)
            return Promise.reject(new Error(meta.msg))
        }
    },
    error => {
        error.response && ElMessage.error(error.response.data)
        return Promise.reject(new Error(error.response.data))
    }
)
export default service

2.11.10 API 解耦

  • 封裝
import request from "@/utils/request";
export function getsliders() {
	return request({
		url: "/api/slider/getsliders",
	});
}
  • 使用
<script type="text/javascript">
	import {getSliders} from '@/utils/api/course'
	export default{
	    data (){
	        return {
	            list:[]
	        }
	    },
	    created() {
	        getsliders().then (res=>{
	            console.log(res)
	        })
	    }
</script>

3. Ajax 原理

3.1 XMLHttpRequest

  • Ajax 是瀏覽器與伺服器通訊的技術,採用 XMLHttpRequest 物件相關程式碼

  • axios 是對 XHR 相關程式碼進行了封裝,讓我們只關心傳遞的介面引數

  • 學習 XHR 也是瞭解 axios 內部與伺服器互動過程的真正原理

  • 語法:

const xhr = new XMLHttpRequest();
xhr.open("請求方法", "請求url網址");
xhr.addEventListener("loadend", () => {
	// 響應結果
	console.log(xhr.response);
});
xhr.send();

  • 示例 1:獲取所有省份列表並展示到頁面上
<p class="my-p"></p>
<script>
	/**
	 * 目標:使用XMLHttpRequest物件與伺服器通訊
	 *  1. 建立 XMLHttpRequest 物件
	 *  2. 配置請求方法和請求 url 地址
	 *  3. 監聽 loadend 事件,接收響應結果
	 *  4. 發起請求
	 */
	const xhr = new XMLHttpRequest();

	xhr.open("get", "http://hmajax.itheima.net/api/province");
	// 攜帶查詢引數
	// xhr.open('get', 'http://hmajax.itheima.net/api/city?pname=遼寧省')

	xhr.addEventListener("loadend", () => {
		// console.log(xhr.response)
		const data = JSON.parse(xhr.response);
		document.querySelector(".my-p").innerHTML = data.list.join("<br>");
	});

	xhr.send();
</script>
  • 示例 2:地區查詢
<div class="container">
	<form id="editForm" class="row">
		<!-- 輸入省份名字 -->
		<div class="mb-3 col">
			<label class="form-label">省份名字</label>
			<input type="text" value="北京" name="province" class="form-control province" placeholder="請輸入省份名稱" />
		</div>
		<!-- 輸入城市名字 -->
		<div class="mb-3 col">
			<label class="form-label">城市名字</label>
			<input type="text" value="北京市" name="city" class="form-control city" placeholder="請輸入城市名稱" />
		</div>
	</form>
	<button type="button" class="btn btn-primary sel-btn">查詢</button>
	<br />
	<br />
	<p>地區列表:</p>
	<ul class="list-group">
		<!-- 示例地區 -->
		<!-- <li class="list-group-item">東城區</li> -->
	</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	/**
	 * 目標: 根據省份和城市名字, 查詢對應的地區列表
	 */
	document.querySelector(".sel-btn").addEventListener("click", () => {
		const pname = document.querySelector(".province").value;
		const cname = document.querySelector(".city").value;

		paramsObj = new URLSearchParams({
			pname,
			cname,
		});

		const xhr = new XMLHttpRequest();
		xhr.open("get", `http://hmajax.itheima.net/api/area?${paramsObj}`);
		xhr.addEventListener("loadend", () => {
			const data = JSON.parse(xhr.response);
			document.querySelector(".list-group").innerHTML = data.list
				.map(areaName => {
					return `<li class="list-group-item">${areaName}</li>`;
				})
				.join("");
		});
		xhr.send();
	});
</script>
  • 示例 3:資料提交
<button class="reg-btn">註冊使用者</button>
<script>
	/**
	 * 目標:使用xhr進行資料提交-完成註冊功能
	 */
	document.querySelector(".reg-btn").addEventListener("click", () => {
		const xhr = new XMLHttpRequest();

		xhr.open("post", "http://hmajax.itheima.net/api/register");

		xhr.addEventListener("loadend", () => {
			console.log(xhr.response);
		});
		// 設定請求頭
		xhr.setRequestHeader("content-type", "application/json");

		// 準備提交的資料
		const userObj = {
			username: "itheima667",
			password: "123456",
		};

		const userStr = JSON.stringify(userObj);
		xhr.send(userStr);
	});
</script>

3.2 Promise

3.2.1 對於非同步的理解

  • 示例
console.log("開始"); // 立即執行,輸出 "開始"

setTimeout(() => {
	// 設定一個非同步任務,延遲 1000 毫秒後執行
	console.log("非同步任務完成"); // 這個程式碼在 1 秒後執行
}, 1000);

console.log("結束"); // 立即執行,輸出 "結束"
  • 輸出順序

    • 開始:第一個console.log立即執行,輸出"開始"

    • 結束:第二個console.log立即執行,輸出"結束"

    • 非同步任務完成:在設定的 1 秒之後,setTimeout中的回撥函式才被呼叫,輸出"非同步任務完成"

  • 非同步體現

    • 非阻塞:在setTimeout呼叫後,程式碼並沒有等待 1 秒,而是繼續執行後面的console.log('結束')。這說明setTimeout是非阻塞的

    • 執行順序:即使setTimeout設定了一個 1 秒的延遲,它的回撥函式並不會立即執行,而是被放入事件佇列中,等待主執行緒空閒時再執行。這導致"結束"會在"非同步任務完成"之前輸出

  • 總結

    • 非同步允許程式碼在等待某個操作(如定時器)時繼續執行其他程式碼,而不會阻塞整個程式的執行

3.2.2 概述

  • 是什麼

    • 表示(管理)一個非同步操作最終狀態和結果值的物件
  • Promise 的好處是什麼?

    • 邏輯更清晰(成功或失敗會關聯後續的處理函式)

    • 瞭解 axios 函式內部運作的機制

    • 解決回撥函式地獄問題

  • Promise 管理非同步任務的語法

// 1. 建立 Promise 物件
const p = new Promise((resolve, reject) => {
	// 2. 執行非同步任務-並傳遞結果
	// 成功呼叫: resolve(值) 觸發 then() 執行
	// 失敗呼叫: reject(值) 觸發 catch() 執行
});
// 3. 接收結果
p.then(result => {
	// 成功
}).catch(error => {
	// 失敗
});
  • 示例
/**
 * 目標:使用Promise管理非同步任務
 */
const p = new Promise((resolve, reject) => {
	setTimeout(() => {
		// resolve('模擬AJAX請求-成功結果')
		reject(new Error("模擬AJAX請求-失敗結果"));
	}, 2000);
});

p.then(result => {
	console.log(result);
}).catch(error => {
	console.log(error);
});

3.2.3 三種狀態

  • 待定(pending):初始狀態,既沒有被兌現,也沒有被拒絕

  • 已兌現(fulfilled):操作成功完成

  • 已拒絕(rejected):操作失敗

  • 改變 Promise 物件狀態後,內部觸發對應回撥函式傳參並執行

Ajax入門以及Axios的詳細使用(含Promise)
  • 注:每個 Promise 物件一旦被兌現/拒絕,那麼狀態無法再被改變

3.2.4 案例-獲取省份列表

<p class="my-p"></p>
<script>
	const p = new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest();

		xhr.open("get", "http://hmajax.itheima.net/api/province");

		xhr.addEventListener("loadend", () => {
			// 判斷請求成功與否
			if (xhr.status >= 200 && xhr.status < 300) {
				resolve(JSON.parse(xhr.response));
			} else {
				reject(new Error(xhr.response));
			}
		});

		xhr.send();
	});

	p.then(reslut => {
		document.querySelector(".my-p").innerHTML = reslut.list.join("<br>");
	}).catch(error => {
		// 列印詳細資訊
		// console.dir(error)
		document.querySelector(".my-p").innerHTML = error.message;
	});
</script>

3.2.5 封裝簡易 axios

  • 步驟

    • 定義 myAxios 函式,接收配置物件,返回 Promise 物件

    • 發起 XHR 請求,預設請求方法為 GET

    • 呼叫成功/失敗的處理程式

    • 使用 myAxios 函式,獲取省份列表展示

  • 核心語法

function myAxios(config) {
	return new Promise((resolve, reject) => {
		// XHR 請求
		// 呼叫成功/失敗的處理程式
	});
}

myAxios({
	url: "目標資源地址",
})
	.then(result => {})
	.catch(error => {});
  • 示例:獲取省份列表
<p class="my-p"></p>
<script>
	function myAxios(config) {
		return new Promise((resolve, reject) => {
			const xhr = new XMLHttpRequest();

			xhr.open(config.method || "get", config.url);

			xhr.addEventListener("loadend", () => {
				if (xhr.status >= 200 && xhr.status < 300) {
					resolve(JSON.parse(xhr.response));
				} else {
					reject(new Error(xhr.response));
				}
			});

			xhr.send();
		});
	}

	myAxios({
		url: "http://hmajax.itheima.net/api/province",
	})
		.then(result => {
			document.querySelector(".my-p").innerHTML = result.list.join("<br>");
		})
		.catch(error => {
			document.querySelector(".my-p").innerHTML = error.message;
		});
</script>
  • 示例:獲取地區列表
<p class="my-p"></p>
<script>
	function myAxios(config) {
		return new Promise((resolve, reject) => {
			const xhr = new XMLHttpRequest();
			// 查詢引數
			if (config.params) {
				const paramsObj = new URLSearchParams(config.params);
				const paramsStr = paramsObj.toString();
				config.url += `?${paramsStr}`;
			}

			xhr.open(config.method || "get", config.url);

			xhr.addEventListener("loadend", () => {
				if (xhr.status >= 200 && xhr.status < 300) {
					resolve(JSON.parse(xhr.response));
				} else {
					reject(new Error(xhr.response));
				}
			});

			xhr.send();
		});
	}

	myAxios({
		url: "http://hmajax.itheima.net/api/area",
		params: {
			pname: "遼寧省",
			cname: "大連市",
		},
	})
		.then(result => {
			document.querySelector(".my-p").innerHTML = result.list.join("<br>");
		})
		.catch(error => {
			document.querySelector(".my-p").innerHTML = error.message;
		});
</script>
  • 示例:註冊使用者
<p class="my-p"></p>
<script>
	function myAxios(config) {
		return new Promise((resolve, reject) => {
			const xhr = new XMLHttpRequest();
			// 查詢引數
			if (config.params) {
				const paramsObj = new URLSearchParams(config.params);
				const paramsStr = paramsObj.toString();
				config.url += `?${paramsStr}`;
			}

			xhr.open(config.method || "get", config.url);

			xhr.addEventListener("loadend", () => {
				if (xhr.status >= 200 && xhr.status < 300) {
					resolve(JSON.parse(xhr.response));
				} else {
					reject(new Error(xhr.response));
				}
			});

			if (config.data) {
				xhr.setRequestHeader("content-type", "application/json");
				const jsonStr = JSON.stringify(config.data);
				xhr.send(jsonStr);
			} else {
				xhr.send();
			}
		});
	}

	myAxios({
		url: "http://hmajax.itheima.net/api/register",
		method: "post",
		data: {
			username: "itheima776",
			password: "66688879",
		},
	})
		.then(result => {
			document.querySelector(".my-p").innerHTML = result.message;
		})
		.catch(error => {
			document.querySelector(".my-p").innerHTML = error.message;
		});
</script>

3.2.6 回撥函式地獄

  • 概念:在回撥函式中巢狀回撥函式,一直巢狀下去就形成了回撥函式地獄

  • 缺點:可讀性差,異常無法捕獲,耦合性嚴重,牽一髮動全身

<form>
	<span>省份:</span>
	<select>
		<option class="province"></option>
	</select>
	<span>城市:</span>
	<select>
		<option class="city"></option>
	</select>
	<span>地區:</span>
	<select>
		<option class="area"></option>
	</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	/**
	 * 目標:演示回撥函式地獄
	 * 需求:獲取預設第一個省,第一個市,第一個地區並展示在下拉選單中
	 */
	axios({ url: "http://hmajax.itheima.net/api/province" })
		.then(result => {
			const pname = result.data.list[0];
			document.querySelector(".province").innerHTML = pname;
			axios({ url: "http://hmajax.itheima.net/api/city", params: { pname } }).then(result => {
				const cname = result.data.list[0];
				document.querySelector(".city").innerHTML = cname;
				axios({ url: "http://hmajax.itheima.net/api/area", params: { pname, cname } }).then(result => {
					const aname = result.data.list[0];
					document.querySelector(".area").innerHTML = aname;
				});
			});
		})
		.catch(error => {
			console.log(error);
		});
</script>

3.2.7 Promise 鏈式呼叫

  • 概念:依靠 then() 方法會返回一個新生成的 Promise 物件特性,繼續串聯下一環任務,直到結束

  • 細節:then() 回撥函式中的返回值,會影響新生成的 Promise 物件最終狀態和結果

  • 好處:透過鏈式呼叫,解決回撥函式巢狀問題

const p = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve("北京市");
	}, 1000);
});

const p2 = p.then(result => {
	console.log(result);
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(`${result} --- 北京`);
		}, 1000);
	});
});

p2.then(result => {
	console.log(result);
});
  • 解決回撥函式地獄
<form>
	<span>省份:</span>
	<select>
		<option class="province"></option>
	</select>
	<span>城市:</span>
	<select>
		<option class="city"></option>
	</select>
	<span>地區:</span>
	<select>
		<option class="area"></option>
	</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	let pname = "";
	axios({ url: "http://hmajax.itheima.net/api/province" })
		.then(result => {
			pname = result.data.list[0];
			document.querySelector(".province").innerHTML = pname;
			return axios({ url: "http://hmajax.itheima.net/api/city", params: { pname } });
		})
		.then(result => {
			const cname = result.data.list[0];
			document.querySelector(".city").innerHTML = cname;
			return axios({ url: "http://hmajax.itheima.net/api/area", params: { pname, cname } });
		})
		.then(result => {
			const aname = result.data.list[0];
			document.querySelector(".area").innerHTML = aname;
		});
</script>

3.2.8 async 函式和 await

在 async 函式內,使用 await 關鍵字取代 then 函式,等待獲取 Promise 物件成功狀態的結果值

<form>
	<span>省份:</span>
	<select>
		<option class="province"></option>
	</select>
	<span>城市:</span>
	<select>
		<option class="city"></option>
	</select>
	<span>地區:</span>
	<select>
		<option class="area"></option>
	</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	async function getData() {
		const pObj = await axios({ url: "http://hmajax.itheima.net/api/province" });
		const pname = pObj.data.list[0];
		const cObj = await axios({ url: "http://hmajax.itheima.net/api/city", params: { pname } });
		const cname = cObj.data.list[0];
		const aObj = await axios({ url: "http://hmajax.itheima.net/api/area", params: { pname, cname } });
		const aname = aObj.data.list[0];

		document.querySelector(".province").innerHTML = pname;
		document.querySelector(".city").innerHTML = cname;
		document.querySelector(".area").innerHTML = aname;
	}
	getData();
</script>
  • 捕獲錯誤
try {
	// 要執行的程式碼
	// 如果try裡某行程式碼報錯後,try中剩餘的程式碼不會執行了
} catch (error) {
	// error 接收的是
	錯誤訊息;
	// try 裡程式碼,如果有錯誤,直接進入這裡執行
}
  • 示例
<form>
	<span>省份:</span>
	<select>
		<option class="province"></option>
	</select>
	<span>城市:</span>
	<select>
		<option class="city"></option>
	</select>
	<span>地區:</span>
	<select>
		<option class="area"></option>
	</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	async function getData() {
		try {
			const pObj = await axios({ url: "http://hmajax.itheima.net/api/province" });
			const pname = pObj.data.list[0];
			const cObj = await axios({ url: "http://hmajax.itheima.net/api/city", params: { pname } });
			const cname = cObj.data.list[0];
			const aObj = await axios({ url: "http://hmajax.itheima.net/api/area", params: { pname, cname } });
			const aname = aObj.data.list[0];

			document.querySelector(".province").innerHTML = pname;
			document.querySelector(".city").innerHTML = cname;
			document.querySelector(".area").innerHTML = aname;
		} catch (error) {
			console.dir(error);
		}
	}
	getData();
</script>

3.3 事件迴圈(EventLoop)

  • 作用:事件迴圈負責執行程式碼,收集和處理事件以及執行佇列中的子任務

  • 原因:JavaScript 單執行緒(某一刻只能執行一行程式碼),為了讓耗時程式碼不阻塞其他程式碼執行,設計了事件迴圈模型

  • 概念:執行程式碼和收集非同步任務的模型,在呼叫棧空閒時,反覆呼叫任務佇列裡回撥函式的執行機制

  • JavaScript 內程式碼如何執行?

    • 執行同步程式碼,遇到非同步程式碼交給宿主瀏覽器環境執行,非同步有了結果後,把回撥函式放入任務佇列排隊當呼叫棧空閒後,反覆呼叫任務佇列裡的回撥函式
  • 練習

/**
 * 目標:閱讀並回答執行的順序結果
 */
console.log(1);
setTimeout(() => {
	console.log(2);
}, 0);
function myFn() {
	console.log(3);
}
function ajaxFn() {
	const xhr = new XMLHttpRequest();
	xhr.open("GET", "http://hmajax.itheima.net/api/province");
	xhr.addEventListener("loadend", () => {
		console.log(4);
	});
	xhr.send();
}
for (let i = 0; i < 1; i++) {
	console.log(5);
}
ajaxFn();
document.addEventListener("click", () => {
	console.log(6);
});
myFn();

結果:1 5 3 2 4 點選一次 document 就會執行一次列印 6

3.4 宏任務與微任務

  • 非同步任務分為

    • 宏任務:由瀏覽器環境執行的非同步程式碼

    • 微任務:由 JS 引擎環境執行的非同步程式碼

  • 事件迴圈模型
console.log(1);
setTimeout(() => {
	console.log(2);
}, 0);
const p = new Promise((resolve, reject) => {
	resolve(3);
});
p.then(res => {
	console.log(res);
});
console.log(4);

  • 注:宏任務每次在執行同步程式碼時,產生微任務佇列,清空微任務佇列任務後,微任務佇列空間釋放!下一次宏任務執行時,遇到微任務程式碼,才會再次申請微任務佇列空間放入回撥函式訊息排隊

  • 總結:一個宏任務包含微任務佇列,他們之間是包含關係,不是並列關係

  • 經典面試題

// 目標:回答程式碼執行順序
console.log(1);
setTimeout(() => {
	console.log(2);
	const p = new Promise(resolve => resolve(3));
	p.then(result => console.log(result));
}, 0);
const p = new Promise(resolve => {
	setTimeout(() => {
		console.log(4);
	}, 0);
	resolve(5);
});
p.then(result => console.log(result));
const p2 = new Promise(resolve => resolve(6));
p2.then(result => console.log(result));
console.log(7);

結果:1 7 5 6 2 3 4

3.5 Promise.all 靜態方法

  • 作用:合併多個 Promise 物件,等待所有同時成功完成(或某一個失敗),做後續邏輯
Ajax入門以及Axios的詳細使用(含Promise)
  • 語法:
const p = Promise.all([Promise物件, Promise物件, ...])	// 需要傳入一個可迭代的資料
p.then(result => {
  // result 結果: [Promise物件成功結果, Promise物件成功結果, ...]
}).catch(error => {
  // 第一個失敗的 Promise 物件,丟擲的異常物件
})
  • 示例:同時請求“北京”,“上海”,“廣州”,“深圳”的天氣並在網頁儘可能同時顯示
<ul class="my-ul"></ul>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	/**
	 * 業務:當我需要同一時間顯示多個請求的結果時,就要把多請求合併
	 * 例如:預設顯示"北京", "上海", "廣州", "深圳"的天氣在首頁檢視
	 * code:
	 * 北京-110100
	 * 上海-310100
	 * 廣州-440100
	 * 深圳-440300
	 */
	// 1. 請求城市天氣,得到Promise物件(此處可以使用陣列遍歷)
	const bjPromise = axios({ url: "http://hmajax.itheima.net/api/weather", params: { city: "110100" } });
	const shPromise = axios({ url: "http://hmajax.itheima.net/api/weather", params: { city: "310100" } });
	const gzPromise = axios({ url: "http://hmajax.itheima.net/api/weather", params: { city: "440100" } });
	const szPromise = axios({ url: "http://hmajax.itheima.net/api/weather", params: { city: "440300" } });

	// 2. 使用Promise.all,合併多個Promise物件
	const p = Promise.all([bjPromise, shPromise, gzPromise, szPromise]);
	p.then(result => {
		// 注意:結果陣列順序和合並時順序是一致
		console.log(result);
		const htmlStr = result
			.map(item => {
				return `<li>${item.data.data.area} --- ${item.data.data.weather}</li>`;
			})
			.join("");
		document.querySelector(".my-ul").innerHTML = htmlStr;
	}).catch(error => {
		console.dir(error);
	});
</script>
  • 案例:商品分類

    • 目標:把所有商品分類“同時”渲染到頁面上

    • 獲取所有一級分類資料

    • 遍歷 id,建立獲取二級分類請求

    • 合併所有二級分類 Promise 物件

    • 等待同時成功後,渲染頁面

<!-- 大容器 -->
<div class="container">
	<div class="sub-list"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	// 1. 獲取所有一級分類資料
	axios({
		url: "http://hmajax.itheima.net/api/category/top",
	}).then(result => {
		// console.log(result)
		const secPromiseList = result.data.data.map(item => {
			// 2. 遍歷id,建立獲取二級分類請求
			return axios({
				url: "http://hmajax.itheima.net/api/category/sub",
				params: {
					id: item.id,
				},
			});
		});
		const p = Promise.all(secPromiseList);
		p.then(result => {
			// console.log(result)
			document.querySelector(".sub-list").innerHTML = result
				.map(item => {
					const dataObj = item.data.data;
					return `
          <div class="item">
            <h3>${dataObj.name}</h3>
            <ul>
              ${dataObj.children
								.map(item => {
									return `
                    <li>
                      <a href="javascript:;">
                        <img src="${item.picture}" />
                        <p>${item.name}</p>
    				  </a>
 				   </li>
                    `;
								})
								.join("")}
    		</ul>
    	   </div>
          `;
				})
				.join("");
		});
	});
</script>
  • 案例:學習反饋

  • 框架

<div class="container">
	<h4 class="stu-title">學習反饋</h4>
	<img class="bg" src="./img/head.png" alt="" />
	<div class="item-wrap">
		<div class="hot-area">
			<span class="hot">熱門校區</span>
			<ul class="nav">
				<li><a target="_blank" href="http://bjcp.itheima.com/">北京</a></li>
				<li><a target="_blank" href="http://sh.itheima.com/">上海</a></li>
				<li><a target="_blank" href="http://gz.itheima.com/">廣州</a></li>
				<li><a target="_blank" href="http://sz.itheima.com/">深圳</a></li>
			</ul>
		</div>
		<form class="info-form">
			<div class="area-box">
				<span class="title">地區選擇</span>
				<select name="province" class="province">
					<option value="">省份</option>
				</select>
				<select name="city" class="city">
					<option value="">城市</option>
				</select>
				<select name="area" class="area">
					<option value="">地區</option>
				</select>
			</div>
			<div class="area-box">
				<span class="title">您的稱呼</span>
				<input type="text" name="nickname" class="nickname" value="播仔" />
			</div>
			<div class="area-box">
				<span class="title">寶貴建議</span>
				<textarea type="text" name="feedback" class="feedback" placeholder="您對AJAX階段課程寶貴的建議"></textarea>
			</div>
			<div class="area-box">
				<button type="button" class="btn btn-secondary submit">確定提交</button>
			</div>
		</form>
	</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script>
<script src="./js/form-serialize.js"></script>
<!-- 核心程式碼 -->
<script src="./js/index.js"></script>
  • index.js
/**
 * 目標1:完成省市區下拉選單切換
 *  1.1 設定省份下拉選單資料
 *  1.2 切換省份,設定城市下拉選單資料,清空地區下拉選單
 *  1.3 切換城市,設定地區下拉選單資料
 */
// 1.1 設定省份下拉選單資料
axios({
	url: "http://hmajax.itheima.net/api/province",
}).then(result => {
	const str = result.data.list.map(pname => `<option value="${pname}">${pname}</option>`).join("");
	document.querySelector(".province").innerHTML = `<option value="">省份</option>` + str;
});

// 1.2 切換省份,設定城市下拉選單資料,清空地區下拉選單
document.querySelector(".province").addEventListener("change", async e => {
	const result = await axios({
		url: "http://hmajax.itheima.net/api/city",
		params: {
			pname: e.target.value,
		},
	});
	// console.log(result)
	const str = result.data.list
		.map(cname => {
			return `<option value="${cname}">${cname}</option>`;
		})
		.join("");

	document.querySelector(".city").innerHTML = `<option value="">城市</option>` + str;

	// 清空地區下拉選單
	document.querySelector(".area").innerHTML = `<option value="">地區</option>`;
});

// 1.3 切換城市,設定地區下拉選單資料
document.querySelector(".city").addEventListener("change", async e => {
	const result = await axios({
		url: "http://hmajax.itheima.net/api/area",
		params: {
			pname: document.querySelector(".province").value,
			cname: e.target.value,
		},
	});
	const str = result.data.list.map(aname => `<option value="${aname}">${aname}</option>`);
	document.querySelector(".area").innerHTML = `<option value="">地區</option>` + str;
});
/**
 * 目標2:收集資料提交儲存
 *  2.1監聽提交的點選事件
 *  2.2依靠外掛收集表單資料
 *  2.3基於axios提交儲存,顯示結果
 */

document.querySelector(".submit").addEventListener("click", async () => {
	try {
		const form = document.querySelector(".info-form");
		const data = serialize(form, { hash: true, empty: true });
		const result = await axios({
			url: "http://hmajax.itheima.net/api/feedback",
			method: "post",
			data,
		});
		alert(result.data.message);
	} catch (error) {
		// console.dir(error)
		alert(error.response.data.message);
	}
});

相關文章