一、为什么这3天是"前端全栈能力"的关键突破?
职场前端开发绝非"孤芳自赏"的页面绘制------脱离后端数据的页面只是"空壳子"。用户注册后数据要存入数据库、商品列表要从服务器获取、登录状态要通过接口验证,这些核心场景都依赖"前后端数据交互"。很多新手卡在"接口调用失败""跨域问题解决不了""数据格式不匹配",核心是没掌握AJAX核心逻辑和接口对接规范。
这3天我们聚焦2个职场核心能力:1. 用AJAX(Fetch API)实现"请求发送-数据接收"全流程,解决跨域等常见问题;2. 掌握接口对接的"请求规范、数据解析、异常处理"技巧,让你的页面从"静态展示"升级到"动态数据交互",完全适配企业级开发需求。
二、Day16:AJAX核心------前后端交互的"通信协议"
1. 职场避坑:别再混淆"AJAX"和"Fetch"了
新手常听说"AJAX""Fetch""axios",却分不清三者关系:AJAX是"异步JavaScript和XML"的统称,是一种交互思想;Fetch API是浏览器原生提供的实现AJAX的接口(替代旧的XMLHttpRequest);axios是基于Fetch/XMLHttpRequest封装的第三方库。职场中推荐用Fetch(原生无依赖)或axios(封装更完善),避免直接写复杂的XMLHttpRequest。
核心认知:AJAX的核心是"异步请求"------发送请求后无需等待服务器响应,可继续操作页面,响应回来后再处理数据,避免页面卡顿。
2. 核心知识点:Fetch API实战四步曲
Fetch API是职场主流的原生请求方案,核心流程为"配置请求参数→发送请求→解析响应→处理数据",支持GET(获取数据)、POST(提交数据)等常见请求方式。
| 步骤 | 核心操作 | 职场代码示例 | 关键说明 |
|---|---|---|---|
| 1. 配置参数 | 定义请求地址、方法、请求头、请求体 | const options = { method: 'GET', headers: { 'Content-Type': 'application/json' } } | GET请求无请求体,POST需设置body |
| 2. 发送请求 | 调用fetch()方法发送请求,返回Promise对象 | fetch('https://api.example.com/data', options) | Promise状态:pending(请求中)→ resolved/rejected |
| 3. 解析响应 | 根据后端返回格式解析(JSON/文本/Blob) | .then(response => response.json()) | 常用response.json()(JSON格式)、response.text()(文本) |
| 4. 处理数据 | 处理成功数据或捕获请求异常 | .then(data => console.log(data)).catch(err => console.error(err)) | catch捕获网络错误或解析错误 |
3. 实战:GET请求获取数据+POST提交数据(对接职场基础接口需求)
需求:1. 用GET请求获取后端用户列表数据,渲染到页面表格;2. 用POST请求提交新增用户表单数据到后端,成功后刷新列表;3. 处理请求中、请求成功、请求失败三种状态的视觉反馈。
<div style="max-width: 800px; margin: 30px auto; padding: 0 20px;"> <form id="addUserForm" style="border: 1px solid #eee; padding: 20px; border-radius: 8px; margin-bottom: 30px;"> <div style="display: flex; gap: 20px; margin-bottom: 15px;"> <div style="flex: 1;"> <label style="display: block; margin-bottom: 5px; font-size: 14px;">姓名:</label> <input type="text" id="userName" name="userName" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" placeholder="请输入姓名" required> </div> <div style="flex: 1;"> <label style="display: block; margin-bottom: 5px; font-size: 14px;">手机号:</label> <input type="text" id="userPhone" name="userPhone" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" placeholder="请输入手机号" required> </div> </div> <button type="submit" id="submitBtn" style="padding: 8px 16px; border: none; border-radius: 4px; background: #42b983; color: #fff; cursor: pointer; font-size: 14px;">新增用户</button> </form> <div id="loading" style="display: none; text-align: center; padding: 50px 0;">加载中...</div> <div id="errorMsg" style="display: none; padding: 10px; background: #fef2f2; border-radius: 4px; color: #b42323; margin-bottom: 20px;"></div> <table style="width: 100%; border-collapse: collapse; border: 1px solid #eee;"> <thead> <tr style="background: #f9f9f9;"> <th style="padding: 12px; text-align: left; border-bottom: 1px solid #eee;">ID</th> <th style="padding: 12px; text-align: left; border-bottom: 1px solid #eee;">姓名</th> <th style="padding: 12px; text-align: left; border-bottom: 1px solid #eee;">手机号</th> <th style="padding: 12px; text-align: left; border-bottom: 1px solid #eee;">创建时间</th> </tr> </thead> <tbody id="userTableBody"> </tbody> </table> </div> <script> // 获取元素 const addUserForm = document.getElementById('addUserForm'); const userNameInput = document.getElementById('userName'); const userPhoneInput = document.getElementById('userPhone'); const submitBtn = document.getElementById('submitBtn'); const loading = document.getElementById('loading'); const errorMsg = document.getElementById('errorMsg'); const userTableBody = document.getElementById('userTableBody'); // 后端接口地址(实际开发中替换为真实接口) const API_BASE_URL = 'https://mock.apifox.cn/m1/2644446-0-default/api'; // 1. 显示加载状态 function showLoading() { loading.style.display = 'block'; errorMsg.style.display = 'none'; // 禁用表单提交按钮,防止重复提交 submitBtn.disabled = true; submitBtn.textContent = '提交中...'; } // 2. 隐藏加载状态 function hideLoading() { loading.style.display = 'none'; submitBtn.disabled = false; submitBtn.textContent = '新增用户'; } // 3. 显示错误信息 function showError(msg) { errorMsg.style.display = 'block'; errorMsg.textContent = msg; } // 4. GET请求:获取用户列表 async function getUserList() { showLoading(); try { // 1. 发送GET请求(默认就是GET,可省略method配置) const response = await fetch(`${API_BASE_URL}/users`, { headers: { 'Content-Type': 'application/json', // 职场中常用Token进行身份验证(从本地存储获取) 'Authorization': `Bearer ${localStorage.getItem('token') || ''}` } }); // 2. 检查响应状态(200-299为成功) if (!response.ok) { throw new Error(`请求失败:${response.status} ${response.statusText}`); } // 3. 解析JSON格式响应数据 const data = await response.json(); // 4. 验证后端返回的数据结构(职场必备:防止后端数据异常) if (!Array.isArray(data.data)) { throw new Error('后端返回数据格式错误,预期为数组'); } // 5. 渲染用户列表 renderUserList(data.data); } catch (err) { // 捕获所有异常(网络错误、响应错误、数据解析错误) showError(`获取用户列表失败:${err.message}`); console.error('GET请求异常:', err); } finally { // 无论成功失败,都隐藏加载状态 hideLoading(); } } // 5. POST请求:新增用户 async function addUser(userData) { showLoading(); try { // 1. 发送POST请求(需指定method和body) const response = await fetch(`${API_BASE_URL}/users`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token') || ''}` }, // 重要:POST请求体需转成JSON字符串(和本地存储类似) body: JSON.stringify(userData) }); if (!response.ok) { // 解析后端返回的错误信息(职场常见:后端会返回具体错误原因) const errData = await response.json(); throw new Error(errData.message || `请求失败:${response.status}`); } const data = await response.json(); alert('新增用户成功!'); // 刷新用户列表 getUserList(); // 重置表单 addUserForm.reset(); } catch (err) { showError(`新增用户失败:${err.message}`); console.error('POST请求异常:', err); } finally { hideLoading(); } } // 6. 渲染用户列表到表格 function renderUserList(users) { if (users.length === 0) { userTableBody.innerHTML = '<tr><td colspan="4" style="padding: 20px; text-align: center; border-bottom: 1px solid #eee;">暂无用户数据</td></tr>'; return; } let tableHtml = ''; users.forEach(user => { // 格式化时间(职场常用:处理后端返回的时间戳或ISO时间) const createTime = new Date(user.createTime).toLocaleString(); tableHtml += ` <tr> <td style="padding: 12px; border-bottom: 1px solid #eee;">${user.id}</td> <td style="padding: 12px; border-bottom: 1px solid #eee;">${user.name}</td> <td style="padding: 12px; border-bottom: 1px solid #eee;">${user.phone}</td> <td style="padding: 12px; border-bottom: 1px solid #eee;">${createTime}</td> </tr> `; }); userTableBody.innerHTML = tableHtml; } // 7. 表单提交事件 addUserForm.addEventListener('submit', async function(e) { e.preventDefault(); // 阻止默认提交 // 前端基础验证(和第五篇表单验证逻辑衔接) const userName = userNameInput.value.trim(); const userPhone = userPhoneInput.value.trim(); const phoneReg = /^1[3-9]\d{9}$/; if (!userName) { showError('姓名不能为空'); return; } if (!phoneReg.test(userPhone)) { showError('请输入有效的手机号'); return; } // 组装提交数据(和后端接口字段对应) const userData = { name: userName, phone: userPhone }; // 调用POST请求新增用户 await addUser(userData); }); // 8. 页面加载时获取用户列表 window.addEventListener('load', getUserList); </script>
职场接口请求规范拆解:
-
身份验证:通过请求头`Authorization`携带Token(从本地存储获取,第五篇登录功能中存储),这是职场接口安全的基本要求,避免匿名访问;
-
异常处理:用`try/catch`捕获所有异常(网络错误、响应状态错误、数据格式错误),并显示具体错误信息,方便问题排查;
-
防重复提交:请求期间禁用提交按钮,避免用户多次点击导致重复提交,这是职场高频优化点;
-
数据校验:前端先做基础验证(如手机号格式),再发送请求,同时后端返回数据后验证格式,双重保障数据安全。
三、Day17-18:接口对接进阶------解决职场"复杂交互+跨域"问题
1. 职场认知:接口对接的核心痛点是什么?
基础请求能实现后,职场中还会遇到更复杂的场景:跨域请求被浏览器拦截、需要上传文件、接口需要携带Cookie、请求需要中断(如切换页面时取消未完成的请求)。其中跨域问题是新手最常踩的坑,必须彻底解决。
2. 核心知识点:跨域问题+复杂请求解决方案
(1)跨域问题:是什么?为什么?怎么解?
跨域:当前端页面地址(协议、域名、端口)与后端接口地址不一致时,浏览器出于安全考虑会拦截请求(同源策略限制)。比如前端`http://localhost:5500`请求后端`https://api.example.com`就会跨域。
| 解决方案 | 实现方式 | 职场适用场景 | 优缺点 |
|---|---|---|---|
| CORS(后端配置) | 后端在响应头添加`Access-Control-Allow-Origin: *`(允许所有域名)或指定域名 | 企业生产环境(推荐) | 优点:标准方案,安全可靠;缺点:需后端配合配置 |
| 代理服务器(前端配置) | 开发环境用Vite/Webpack配置代理,将请求转发到后端(如Vite的proxy配置) | 前端开发调试阶段 | 优点:前端独立解决,无需后端配合;缺点:仅适用于开发环境 |
| JSONP | 利用script标签不受同源策略限制,动态创建script发送请求 | 兼容旧浏览器(极少用) | 优点:兼容低版本浏览器;缺点:仅支持GET请求,不安全 |
(2)复杂请求:文件上传+请求中断
职场中除了普通的JSON请求,还会遇到文件上传(如头像上传)和请求中断(如用户快速切换标签页)场景,需掌握对应的Fetch API用法。
| 场景 | 核心技术点 | 职场代码示例 |
|---|---|---|
| 文件上传 | 用FormData对象封装文件数据,请求头无需手动设置Content-Type(浏览器自动设置为multipart/form-data) | const formData = new FormData(); formData.append('avatar', fileInput.files[0]); fetch(url, { method: 'POST', body: formData }) |
| 请求中断 | 用AbortController创建信号,关联到请求配置中,调用abort()方法中断请求 | const controller = new AbortController(); fetch(url, { signal: controller.signal }); controller.abort(); |
3. 实战:文件上传+跨域代理+请求中断(对接职场复杂接口需求)
需求:1. 实现头像上传功能,支持预览和文件类型/大小验证;2. 开发环境配置Vite代理解决跨域问题;3. 实现"取消上传"按钮,中断正在进行的上传请求;4. 上传进度显示(实时展示上传百分比)。
<!-- 1. 页面结构 --> <div style="max-width: 500px; margin: 30px auto; padding: 0 20px;"> <div style="text-align: center; margin-bottom: 30px;"> <div id="avatarPreview" style="width: 120px; height: 120px; border-radius: 50%; border: 2px dashed #eee; margin: 0 auto; overflow: hidden; cursor: pointer;"> <img src="" alt="头像预览" style="width: 100%; height: 100%; object-fit: cover; display: none;"> <span style="line-height: 120px; color: #999;">点击上传头像</span> </div> <input type="file" id="avatarInput" accept="image/jpg,image/png,image/jpeg" style="display: none;"> </div> <div style="display: flex; gap: 10px; justify-content: center;"> <button id="uploadBtn" style="padding: 8px 16px; border: none; border-radius: 4px; background: #42b983; color: #fff; cursor: pointer; font-size: 14px;">开始上传</button> <button id="cancelBtn" style="padding: 8px 16px; border: none; border-radius: 4px; background: #ff4d4f; color: #fff; cursor: pointer; font-size: 14px; display: none;">取消上传</button> </div> <div style="margin-top: 20px; display: none;" id="progressContainer"> <div style="display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 14px;"> <span>上传进度</span> <span id="progressPercent">0%</span> </div> <div style="height: 8px; width: 100%; background: #f5f5f5; border-radius: 4px; overflow: hidden;"> <div id="progressBar" style="height: 100%; width: 0; background: #42b983; transition: width 0.3s ease;"></div> </div> </div> <div id="resultMsg" style="margin-top: 20px; padding: 10px; border-radius: 4px; display: none;"></div> </div> <!-- 2. JavaScript代码 --> <script> // 获取元素 const avatarPreview = document.getElementById('avatarPreview'); const avatarInput = document.getElementById('avatarInput'); const uploadBtn = document.getElementById('uploadBtn'); const cancelBtn = document.getElementById('cancelBtn'); const progressContainer = document.getElementById('progressContainer'); const progressBar = document.getElementById('progressBar'); const progressPercent = document.getElementById('progressPercent'); const resultMsg = document.getElementById('resultMsg'); // 接口地址(开发环境用相对路径,配合代理) const UPLOAD_API = '/api/avatar/upload'; // AbortController实例(用于中断请求) let uploadController = null; // 1. 头像预览 avatarPreview.addEventListener('click', () => { avatarInput.click(); // 触发文件选择 }); avatarInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; // 验证文件类型和大小(职场安全规范) const allowedTypes = ['image/jpg', 'image/png', 'image/jpeg']; if (!allowedTypes.includes(file.type)) { showResult('错误:仅支持JPG、PNG格式图片', 'error'); return; } if (file.size > 5 * 1024 * 1024) { // 5MB showResult('错误:图片大小不能超过5MB', 'error'); return; } // 预览图片 const imgUrl = URL.createObjectURL(file); const img = avatarPreview.querySelector('img'); img.src = imgUrl; img.style.display = 'block'; avatarPreview.querySelector('span').style.display = 'none'; // 重置结果提示 showResult('', ''); }); // 2. 开始上传 uploadBtn.addEventListener('click', async () => { const file = avatarInput.files[0]; if (!file) { showResult('请先选择要上传的头像', 'error'); return; } // 创建FormData封装文件数据 const formData = new FormData(); formData.append('avatar', file); // 'avatar'为后端接收的字段名 // 创建AbortController(用于取消上传) uploadController = new AbortController(); const signal = uploadController.signal; // 显示进度条和取消按钮 progressContainer.style.display = 'block'; cancelBtn.style.display = 'inline-block'; uploadBtn.disabled = true; uploadBtn.textContent = '上传中...'; try { // 发送上传请求(带进度监听) const response = await fetch(UPLOAD_API, { method: 'POST', body: formData, signal, // 关联中断信号 headers: { 'Authorization': `Bearer ${localStorage.getItem('token') || ''}` } }); if (!response.ok) { const errData = await response.json(); throw new Error(errData.message || '上传失败'); } const data = await response.json(); showResult(`上传成功!头像地址:${data.data.avatarUrl}`, 'success'); // 保存头像地址到本地存储(和第五篇数据持久化衔接) localStorage.setItem('userAvatar', data.data.avatarUrl); } catch (err) { // 区分是取消上传还是其他错误 if (err.name === 'AbortError') { showResult('上传已取消', 'warning'); } else { showResult(`上传失败:${err.message}`, 'error'); } } finally { // 重置状态 uploadController = null; progressContainer.style.display = 'none'; cancelBtn.style.display = 'none'; uploadBtn.disabled = false; uploadBtn.textContent = '开始上传'; } }); // 3. 取消上传 cancelBtn.addEventListener('click', () => { if (uploadController) { uploadController.abort(); // 中断请求 } }); // 4. 显示结果提示 function showResult(msg, type) { resultMsg.style.display = msg ? 'block' : 'none'; resultMsg.textContent = msg; if (type === 'error') { resultMsg.style.background = '#fef2f2'; resultMsg.style.color = '#b42323'; } else if (type === 'success') { resultMsg.style.background = '#f0f9eb'; resultMsg.style.color = '#52c41a'; } else if (type === 'warning') { resultMsg.style.background = '#fffbe6'; resultMsg.style.color = '#faad14'; } } // 5. 页面加载时回显已上传的头像(和本地存储衔接) window.addEventListener('load', () => { const savedAvatar = localStorage.getItem('userAvatar'); if (savedAvatar) { const img = avatarPreview.querySelector('img'); img.src = savedAvatar; img.style.display = 'block'; avatarPreview.querySelector('span').style.display = 'none'; } }); </script> <!-- 3. 开发环境跨域代理配置(Vite示例) --> <!-- vite.config.js 文件 --> <script id="viteConfig" lang="javascript"> import { defineConfig } from 'vite'; export default defineConfig({ server: { // 配置代理,解决跨域问题 proxy: { // 匹配以/api开头的请求 '/api': { target: 'https://mock.apifox.cn/m1/2644446-0-default', // 后端真实接口地址 changeOrigin: true, // 开启跨域 rewrite: (path) => path.replace(/^\/api/, '') // 重写路径:去掉/api前缀 } } } }); </script>
4. 职场复杂接口逻辑拆解
-
跨域解决方案落地:开发环境用Vite代理配置,将`/api`前缀的请求转发到后端真实地址,前端代码中用相对路径请求,无需关注跨域细节;生产环境由后端配置CORS响应头,前后端配合无缝衔接;
-
文件上传核心:用`FormData`对象封装文件,浏览器会自动设置`Content-Type: multipart/form-data`,无需手动配置,这是职场文件上传的标准写法;
-
请求中断实现:用`AbortController`的信号关联请求,点击取消按钮时调用`abort()`,避免无效请求占用资源,提升页面性能;
-
用户体验优化:上传前验证文件类型和大小、实时显示上传进度、区分不同结果提示(成功/错误/取消),这些细节直接影响产品体验评分。
四、3天总结:职场能力落地清单
-
能熟练使用Fetch API实现GET/POST请求,掌握"请求配置-响应解析-异常处理"全流程,理解异步请求的核心逻辑;
-
能独立解决跨域问题(开发环境代理+生产环境CORS),掌握文件上传、请求中断等复杂接口场景的实现技巧;
-
能对接后端接口规范,实现"数据获取-表单提交-文件上传"等完整业务流程,理解Token身份验证的实现逻辑;
-
完成作业:基于本文代码,添加"头像裁剪"功能(用cropper.js库),裁剪后再上传;同时实现"多文件上传"功能,支持批量选择图片并显示各自上传进度。