《React全栈开发:从零搭建企业级应用的全指南》‌

来源:这里教程网 时间:2026-03-03 22:53:22 作者:

在前端开发领域,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="iti.tlbelt.cn">
< href="it0.tlbelt.cn">
< href="itq.tlbelt.cn">
< href="itg.tlbelt.cn">
< href="itg.tlbelt.cn">
< href="itb.tlbelt.cn">
< href="itb.tlbelt.cn">
< href="it7.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="it6.tlbelt.cn">
< href="it4.guance33.com">
< href="itj.guance33.com">
< href="itt.guance33.com">
< href="itw.guance33.com">
< href="it5.guance33.com">
< href="ity.guance33.com">
< href="itd.guance33.com">
< href="ito.guance33.com">
< href="itp.guance33.com">
< href="itw.guance33.com">
<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可实现"输入即校验、提交无刷新",大幅提升体验。核心需求包括:实时字段校验(手机号、邮箱格式)、防止重复提交、错误信息即时反馈。

1.1 需求拆解与技术选型

  • 实时校验:输入框失焦(blur)或输入停止后(防抖)触发AJAX请求,校验字段合法性;
  • 防重复提交:提交按钮置灰+状态锁,避免用户连续点击;
  • 错误处理:后端返回错误信息后,精准渲染到对应字段下方,而非全局弹窗。
    技术选型:原生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="itp.guance33.com">
    < href="ita.guance33.com">
    < href="itb.guance33.com">
    < href="ito.guance33.com">
    < href="itl.guance33.com">
    < href="itx.guance33.com">
    < href="itr.guance33.com">
    < href="iti.guance33.com">
    < href="itf.guance33.com">
    < href="itr.guance33.com">
    < href="itf.guance33.com">
    < href="it4.guance33.com">
    < href="it6.guance33.com">
    < href="iti.guance33.com">
    < href="it4.guance33.com">
    < href="itg.guance33.com">
    < href="it4.guance33.com">
    < href="itu.guance33.com">
    < href="it6.guance33.com">
    < href="its.guance33.com">
        <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 核心技术方案

    1. 防抖控制:用户输入停止300ms后再发起请求,避免输入过程中频繁请求(如输入“手机”时,只在输入完成后请求一次);
    2. 结果缓存:相同关键词的搜索结果缓存到内存,短时间内重复输入无需再次请求;
    3. 请求中断:用户快速输入新关键词时,中断上一次未完成的请求,避免旧结果覆盖新结果;
    4. 空值处理:输入为空时清空搜索结果,避免无效请求,同时隐藏结果列表。

    2.2 完整代码:商品实时搜索(含联想功能)

    电商平台的商品搜索、文档系统的内容检索等场景,需要根据用户输入实时返回匹配结果。核心痛点是"输入频率高导致请求泛滥",解决方案是通过防抖控制请求时机,配合缓存减少重复请求。

    2.1 核心技术点

    1. 防抖控制:用户输入停止300ms后再发起请求,避免输入过程中频繁请求;
    2. 结果缓存:相同关键词的搜索结果缓存到内存,短时间内重复输入无需再次请求;
    3. 空值处理:输入为空时清空搜索结果,避免无效请求;
    4. 请求中断:用户快速输入新关键词时,中断上一次未完成的请求,避免结果错乱。

    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 核心原理

    1. 文件分片:使用  Blob.slice()将文件分割为固定大小的分片(如5MB);
    2. 唯一标识:生成文件MD5作为唯一标识,确保后端能将分片关联到同一文件;
    3. 状态查询:上传前请求后端,获取已上传的分片索引,避免重复上传;
    4. 分片上传:并发上传未完成的分片,实时计算整体进度;
    5. 分片合并:所有分片上传完成后,请求后端合并为完整文件。

    4.2 完整代码实现(基于Axios)

  • 相关推荐

    热文推荐