
1. 实验14-16到底在练什么从“能写页面”到“能写应用”如果你的Web前端开发技术课程已经上到实验14我基本可以断定你已经熬过了最枯燥的HTML标签、CSS盒子模型和JavaScript基础语法马上就要进入这门课真正值钱的部分了。多数学校把前13个实验安排成“地基工程”——写一个静态网页、调一个浮动布局、用JS计算一个斐波那契数列、操作一下DOM节点。这些实验做完了你的感受往往是“我会写了但这些东西凑在一起能干嘛”实验14到16就是为了回答这个“能干嘛”而存在的。这三组实验和前面的最大区别在于两个字联动。页面上的按钮开始真正触发数据变化数据变化之后界面会自动更新刷新浏览器之后用户的操作还能被记住。简单说你要从一个“能画出页面的人”变成“能做出应用的人”。先说清楚这三组实验在整个课程体系里的定位。前端开发的核心能力可以拆成三块结构HTML、表现CSS、行为JavaScript。如果你前面的实验已经能解决结构和表现那么14-16几乎全部压在“行为”这一块上而且是行为里最难的三类异步请求、数据持久化、综合状态管理。具体来说实验14通常围绕“前端怎么和后端要数据”展开核心是AJAX、Fetch、JSON解析以及渲染数据时的DOM操作。实验15一般考“数据存哪儿”核心是localStorage、sessionStorage、Cookie这三兄弟的选型、读写和边界问题。实验16基本就是终极考核把事件绑定、DOM渲染、异步交互、本地存储串成一个完整的小应用比如待办清单、留言板、成绩管理系统。我见过太多同学在实验13之前顺风顺水到了14-16突然卡住卡的原因其实不是知识点难而是习惯了“写完就完了”的做题思维面对多环节协作时不知道先从哪下手。这篇文章我就按这套逻辑把三组实验的常见做法、必踩的坑、以及我当时是怎么一步步调通的全部拆给你看。2. 实验14异步数据交互——AJAX与Fetch的实操拆解2.1 题目通常长什么样核心考点在哪里实验14的题目版本繁多但骨架高度相似。最典型的一种是利用AJAX技术从服务器接口获取一组学生成绩数据JSON格式并将其渲染到页面表格中要求页面加载时显示“数据加载中”提示加载失败时显示错误信息同时需要提供一个“刷新数据”按钮允许用户重新请求。这种题看起来只有一句话实际上藏着四个考点异步请求是否写对、JSON数据是否被正确解析、渲染表格时是否用DOM操作而不是拼接字符串、有没有处理加载状态和异常情况。多数人丢分就丢在最后两项。老师让你写“加载中”提示不是为了让你多打几行字而是在考察你是否理解异步操作的基本体验——用户在等待期间必须有反馈请求失败时必须有兜底这是真实项目里最基本的要求。我见过很多同学直接用原生XHRXMLHttpRequest写一大坨回调也能跑通但在2025年的课程里我更建议你直接上Fetch理由下面细说。2.2 从XMLHttpRequest到Fetch到底该用哪个先看一段原生的XHR写法很多教材还在教这个function loadData() { const xhr new XMLHttpRequest(); xhr.open(GET, https://api.example.com/students, true); xhr.setRequestHeader(Content-Type, application/json); xhr.onreadystatechange function () { if (xhr.readyState 4) { if (xhr.status 200) { const data JSON.parse(xhr.responseText); renderTable(data); } else { showError(请求失败 xhr.status); } } }; xhr.send(); }这段代码没有任何语法问题但它的问题在于可读性差、嵌套多了之后难以维护。同样的功能用Fetch写清爽得多async function loadData() { const loadingEl document.getElementById(loading); const errorEl document.getElementById(error); loadingEl.style.display block; errorEl.style.display none; try { const response await fetch(https://api.example.com/students); if (!response.ok) { throw new Error(HTTP状态码异常 response.status); } const data await response.json(); renderTable(data); } catch (err) { errorEl.textContent 数据加载失败 err.message; errorEl.style.display block; } finally { loadingEl.style.display none; } }两者的核心区别在于XHR要求你手动判断readyState和status两层状态而Fetch用Promise把“网络请求”和“数据解析”拆成了两步再用async/await把代码拍扁。我的建议是如果课程没有强制要求XHR直接用Fetch。现在的浏览器兼容性已经完全不是问题了而且写出来的代码更接近工业界实际做法。注意Fetch有个新手必踩的坑——它只有在网络连接完全失败比如断网、域名解析失败时才会rejectHTTP 404、500这类状态码它照样resolve。所以你必须在拿到response之后手动检查response.ok。我见过不止一个同学漏掉这一步接口返回404页面却显示“请求成功”。2.3 解析JSON和渲染表格时的关键细节解析JSON看起来是JSON.parse或者response.json()一句话的事但数据结构和你的渲染逻辑必须匹配。比如接口返回的是这种包裹对象{ code: 0, message: success, data: [ { id: 1, name: 张三, score: 88 }, { id: 2, name: 李四, score: 75 } ] }前端代码就不能默认data直接是数组要先判断result.code 0再做渲染。这在实际项目里叫“和后端约定数据格式”考的就是你有没有这个意识。渲染表格的时候新手最常见的做法是拼HTML字符串let html ; for (const item of list) { html trtd item.name /tdtd item.score /td/tr; } document.getElementById(tbody).innerHTML html;这样能跑但有两个隐患。第一如果item.name里含HTML标签比如用户提交了img onerroralert(1)会被浏览器当成标签解析这就是XSS注入的雏形第二innerHTML每次赋值都会触发浏览器重新解析频繁大列表操作时性能不佳。我建议用DOM方法构建节点function renderTable(list) { const tbody document.getElementById(tbody); tbody.innerHTML ; // 先清空旧数据 const fragment document.createDocumentFragment(); list.forEach(item { const tr document.createElement(tr); const tdId document.createElement(td); tdId.textContent item.id; const tdName document.createElement(td); tdName.textContent item.name; const tdScore document.createElement(td); tdScore.textContent item.score; tr.appendChild(tdId); tr.appendChild(tdName); tr.appendChild(tdScore); fragment.appendChild(tr); }); tbody.appendChild(fragment); }用textContent赋值会自动转义HTML字符从根上避免了XSS问题用DocumentFragment组装多个节点只触发一次回流性能也好得多。这个习惯如果实验14就养成实验16的综合项目会受益很大。2.4 实验14常见报错排查速查表我整理了实验14里最高频的几个报错场景直接对照排查现象常见原因解决办法控制台报跨域CORS错误请求的接口域名与页面域名不一致且服务器未开启CORS改用老师提供的本地JSON文件或配置了CORS的代理接口页面一直显示“加载中”请求挂起或finally逻辑缺失检查接口地址是否能直接访问确认finally中关闭loading渲染出来是[object Object]对数组元素直接做了字符串拼接用JSON.stringify调试确认数据结构后按字段取值点击刷新按钮页面报错按钮click事件绑定时函数被立即执行检查是否写了btn.onclick loadData()应该写btn.onclick loadData数据出现但表格错位渲染前没有清空tbody旧数据每次渲染前执行tbody.innerHTML 这里第4个问题我多说一句。btn.onclick loadData()和btn.onclick loadData的区别是前者把loadData的返回值赋给onclick按钮点击时什么都不发生后者才是把函数本身交给按钮点一下执行一次。这个细节几乎每届学生都会有人栽写的时候多看一眼。3. 实验15本地存储实战——localStorage、sessionStorage与前端状态保持3.1 三种存储方案到底该选谁如果说实验14解决的是“程序运行时怎么拿数据”实验15解决的就是“浏览器关了再打开数据还在不在”。前端本地存储有三兄弟Cookie、localStorage、sessionStorage。先看一张对比表特性CookielocalStoragesessionStorage容量上限约4KB约5MB约5MB生命周期由Max-Age/Expires控制永久保存除非手动清除标签页关闭即清除是否随请求发送到服务器是否否访问方式document.cookielocalStorageAPIsessionStorageAPI使用场景会话标识、跨域携带少量数据用户偏好、草稿、购物车临时状态、页面间传值实验15的题目一般会让你做一个“用户偏好设置”或者“表单数据保存”的小功能。比如用户输入一段文字、勾选一个颜色主题点击保存后刷新页面内容还在。这种场景用localStorage是完全正确的因为数据量小、不需要发给服务器、而且要求持久保存。3.2 实验15的关键实现序列化与反序列化localStorage的API本身只有五个方法getItem、setItem、removeItem、clear、key。但有个最核心的坑它只能存字符串。你存一个数字18取出来也是字符串18你存一个对象直接setItem(user, {name: 张三})取出来只会是[object Object]。所以要存对象必须先JSON.stringify读取时再JSON.parse。这是实验15的绝对核心也是评分点。我推荐一个通用的封装写法// storage.js —— 一个极简的工具函数集 const storage { get(key, defaultValue) { try { const raw localStorage.getItem(key); return raw null ? defaultValue : JSON.parse(raw); } catch (e) { return defaultValue; // 解析失败时返回默认值避免页面崩掉 } }, set(key, value) { localStorage.setItem(key, JSON.stringify(value)); }, remove(key) { localStorage.removeItem(key); }, clear() { localStorage.clear(); } };注意get方法里的try...catch如果某一次存进去的数据格式坏了或者用户手动在控制台改坏了JSON.parse会抛异常整个页面跟着崩。加上这层保护至少能保证页面正常渲染最多就是取到默认值——这个处理方式在真实项目里叫“防御性编程”实验里写出来很加分。3.3 实验15必看的几个边界问题第一容量上限问题。localStorage的5MB看起来大但如果存的是循环引用的对象或者大量历史记录很快就满了。存满时setItem会抛QuotaExceededError。我的建议是在写入前判断一下数据规模或者每次写入都放在try...catch里满了之后给出“存储空间已满请清理后重试”的提示而不是页面无响应。第二不要存敏感信息。这是很多课程不会明说但老师在评分时会看的一点。学生的实验里经常有“记住登录状态”的功能于是有人把密码直接明文存进localStorage。这是绝对要避免的。localStorage明文数据可以被任何页面脚本读取没有隐私可言。实验报告里你只需要说明“密码等敏感信息不应存储于localStorage应存储在服务端session中”这句话能体现你的工程素养。第三页面刷新后的数据恢复时机。实验15会要求“刷新后数据自动回填”这时候最容易被忽略的是回填操作必须在DOM结构加载完之后执行。如果你把读取localStorage的代码放在head里的脚本中此时body里的输入框还没生成document.getElementById(name)拿到的是null代码直接报错。解决方法是把脚本放到body底部或者用DOMContentLoaded事件document.addEventListener(DOMContentLoaded, () { const savedNote storage.get(note, ); document.getElementById(noteInput).value savedNote; });3.4 实验15衍生场景多标签页同步如果实验15想拿高分可以主动做一个加分项多标签页同步。localStorage本身不触发跨页面通知但浏览器提供了storage事件当其他标签页修改localStorage时当前页会收到通知。监听它就能实现“两个标签页同时打开一边改数据另一边实时更新”的效果window.addEventListener(storage, (e) { if (e.key themeColor) { document.body.style.backgroundColor e.newValue; } });这个功能看着高级代码就几行放进实验报告里的“扩展功能”部分比你多写十个console.log都有用。4. 实验16综合项目实战——把事件、DOM、异步、存储全部打通4.1 实验16的项目怎么设计才不容易翻车实验16通常不再给特别细的步骤而是给一个完整需求让你自己实现。常见题目有待办事项管理、简易留言板、图书借阅管理系统、个人博客后台。不管题目是什么你需要主动划分出三层结构数据层、渲染层、交互层。数据层负责从localStorage读数据、写数据、增删改查。它不关心页面长什么样只负责维护数据。渲染层接收数据生成页面结构。它不关心数据从哪来只负责把数据显示出来。交互层监听页面事件调用数据层和渲染层完成联动。我见过太多实验16的代码是“一大坨”所有逻辑都堆在页面的script标签里状态变量散落各处一个函数里又做数据更新又做DOM操作又做事件绑定。这种代码别人看不懂你自己调bug的时候也痛苦。分层思想哪怕只是函数级别的分层是实验16最值得练习的东西。4.2 一个可复现的完整例子带持久化的待办事项应用下面我给你一个完整的参考实现功能是添加待办事项、标记完成、删除事项、刷新后数据不丢。代码不多但每一层都清晰。HTML结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 title实验16待办事项/title /head body div classapp h1待办事项/h1 div input typetext idtodoInput placeholder输入新的待办事项 / button idaddBtn添加/button /div ul idtodoList/ul /div script srcapp.js/script /body /htmlJavaScriptapp.js// 数据层所有对localStorage的读写都收口在这里 const store { TODOS_KEY: todos, getAll() { try { const raw localStorage.getItem(this.TODOS_KEY); const todos raw ? JSON.parse(raw) : []; return Array.isArray(todos) ? todos : []; } catch (e) { return []; } }, save(todos) { localStorage.setItem(this.TODOS_KEY, JSON.stringify(todos)); } }; // 渲染层把数据渲染成DOM function render(todos) { const listEl document.getElementById(todoList); listEl.innerHTML ; if (todos.length 0) { const li document.createElement(li); li.textContent 暂无待办事项添加一条吧; listEl.appendChild(li); return; } const fragment document.createDocumentFragment(); todos.forEach((todo, index) { const li document.createElement(li); const checkbox document.createElement(input); checkbox.type checkbox; checkbox.checked todo.done; checkbox.dataset.index index; const span document.createElement(span); span.textContent todo.text; // 已完成的事项加删除线样式 if (todo.done) { span.style.textDecoration line-through; span.style.color #999; } const deleteBtn document.createElement(button); deleteBtn.textContent 删除; deleteBtn.dataset.index index; li.appendChild(checkbox); li.appendChild(span); li.appendChild(deleteBtn); fragment.appendChild(li); }); listEl.appendChild(fragment); } // 交互层事件绑定和业务逻辑 let todos store.getAll(); // 页面加载时从存储中恢复 function addTodo() { const inputEl document.getElementById(todoInput); const text inputEl.value.trim(); if (!text) { alert(请输入待办内容); return; } todos.push({ text, done: false }); store.save(todos); render(todos); inputEl.value ; } function toggleTodo(index) { todos[index].done !todos[index].done; store.save(todos); render(todos); } function deleteTodo(index) { todos.splice(index, 1); store.save(todos); render(todos); } // 事件绑定 document.getElementById(addBtn).addEventListener(click, addTodo); document.getElementById(todoInput).addEventListener(keydown, (e) { if (e.key Enter) { addTodo(); } }); // 用事件委托统一处理checkbox和删除按钮 document.getElementById(todoList).addEventListener(click, (e) { const target e.target; if (target.type checkbox) { toggleTodo(Number(target.dataset.index)); } else if (target.textContent 删除) { deleteTodo(Number(target.dataset.index)); } }); // 初始渲染 render(todos);拆开看这个实现里有几个点值得你在实验报告里专门写一段说明第一为什么用事件委托如果给每个动态生成的checkbox和按钮都单独绑定事件每次render()都要重新绑定一次容易出现重复绑定或者绑定丢失。事件委托把监听器统一挂在父容器todoList上利用事件冒泡机制不管子节点是新是旧点击都能被统一处理。这就是真实项目中常用的做法。第二为什么用dataset.index而不是直接把todo对象存在事件里因为todos数组会变增删改而下标索引在每次渲染时都是最新对齐的。虽然简单但可以避免很多闭包陷阱。第三为什么每次操作后都立即store.save(todos)很多人习惯等到所有操作结束再统一保存结果一旦中间出错用户数据全丢。在状态变更的源头就地保存是最不容易出问题的策略。这也呼应了实验15学的localStorage实验16的核心就是真实地把实验15的内容用起来。4.3 联调时最容易忽略的细节实验16综合了太多知识点联调阶段常见的坑也是五花八门。我列几个我实际教学和改作业时反复看到的刷新按钮/提交按钮触发了表单提交。如果页面里有个form按钮在表单内部且没有设置typebutton点击后浏览器会执行表单提交、刷新页面。这是实验16第一杀手。解决方法要么给按钮加typebutton要么在submit事件里e.preventDefault()。初值读取时机不对。拿上面待办事项来说let todos store.getAll()必须放在render(todos)之前。如果你把读取放在事件绑定之后虽然通常也能跑但一旦渲染函数内部有依赖顺序错了就白屏。代码规范上我建议数据初始化 → 渲染 → 绑定事件这个顺序不要乱。修改数组后忘记重新渲染。todos.push(...)只是改了内存里的数组页面上的DOM不会自动更新。很多同学会纠结“为什么我加了数据页面没反应”这就是原因——你在交互层改了数据但没有调用render(todos)把新状态同步到页面。以后你学Vue、React会知道这是“单向数据流”的问题但现在用原生JS请记住每次数据变化手动执行一次渲染。5. 关于这三组实验我最想对你说的话写完这三组实验你在前端这条路上就算是真正入门了。回头看你可能会发现实验14让你懂得了“页面可以向别人要数据”实验15让你懂得了“页面可以记住自己的状态”实验16让你把它们拧成了一台能自己运转的小机器。这个转变比你会背十个API都重要。最后给你三个我自己的经验第一实验报告别只贴代码要写“为什么”。我改作业的时候最烦看到一份报告全是代码、没有任何说明。哪怕你只写一句“这里用事件委托是为了避免重复绑定”在老师眼里也是独立思考的体现。你踩过的每一个坑都应该写进报告的“遇到的问题”部分这不是暴露缺点这是展示成长过程。第二把浏览器开发者工具当成你的第二个屏幕。实验14调试接口看Network面板实验15看Application面板里的Local Storage实验16看Console报错和Sources断点。这三组实验如果你能不靠alert弹数据调试你就已经领先大部分同学了。按F12打开开发者工具Network、Console、Application这三个面板是前端调试的三块基石。第三别怕把代码推倒重来。做实验16的时候我见过太多同学在一个错误的方向上越走越远代码补丁叠补丁最后自己都看不懂。如果你发现代码已经乱到没法维护大胆从头开始花一小时重构比耗一下午在一堆烂代码里找bug划算得多。写代码这行删掉重来从来不是浪费时间它是成本最低的纠错方式。这三组实验做完去试试给自己写一个真正想用的小工具吧——记账本、学习打卡、电影收藏夹都行。当你写的代码真的能帮你解决一个生活里的小问题你才真正感受到前端的乐趣。