在前端开发领域,AJAX(异步JavaScript和XML)早已不是单纯的技术概念,而是连接用户操作与后端数据的“桥梁”。从电商平台的实时库存更新,到后台系统的批量数据提交,再到社交应用的消息推送,AJAX的实战能力直接决定了产品的交互体验与性能表现。很多开发者掌握了“发起请求-接收响应”的基础用法,却在面对“高频请求泛滥”“大文件上传中断”“并发请求拥堵”等实战问题时束手无策。本文摒弃空泛理论,聚焦 4大核心业务场景 ,通过“需求拆解-技术选型-完整代码-避坑技巧”的闭环,带你掌握AJAX在实战中的落地方法,解决80%的异步交互痛点。
一、表单交互:从“无刷新提交”到“精准校验”
表单是用户与系统交互的核心载体,注册登录、信息编辑、数据提交等场景都离不开表单。传统表单提交依赖页面刷新,不仅体验割裂,还容易导致数据丢失。AJAX的核心价值在于实现“输入即校验、提交无刷新”,同时通过前端预校验与状态控制,减少无效请求,提升交互流畅度。
1.1 实战痛点与技术选型
|
实战痛点 |
解决方案 |
技术工具 |
|---|---|---|
|
输入时高频请求后端校验 |
防抖控制请求时机,前端预校验优先 |
防抖函数+正则表达式 |
|
用户连续点击提交按钮 |
状态锁+按钮置灰双重保障 |
isSubmitting状态变量 |
|
错误信息展示混乱 |
与后端约定结构化返回格式,精准渲染字段错误 |
字段级错误容器+统一渲染函数 |
1.2 完整代码:用户注册表单(含验证码)
<!-- HTML结构:语义化布局+错误容器 -->
<form id="registerForm" class="form-container">
<div class="form-item">
<label for="phone">手机号:</label>
<input type="tel" name="phone" id="phone" placeholder="请输入11位手机号">
<span class="error-message" id="phoneError"></span>
</div>
<div class="form-item">
<label for="code">验证码:</label>
<div class="code-group">
<input type="text" name="code" id="code" placeholder="请输入6位验证码">
<button type="button" id="getCodeBtn" class="btn-code">获取验证码</button>
</div>
<span class="error-message" id="codeError"></span>
</div>
<div class="form-item">
<label for="password">密码:</label>
<input type="password" name="password" id="password" placeholder="请输入6-16位密码">
<span class="error-message" id="passwordError"></span>
</div>
<button type="submit" id="submitBtn" class="btn-submit">注册并登录</button>
</form>
< href="it8.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="ity.tlbelt.cn">
< href="itz.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="it9.tlbelt.cn">
< href="itc.tlbelt.cn">
< href="itx.tlbelt.cn">
< href="ito.tlbelt.cn">
< href="itg.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="itm.tlbelt.cn">
< href="ito.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="itb.tlbelt.cn">
< href="itf.tlbelt.cn">
< href="itr.tlbelt.cn">
< href="itx.tlbelt.cn">
< href="itr.tlbelt.cn">
< href="ita.tlbelt.cn">
<script>
// 1. 通用工具函数:防抖(减少高频请求)
function debounce(func, delay = 500) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// 2. 错误处理工具:统一渲染与清除
function showError(field, message) {
const errorEl = document.getElementById(`${field}Error`);
const inputEl = document.getElementById(field);
errorEl.textContent = message;
errorEl.style.color = "#ff4d4f";
inputEl.classList.add("error-border"); // 输入框红色边框提示
}
function clearError(field) {
const errorEl = document.getElementById(`${field}Error`);
const inputEl = document.getElementById(field);
errorEl.textContent = "";
inputEl.classList.remove("error-border");
}
// 3. 手机号校验:失焦触发+前端预校验+后端查重
const phoneInput = document.getElementById("phone");
phoneInput.addEventListener("blur", debounce(async (e) => {
const phone = e.target.value.trim();
// 前端预校验:先判断格式,减少后端请求
if (!phone) {
showError("phone", "手机号不能为空");
return;
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phone", "请输入正确的11位手机号");
return;
}
// 后端校验:判断是否已注册
try {
const res = await fetch("/api/user/check-phone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
showError("phone", data.message); // 后端返回:"该手机号已注册"
} else {
clearError("phone");
}
} catch (err) {
showError("phone", "网络异常,请稍后再试");
}
}));
// 4. 验证码逻辑:倒计时+防重复发送
const getCodeBtn = document.getElementById("getCodeBtn");
let isCodeSending = false; // 状态锁
getCodeBtn.addEventListener("click", async () => {
const phone = phoneInput.value.trim();
// 先校验手机号合法性
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phone", "请先输入正确的手机号");
phoneInput.focus();
return;
}
if (isCodeSending) return; // 防止重复点击
// 按钮状态切换:置灰+倒计时
isCodeSending = true;
getCodeBtn.disabled = true;
getCodeBtn.textContent = "60s后重新获取";
let countdown = 60;
try {
// 调用验证码接口
const res = await fetch("/api/user/send-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
throw new Error(data.message);
}
} catch (err) {
// 异常处理:恢复按钮状态
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
showError("code", err.message || "验证码发送失败");
}
// 倒计时逻辑
const timer = setInterval(() => {
countdown--;
getCodeBtn.textContent = `${countdown}s后重新获取`;
if (countdown <= 0) {
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
}, 1000);
});
// 5. 表单提交:全量校验+防重复提交+结果处理
const registerForm = document.getElementById("registerForm");
const submitBtn = document.getElementById("submitBtn");
let isSubmitting = false;
registerForm.addEventListener("submit", async (e) => {
e.preventDefault(); // 阻止默认刷新
if (isSubmitting) return;
// 1. 收集表单数据
const formData = {
phone: document.getElementById("phone").value.trim(),
code: document.getElementById("code").value.trim(),
password: document.getElementById("password").value.trim()
};
// 2. 前端全量校验
let isFormValid = true;
Object.keys(formData).forEach(field => {
if (!formData[field]) {
showError(field, `${field === 'phone' ? '手机号' : field === 'code' ? '验证码' : '密码'}不能为空`);
isFormValid = false;
} else {
clearError(field);
}
});
if (formData.password.length < 6 || formData.password.length > 16) {
showError("password", "密码长度需为6-16位");
isFormValid = false;
}
if (!isFormValid) return;
// 3. 提交状态控制
isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = "注册中...";
try {
// 4. 调用注册接口
const res = await fetch("/api/user/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
const data = await res.json();
if (data.code === 200) {
// 注册成功:存储Token+跳转登录页
localStorage.setItem("token", data.token);
alert("注册成功!即将跳转到首页");
window.location.href = "/home";
} else {
// 结构化错误:字段级错误精准提示
if (data.field) {
showError(`${data.field}Error`, data.message);
} else {
alert(data.message); // 全局错误(如系统维护)
}
}
} catch (err) {
alert("网络异常,注册失败,请稍后再试");
} finally {
// 5. 恢复状态(无论成功失败)
isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = "注册并登录";
}
});
</script>
1.3 实战避坑指南
前端预校验不可少:手机号格式、密码长度等简单校验优先在前端完成,既能减少无效请求,也能让用户即时感知错误,提升体验。
状态锁是关键:仅靠按钮置灰无法完全防重复提交(用户可通过控制台修改DOM属性),必须配合isSubmitting、isCodeSending等状态变量。
错误格式要约定:与后端提前约定返回格式(如{code, message, field}),field字段指定错误对应的表单字段,实现错误信息精准渲染。
用户注册、登录或信息编辑时,表单是最常见的交互载体。传统表单提交需要页面刷新,而AJAX可实现"输入即校验、提交无刷新",大幅提升体验。核心需求包括:实时字段校验(手机号、邮箱格式)、防止重复提交、错误信息即时反馈。
实时校验:输入框失焦(blur)或输入停止后(防抖)触发AJAX请求,校验字段合法性;
防重复提交:提交按钮置灰+状态锁,避免用户连续点击;
错误处理:后端返回错误信息后,精准渲染到对应字段下方,而非全局弹窗。
1.1 需求拆解与技术选型
技术选型:原生Fetch API(轻量场景)+ 防抖函数(减少无效请求)。
前端预校验优先:手机号格式、密码长度等简单校验优先在前端完成,减少无效的后端请求,降低服务器压力;
状态锁双重保障:除了按钮置灰,增加
1.2 完整代码实现(用户注册表单)
<!-- HTML结构 -->
<form id="registerForm">
<div class="form-item">
<label>手机号:</label>
<input type="tel" name="phone" id="phone" placeholder="请输入手机号">
<span class="error-message" id="phoneError"></span>
</div>
<div class="form-item">
< href="itj.tlbelt.cn">
< href="itw.tlbelt.cn">
< href="itb.tlbelt.cn">
< href="itn.tlbelt.cn">
< href="it3.tlbelt.cn">
< href="itw.tlbelt.cn">
< href="itq.tlbelt.cn">
< href="itn.tlbelt.cn">
< href="itu.tlbelt.cn">
< href="it8.tlbelt.cn">
< href="itc.tlbelt.cn">
< href="it3.tlbelt.cn">
< href="it0.tlbelt.cn">
< href="itk.tlbelt.cn">
< href="itf.tlbelt.cn">
< href="itt.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="it9.tlbelt.cn">
<label>验证码:</label>
<input type="text" name="code" id="code" placeholder="请输入验证码">
<button type="button" id="getCodeBtn">获取验证码</button>
<span class="error-message" id="codeError"></span>
</div>
<div class="form-item">
<label>密码:</label>
<input type="password" name="password" id="password" placeholder="请输入密码">
<span class="error-message" id="passwordError"></span>
</div>
<button type="submit" id="submitBtn">注册</button>
</form>
<script>
// 1. 通用防抖函数(避免输入时高频请求)
function debounce(func, delay = 500) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// 2. 错误信息渲染工具函数
function showError(elementId, message) {
const errorEl = document.getElementById(elementId);
errorEl.textContent = message;
errorEl.style.color = "#ff4d4f";
// 给输入框添加错误样式
document.getElementById(elementId.replace("Error", "")).classList.add("error-border");
}
// 3. 清除错误信息
function clearError(elementId) {
const errorEl = document.getElementById(elementId);
errorEl.textContent = "";
document.getElementById(elementId.replace("Error", "")).classList.remove("error-border");
}
// 4. 手机号实时校验(失焦+防抖)
const phoneInput = document.getElementById("phone");
phoneInput.addEventListener("blur", debounce(async (e) => {
const phone = e.target.value.trim();
if (!phone) {
showError("phoneError", "手机号不能为空");
return;
}
// 先做前端格式校验,减少后端请求
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phoneError", "手机号格式错误");
return;
}
try {
// 调用后端校验接口(是否已注册)
const res = await fetch("/api/user/check-phone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
showError("phoneError", data.message); // 后端返回:"该手机号已注册"
} else {
clearError("phoneError");
}
} catch (err) {
showError("phoneError", "网络异常,请稍后再试");
}
}));
// 5. 获取验证码(防重复点击)
const getCodeBtn = document.getElementById("getCodeBtn");
let isCodeSending = false;
getCodeBtn.addEventListener("click", async () => {
const phone = phoneInput.value.trim();
if (!/^1[3-9]\d{9}$/.test(phone)) {
showError("phoneError", "请先输入正确的手机号");
return;
}
if (isCodeSending) return; // 状态锁防重复点击
// 按钮置灰并倒计时
isCodeSending = true;
getCodeBtn.disabled = true;
getCodeBtn.textContent = "60s后重新获取";
let countdown = 60;
try {
// 调用获取验证码接口
const res = await fetch("/api/user/send-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone })
});
const data = await res.json();
if (data.code !== 200) {
showError("codeError", data.message);
// 倒计时中断,恢复按钮状态
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
} catch (err) {
showError("codeError", "验证码发送失败");
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
// 倒计时逻辑
const timer = setInterval(() => {
countdown--;
getCodeBtn.textContent = `${countdown}s后重新获取`;
if (countdown <= 0) {
clearInterval(timer);
getCodeBtn.disabled = false;
getCodeBtn.textContent = "获取验证码";
isCodeSending = false;
}
}, 1000);
});
// 6. 表单提交(防重复提交+全量校验)
const registerForm = document.getElementById("registerForm");
const submitBtn = document.getElementById("submitBtn");
let isSubmitting = false;
registerForm.addEventListener("submit", async (e) => {
e.preventDefault(); // 阻止默认刷新行为
if (isSubmitting) return;
// 1. 收集表单数据
const formData = {
phone: document.getElementById("phone").value.trim(),
code: document.getElementById("code").value.trim(),
password: document.getElementById("password").value.trim()
};
// 2. 前端全量校验
let isFormValid = true;
if (!formData.phone) {
showError("phoneError", "手机号不能为空");
isFormValid = false;
}
if (!formData.code) {
showError("codeError", "验证码不能为空");
isFormValid = false;
}
if (formData.password.length < 6) {
showError("passwordError", "密码长度不能少于6位");
isFormValid = false;
}
if (!isFormValid) return;
// 3. 提交状态控制
isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = "注册中...";
try {
// 4. 调用注册接口
const res = await fetch("/api/user/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
const data = await res.json();
if (data.code === 200) {
// 注册成功,跳转登录页
alert("注册成功!即将跳转到登录页");
window.location.href = "/login";
} else {
// 后端返回的字段级错误(如验证码过期)
if (data.field) {
showError(`${data.field}Error`, data.message);
} else {
alert(data.message); // 全局错误(如系统维护)
}
}
} catch (err) {
alert("网络异常,注册失败,请稍后再试");
} finally {
// 5. 恢复按钮状态(无论成功失败)
isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = "注册";
}
});
</script>
1.3 实战优化技巧
isSubmitting等状态变量,防止通过控制台等方式绕过UI限制重复提交;
错误信息结构化:与后端约定返回格式(如
{code, message, field}),实现错误信息的精准渲染。
二、实时搜索:平衡性能与体验的核心技巧
电商平台的商品搜索、文档系统的内容检索等场景,需要根据用户输入实时返回匹配结果。这类场景的核心痛点是“用户输入频率高导致请求泛滥”,若直接发起请求,不仅会增加服务器压力,还可能出现“旧请求结果覆盖新请求”的错乱问题。AJAX的实战方案围绕“控制请求时机、减少重复请求、保证结果正确”展开。
2.1 核心技术方案
-
防抖控制:用户输入停止300ms后再发起请求,避免输入过程中频繁请求(如输入“手机”时,只在输入完成后请求一次);
-
结果缓存:相同关键词的搜索结果缓存到内存,短时间内重复输入无需再次请求;
-
请求中断:用户快速输入新关键词时,中断上一次未完成的请求,避免旧结果覆盖新结果;
-
空值处理:输入为空时清空搜索结果,避免无效请求,同时隐藏结果列表。
2.2 完整代码:商品实时搜索(含联想功能)
电商平台的商品搜索、文档系统的内容检索等场景,需要根据用户输入实时返回匹配结果。核心痛点是"输入频率高导致请求泛滥",解决方案是通过防抖控制请求时机,配合缓存减少重复请求。
2.1 核心技术点
-
防抖控制:用户输入停止300ms后再发起请求,避免输入过程中频繁请求;
-
结果缓存:相同关键词的搜索结果缓存到内存,短时间内重复输入无需再次请求;
-
空值处理:输入为空时清空搜索结果,避免无效请求;
-
请求中断:用户快速输入新关键词时,中断上一次未完成的请求,避免结果错乱。
2.2 完整代码实现(商品搜索)
<!-- HTML结构 -->
<div class="search-container">
<input type="text" id="searchInput" placeholder="请输入商品名称搜索...">
<ul id="searchSuggest" class="suggest-list"></ul>
</div>
<script>
// 1. 初始化变量:缓存容器、请求控制器(用于中断请求)
const searchCache = new Map(); // 缓存:key=关键词,value=搜索结果
let abortController = null;
// 2. 防抖函数(搜索场景延迟300ms更合适)
function debounce(func, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// 3. 渲染搜索结果
function renderSuggest(results) {
const suggestEl = document.getElementById("searchSuggest");
if (results.length === 0) {
suggestEl.innerHTML = "<li class='empty'>暂无匹配结果</li>";
return;
}
// 拼接结果列表
const html = results.map(item => `
<li class="suggest-item" data-id="${item.id}">
<img src="${item.cover}" alt="${item.name}">
<div class="info">
<p class="name">${item.name}</p>
<p class="price">¥${item.price.toFixed(2)}</p>
</div>
</li>
`).join("");
suggestEl.innerHTML = html;
// 给结果项绑定点击事件(跳转到商品详情)
suggestEl.querySelectorAll(".suggest-item").forEach(item => {
item.addEventListener("click", () => {
window.location.href = `/product/${item.dataset.id}`;
});
});
}
// 4. 搜索核心函数
async function searchProduct(keyword) {
const suggestEl = document.getElementById("searchSuggest");
// 空关键词:清空结果和缓存
if (!keyword.trim()) {
suggestEl.innerHTML = "";
searchCache.delete(keyword);
return;
}
// 命中缓存:直接渲染
if (searchCache.has(keyword)) {
renderSuggest(searchCache.get(keyword));
return;
}
// 中断上一次未完成的请求(防止结果顺序错乱)
if (abortController) abortController.abort();
abortController = new AbortController();
const signal = abortController.signal;
try {
suggestEl.innerHTML = "<li class='loading'>搜索中...</li>";
// 发起搜索请求(携带信号用于中断)
const res = await fetch(`/api/product/search?q=${encodeURIComponent(keyword)}`, { signal });
if (!res.ok) throw new Error("搜索失败");
const data = await res.json();
if (data.code === 200) {
// 存入缓存(只缓存有效结果)
searchCache.set(keyword, data.data);
renderSuggest(data.data);
}
} catch (err) {
// 忽略中断错误,只处理网络错误
if (err.name !== "AbortError") {
suggestEl.innerHTML = "<li class='error'>搜索异常,请稍后再试</li>";
}
}
}
// 5. 绑定输入事件
const searchInput = document.getElementById("searchInput");
searchInput.addEventListener("input", debounce((e) => {
const keyword = e.target.value.trim();
searchProduct(keyword);
}));
// 6. 点击页面其他区域关闭搜索结果
document.addEventListener("click", (e) => {
const searchContainer = document.querySelector(".search-container");
if (!searchContainer.contains(e.target)) {
document.getElementById("searchSuggest").innerHTML = "";
}
});
</script>
2.3 性能优化关键点
关键词编码:使用
encodeURIComponent处理关键词中的特殊字符(如空格、中文),避免请求参数错误;
缓存有效期:若商品数据更新频繁,可给缓存添加过期时间(如5分钟),避免展示旧数据;
结果截断:后端返回结果建议限制在10条以内,减少前端渲染压力和数据传输量。
三、数据看板场景:并发请求与进度管理——提升数据加载效率
后台管理系统的数据看板通常需要同时加载多个接口数据(如销售额、订单量、用户数),若无序发起请求,可能导致浏览器并发上限被占满,出现部分数据加载缓慢的问题。核心需求是"控制并发数量、展示整体加载进度、失败后可重试"。
3.1 技术方案:并发调度器+进度监控
使用"请求池+队列"模式控制并发数(通常设为3-5,避免超过浏览器默认并发限制),通过已完成请求数/总请求数计算加载进度,单个请求失败时提供重试按钮,不影响其他请求。
3.2 完整代码实现(数据看板)
<!-- HTML结构 -->
<div class="dashboard">
<div class="loading-progress">
<div class="progress-bar" id="progressBar"></div>
<span id="progressText">加载中... 0%</span>
</div>
<div class="dashboard-grid">
<div class="card" id="salesCard">
<h3>今日销售额</h3>
<p class="value" id="salesValue">--</p>
<button class="retry-btn" style="display:none" data-target="sales">重试</button>
</div>
<div class="card" id="orderCard">
<h3>今日订单数</h3>
<p class="value" id="orderValue">--</p>
<button class="retry-btn" style="display:none" data-target="order">重试</button>
</div>
<div class="card" id="userCard">
<h3>新增用户数</h3>
<p class="value" id="userValue">--</p>
<button class="retry-btn" style="display:none" data-target="user">重试</button>
</div>
<div class="card" id="conversionCard">
<h3>转化率</h3>
<p class="value" id="conversionValue">--</p>
<button class="retry-btn" style="display:none" data-target="conversion">重试</button>
</div>
</div>
</div>
<script>
// 1. 定义需要加载的请求配置(关联卡片与接口)
const dashboardApis = [
{
key: "sales", // 对应卡片标识
url: "/api/dashboard/sales",
render: (data) => { // 渲染函数
document.getElementById("salesValue").textContent = `¥${data.toLocaleString()}`;
}
},
{
key: "order",
url: "/api/dashboard/orders",
render: (data) => {
document.getElementById("orderValue").textContent = data.toLocaleString();
}
},
{
key: "user",
url: "/api/dashboard/new-users",
render: (data) => {
document.getElementById("userValue").textContent = data.toLocaleString();
}
},
{
key: "conversion",
url: "/api/dashboard/conversion",
render: (data) => {
document.getElementById("conversionValue").textContent = `${(data * 100).toFixed(2)}%`;
}
}
];
// 2. 并发请求调度器(限制并发数为3)
async function requestConcurrent(requests, limit = 3) {
const results = [];
const executing = new Set();
const queue = [...requests];
let completedCount = 0; // 已完成请求数(用于计算进度)
const totalCount = requests.length; // 总请求数
// 更新进度条
function updateProgress() {
const progress = Math.round((completedCount / totalCount) * 100);
document.getElementById("progressBar").style.width = `${progress}%`;
document.getElementById("progressText").textContent = `加载中... ${progress}%`;
// 全部完成后隐藏进度条
if (progress === 100) {
setTimeout(() => {
document.querySelector(".loading-progress").style.display = "none";
}, 500);
}
}
async function dispatch() {
if (queue.length === 0 && executing.size === 0) return results;
while (executing.size < limit && queue.length > 0) {
const { key, request, render } = queue.shift();
const promise = request()
.then(data => {
render(data); // 成功后渲染数据
return { key, success: true };
})
.catch(err => {
// 失败后显示重试按钮
document.querySelector(`.retry-btn[data-target="${key}"]`).style.display = "inline-block";
return { key, success: false, error: err.message };
})
.finally(() => {
executing.delete(promise);
completedCount++;
updateProgress(); // 每完成一个请求更新进度
dispatch();
});
executing.add(promise);
results.push(promise);
}
await Promise.all(executing);
return dispatch();
}
updateProgress(); // 初始化进度
return dispatch();
}
// 3. 初始化请求函数(包装每个API请求)
function initDashboardRequests() {
return dashboardApis.map(api => ({
key: api.key,
render: api.render,
request: () => fetch(api.url)
.then(res => {
if (!res.ok) throw new Error(`请求失败: ${res.status}`);
return res.json();
})
.then(data => {
if (data.code !== 200) throw new Error(data.message);
return data.data;
})
}));
}
// 4. 重试单个请求
document.querySelectorAll(".retry-btn").forEach(btn => {
btn.addEventListener("click", async () => {
const key = btn.dataset.target;
// 找到对应的API配置
const api = dashboardApis.find(item => item.key === key);
if (!api) return;
// 按钮置为加载中状态
btn.textContent = "重试中...";
btn.disabled = true;
try {
// 重新请求
const res = await fetch(api.url);
const data = await res.json();
if (data.code === 200) {
api.render(data.data); // 重新渲染
btn.style.display = "none"; // 隐藏重试按钮
} else {
throw new Error(data.message);
}
} catch (err) {
btn.textContent = "重试失败,点击再试";
btn.disabled = false;
}
});
});
// 5. 页面加载时初始化数据看板
window.addEventListener("load", () => {
const requests = initDashboardRequests();
requestConcurrent(requests);
});
</script>
四、大文件上传场景:断点续传与进度展示——解决传输痛点
视频、压缩包等大文件上传是企业开发中的常见需求,传统一次性上传容易因网络中断、页面刷新导致前功尽弃。核心解决方案是"文件分片+断点续传",将大文件分割为小分片上传,支持暂停/继续、进度展示,且网络恢复后可从已上传部分继续。
4.1 核心原理
-
文件分片:使用
Blob.slice()将文件分割为固定大小的分片(如5MB);
-
唯一标识:生成文件MD5作为唯一标识,确保后端能将分片关联到同一文件;
-
状态查询:上传前请求后端,获取已上传的分片索引,避免重复上传;
-
分片上传:并发上传未完成的分片,实时计算整体进度;
-
分片合并:所有分片上传完成后,请求后端合并为完整文件。
4.2 完整代码实现(基于Axios)
编辑推荐:
下一篇:
相关推荐
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
雷神推出 MIX PRO II 迷你主机:基于 Ultra 200H,玻璃上盖 + ARGB 灯效
2 月 9 日消息,雷神 (THUNDEROBOT) 现已宣布推出基于英
-
制造商 Musnap 推出彩色墨水屏电纸书 Ocean C:支持手写笔、第三方安卓应用
2 月 10 日消息,制造商 Musnap 现已在海外推出一款 Oce
热文推荐
- 爱奇艺财报“潜台词”:如何服务内容新时代
爱奇艺财报“潜台词”:如何服务内容新时代
26-03-03
- 第二十七届高交会3E亚洲消费电子展在深圳启幕 全景呈现消费电子科技新未来
第二十七届高交会3E亚洲消费电子展在深圳启幕 全景呈现消费电子科技新未来
26-03-03
- TCL科技:三季报“喜色”与多元化“暗伤”
TCL科技:三季报“喜色”与多元化“暗伤”
26-03-03
- “多极引擎”撬动千亿市场 大麦娱乐构建现实娱乐新生态
“多极引擎”撬动千亿市场 大麦娱乐构建现实娱乐新生态
26-03-03
- 引领酒旅数智化!雅里数科受邀参加环球旅讯「大湾区数智论坛」
引领酒旅数智化!雅里数科受邀参加环球旅讯「大湾区数智论坛」
26-03-03
- 异机用 LogMiner 挖掘归档日志:实践要点与最小化恢复思路
异机用 LogMiner 挖掘归档日志:实践要点与最小化恢复思路
26-03-03
- AWR 报告为什么会“缺失”?一次关于 Oracle 性能诊断的深入排查
AWR 报告为什么会“缺失”?一次关于 Oracle 性能诊断的深入排查
26-03-03
- 解决 Oracle 11g Data Guard ORA-16047 的实战经验
- 深入数据库性能优化:从参数调优到RAC高可用架构构建
深入数据库性能优化:从参数调优到RAC高可用架构构建
26-03-03
- 安谋科技发布NPU IP“周易”X3 驱动架构革新再定义端侧AI
安谋科技发布NPU IP“周易”X3 驱动架构革新再定义端侧AI
26-03-03
