做大学生HTML期末大作业这件事,我太有发言权了。"HTML+CSS+JavaScript购物商城"这个题,看似平平无奇,但只要你把框架搭建对了、交互逻辑写清楚了,它就是一份能拿高分的综合性作业——因为它几乎覆盖了前端入门阶段所有核心知识点:语义化标签、CSS布局与美化、DOM操作、事件处理、数据管理。
我刚带过一个学弟做类似的商城项目,他最初的想法是"搞个高大上的后台管理系统",结果在登录注册和权限控制里耗了两周,基础功能反而没做完。后来我们重新把需求收敛成"前端展示型商城",三天就搞定了主体,后面两天打磨细节和准备答辩。这篇博文就把这个完整过程拆给你看,从页面结构到交互逻辑,从常见报错到答辩话术,保证每一步你都能照着做。哪怕你现在连div和span都分不清,也能把这套项目啃下来。
1. 商城项目的整体设计思路与需求拆解
1.1 期末作业的评分点到底在哪
很多同学拿到这个题目,第一反应就是"要做得越复杂越好"。其实大部分老师的评分标准非常朴素,核心就几个维度。
第一是页面完整度。有没有首页、商品列表、商品详情、购物车这些商城必备模块。第二是技术覆盖度。HTML部分有没有用到语义化标签,CSS部分有没有展示布局能力(Flex、Grid、浮动、定位),JavaScript部分有没有事件交互和数据操作。第三是交互体验。轮播图能不能自动播放、手动切换,加入购物车后角标数字会不会变,商品能不能按价格排序。第四是代码规范性。缩进是否整齐、命名是否有意义、CSS和JS是否分离到独立文件。
想清楚这四点,你就明白根本不需要做什么真实支付、用户登录,更不需要后端数据库。你要交的是一个"前端综合演示项目",核心是让老师看到你对HTML/CSS/JS三件事的掌握程度。
1.2 用建房子来理解三层架构
我给学生讲这个项目时,最高效的类比是"建房子"。
HTML是毛坯房,负责把房子的骨架搭出来。客厅在哪儿、卧室在哪儿、墙上要留窗户的位置,都是它来定。对应到网页上,就是头部导航区、轮播图区、商品列表区、底部信息区这些结构。
CSS是装修队。毛坯房能住吗?能,但是没人愿意住。装修队负责铺地板、刷墙漆、摆家具,让空间变得好看好用。对应到网页上,就是颜色、字体、间距、阴影、圆角、动画这些视觉表现。
JavaScript是水电工和智能家居系统。房子装得再好看,灯不亮、水不通也没法住。JS负责让网页"活"起来:点击按钮有反应、数据能计算、界面能更新。
这个类比一定要记牢,因为不管是你自己想清楚怎么做,还是答辩时向老师解释思路,这套说法都特别加分。
1.3 文件目录结构怎么组织最加分
我不建议把所有代码堆在一个index.html文件里。哪怕是一份期末作业,也让老师看出你有"工程意识"。推荐用下面的结构:
code复制shopping-mall/
├── index.html
├── css/
│ ├── reset.css
│ └── style.css
├── js/
│ └── main.js
└── images/
├── banner1.jpg
├── banner2.jpg
├── product1.jpg
└── ...
index.html放页面结构,css目录放样式,js目录放交互逻辑,images目录放图片素材。CSS和JS单独拆出来,既符合"结构、样式、行为三者分离"的前端核心思想,也方便你调试——哪一块出问题了,直接去对应文件里找,不用在一个上千行的HTML文件里来回翻。
至于reset.css和style.css的分工,后面我会详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用HTML搭建商城骨架:页面结构与语义化
2.1 基础框架:head区域不能马虎
很多同学写HTML,最不重视的就是head区域,直接一个title就往里扔。实际上head区域写得好不好,直接影响页面在不同设备上的表现和老师的第一印象。
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>晨曦商城 - 大学生购物平台</title>
<meta name="description" content="晨曦商城,专注校园好物的在线购物平台">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/style.css">
</head>
这里有几个关键点。Charset声明为UTF-8,防止中文乱码。viewport这个meta标签是移动端适配的命根子——不加它,你页面在手机上打开就会像被缩小了看一样。description虽然不是必须的,但写上能让页面更规范。
2.2 页面整体布局:从上到下的模块划分
一个典型的商城首页从上到下依次是:顶部导航栏、轮播图Banner、商品分类导航、商品列表、底部页脚。用HTML语义化标签来表达就是:
html复制<body>
<header class="header">
<!-- 顶部导航 -->
</header>
<main class="main">
<section class="banner" id="banner">
<!-- 轮播图 -->
</section>
<section class="categories">
<!-- 分类导航 -->
</section>
<section class="products" id="products">
<!-- 商品列表 -->
</section>
</main>
<footer class="footer">
<!-- 底部信息 -->
</footer>
<!-- 购物车弹窗 -->
<div class="cart-modal" id="cartModal">
...
</div>
<script src="js/main.js"></script>
</body>
header、main、section、footer这些标签不是随便用的,它们比"清一色div"更有语义。打个比方,如果网页是一篇文章,div相当于一个没有任何标记的普通段落,而header、footer这类标签则是一眼能看出"这是标题""这是落款"的规范结构。搜索引擎和浏览器对语义化页面的识别度更高,答辩时也能跟老师说"我使用了HTML5语义化标签来增强页面的可读性和可维护性"。
有个细节需要注意:script标签放到body最后,是因为JavaScript要去操作DOM元素,如果脚本放在head里先执行,此时页面元素还没加载出来,就会报"找不到元素"的错。放到body末尾,保证HTML全部渲染完成后再执行JS逻辑。
2.3 导航栏:让页面有"层级感"
导航栏是用户进入页面后第一个看到的东西,结构上要清爽明朗。
html复制<header class="header">
<div class="header-inner">
<h1 class="logo">晨曦<span>商城</span></h1>
<nav class="nav">
<ul class="nav-list">
<li class="active"><a href="#banner">首页</a></li>
<li><a href="#products">全部商品</a></li>
<li><a href="#categories">分类</a></li>
<li><a href="#footer">关于我们</a></li>
</ul>
</nav>
<div class="cart-area">
<button class="cart-btn" id="cartBtn">
购物车<span class="cart-badge" id="cartCount">0</span>
</button>
</div>
</div>
</header>
这里我用logo文字来替代图片logo,因为很多同学找不到合适的图片素材。实际上"晨曦商城"这个示例名你可以随便换成"XX商城""校园优选",用文字当logo既清晰又不用处理图片版权问题。
导航里的a标签href分别指向对应区块的id,比如href="#products"点击后页面会平滑滚动到商品列表区域。这就是锚点跳转,是HTML原生就支持的功能,记得给目标section设置id就行。
2.4 商品列表:用ul/li还是用div?
商品列表是最考验HTML功底的地方。我的建议是使用ul/li组合。不是说div不行,而是ul/li在语义上更准确——商品列表本质就是一个"列表",用ul包裹能让结构更清晰。
html复制<section class="products" id="products">
<h2 class="section-title">热门商品</h2>
<ul class="product-list">
<li class="product-card">
<a href="#" class="product-link" data-id="1">
<img src="images/product1.jpg" alt="无线蓝牙耳机">
</a>
<h3 class="product-name">无线蓝牙耳机</h3>
<p class="product-desc">蓝牙5.3,续航30小时</p>
<div class="product-bottom">
<span class="product-price">¥199</span>
<button class="add-btn" data-id="1" data-name="无线蓝牙耳机" data-price="199">加入购物车</button>
</div>
</li>
<!-- 更多商品卡片 -->
</ul>
</section>
每个商品卡片内部包含:图片、名称、描述、价格、加购按钮。写的时候注意给按钮加上data-id、data-name、data-price这些自定义属性,方便后面JavaScript取值。这个设计一定要提前想好,不然写到JS部分你会发现自己拿不到商品信息,还得回头改HTML。
我见过很多同学图省事,把商品信息直接用静态文字写死在页面上,结果到写"搜索功能"时发现完全没法做,只能重写代码。正确的做法是:在JavaScript里准备一个商品数据数组,页面渲染用JS动态生成,这样搜索、排序、筛选都能轻松实现。前面HTML里写的商品卡片其实可以非常少,甚至只写一个空容器,等JS渲染。具体方案在第4章我会详细展开。
3. CSS样式系统:把商城做得好看的关键
3.1 为什么需要reset.css:浏览器默认样式的坑
第一次学CSS的同学经常会困惑:为什么我没写任何样式,h1的字号那么大?为什么body默认有margin?为什么ul前面自带圆点?
这些"自动出现"的东西叫做浏览器默认样式。问题是,不同浏览器的默认样式不一致。Chrome的默认margin和Firefox可能不一样。为了让项目在主流浏览器里表现一致,我们需要先把这些默认样式重置掉。
css复制/* reset.css 简约版 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
ul, ol {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
img {
display: block;
max-width: 100%;
}
button {
border: none;
background: none;
cursor: pointer;
font: inherit;
}
这里用到了通配符选择器*。它的优点是能一次选中所有元素,快速清零全局margin和padding。但也有缺点:性能上,它会遍历页面上所有元素,如果页面特别大,会有微小的性能开销。不过对于期末作业这种量级的页面,完全不用担心。真正需要注意的是*包含哪些元素——它只会匹配CSS加载时已经存在的元素,不会匹配伪元素,所以::before、::after的margin/padding还需要单独设置。
3.2 布局选型:Flex还是Grid?
购物商城这类页面,最核心的布局有两大类:横向排列和网格排列。
横向排列(导航栏、商品信息行)强烈建议用Flexbox。它的核心逻辑是"在一条轴线上分配空间",非常适合做一维布局。比如让logo在左、导航在中间、购物车在右,Flex一行代码就能搞定:
css复制.header-inner {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 15px 20px;
}
justify-content: space-between会让三个子元素分别靠左、居中分布,自动把剩余空间推到中间;align-items: center让它们在交叉轴(垂直方向)上居中。max-width: 1200px配合margin: 0 auto是经典的定宽居中法,让页面在大屏上不会摊得太满。
商品列表这种多行多列的布局,用Grid更合适。Grid的二轴布局天生就是为网格设计的:
css复制.product-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
repeat(4, 1fr)表示四列等宽,1fr是比例单位,四列均分剩余空间。gap: 20px让卡片之间有20px的间距,不需要再单独给每张卡片写margin。这一套组合拳下来,商品列表的骨架就搭好了,而且能自适应容器宽度。
不建议用float来做整体布局了。float本来是设计用来实现文字环绕图片效果的,不是为页面整体布局而生的。用它布局会遇到清除浮动、父元素高度塌陷等各种问题,属于自找麻烦。CSS2时期因为Flex和Grid还没普及,大家才用float硬撑。现在时代变了,直接用现代布局方案。
3.3 商品卡片的质感:细节决定是不是"用心做了"
商品卡片是商城页面的主角,它的精致程度直接决定了老师对整份作业的印象分。提升质感最简单有效的手段有四个。
第一个是盒阴影。给卡片加一个浅浅的、略带扩散的阴影,立刻就有悬浮感:
css复制.product-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.product-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
这里box-shadow的四个值分别是:水平偏移0、垂直偏移2px、模糊半径12px、颜色rgba(0,0,0,0.08)。rgba中的a是透明度,0.08表示8%不透明度,很淡,看着不脏。hover时卡片上移6像素、阴影加深变大,鼠标一放上去就有"被托起来"的反馈感。
第二个是过渡动画transition。上面代码里的transition: transform 0.3s ease, box-shadow 0.3s ease,意思是对transform和box-shadow两个属性做0.3秒的渐变过渡。没有它,hover效果会瞬间切换,生硬;有了它,卡片会平滑上移,细腻多了。
第三个是圆角。现代设计风格里圆角是无处不在的。卡片12px,按钮8px,小标签4px,这些细节能柔和整个页面的气质。但注意别把什么都搞成大圆角,那种风格只适合卡通类设计,商城页面太圆反而显得不专业。
第四个是删除线原价。在价格行里同时展示现价和原价,原价加删除线,会让"折扣"的视觉冲击力很强:
css复制.original-price {
text-decoration: line-through;
color: #aaa;
font-size: 13px;
margin-left: 8px;
}
3.4 Banner轮播图:做个渐变氛围
轮播图区域不用复杂,很多同学想着"左侧小图、右侧大图、上下箭头、底部圆点指示器",做是做出来了,但样式非常粗糙。我建议把重点放在视觉氛围上。
一个稳妥的方案是:用一张宽幅背景图铺底,叠加一层渐变遮罩,让文字信息浮在上面,导航圆点在右下角,左右两侧各有一个半圆形箭头按钮。核心CSS:
css复制.banner {
position: relative;
height: 400px;
overflow: hidden;
}
.banner-slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.6s ease;
}
.banner-slide.active {
opacity: 1;
}
.banner-slide img {
width: 100%;
height: 100%;
object-fit: cover;
}
多张轮播图叠放在一起,默认全部透明,当前显示的类名带active的设为可见。切换就是通过JS改变active类的归属。用透明度的渐入渐出比左右滑动更好实现,视觉上也更柔和。
position: relative和absolute是CSS里最常见的"父子定位"组合。父元素relative定位作为参照系,子元素absolute脱离文档流后按父元素的位置来定自己的位置。这个组合在弹窗、轮播、角标里都会用到。
3.5 响应式:加三行媒体查询
期末作业不需要做到完美响应式,但通过浏览器窗口大小变化来展示自适应能力,是答辩时的加分亮点。最省钱的做法就是给商品列表加媒体查询:
css复制@media (max-width: 768px) {
.product-list {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.product-list {
grid-template-columns: 1fr;
}
.banner {
height: 250px;
}
}
含义很直白:屏幕宽度小于768px时,商品从4列变2列;小于480px时变1列。再加上viewport meta标签的设置,手机打开页面也能看。这就够用了,别贪多。
4. JavaScript交互功能:让商城"活"起来
4.1 商品数据管理:一切交互的基础
前面我提到过,不要在HTML里硬编码所有商品卡片,那样搜索排序会很难实现。正确做法是把商品数据保存在JS数组里,通过JS渲染到页面上。这个思路是前端数据驱动界面的雏形,也是你作业中最能体现"编程思维"的部分。
javascript复制const products = [
{
id: 1,
name: "无线蓝牙耳机",
desc: "蓝牙5.3,续航30小时",
price: 199,
originalPrice: 259,
category: "数码",
img: "images/product1.jpg"
},
{
id: 2,
name: "运动水杯",
desc: "大容量便携,食品级材质",
price: 39,
category: "生活",
img: "images/product2.jpg"
},
// 放8~12个商品数据即可
];
数据准备好后,用JS生成商品卡片:
javascript复制const productList = document.querySelector('.product-list');
function renderProducts(data) {
productList.innerHTML = data.map(item => `
<li class="product-card">
<a href="#" class="product-link">
<img src="${item.img}" alt="${item.name}">
</a>
<h3 class="product-name">${item.name}</h3>
<p class="product-desc">${item.desc}</p>
<div class="product-bottom">
<span class="product-price">¥${item.price}</span>
<span class="original-price">¥${item.originalPrice}</span>
<button class="add-btn" data-id="${item.id}">加入购物车</button>
</div>
</li>
`).join('');
}
renderProducts(products);
ES6的模板字符串(反引号)允许我们把HTML结构直接嵌在JS里,配合${}插值语法填充商品数据。map方法遍历数组返回新的数组,再用join('')把数组元素拼成一个完整的HTML字符串。这样页面上一开始就有完整的商品列表了。
需要注意的是innerHTML虽然方便,但如果数据里包含用户输入的内容,会有XSS风险。不过期末作业的商品数据是自己写死的,不存在安全风险,用innerHTML完全没有问题。
4.2 购物车逻辑:数组+对象管理状态
购物车是商城项目最核心的交互。数据模型用数组,每个元素是一个对象:
javascript复制let cart = [];
function addToCart(productId) {
const product = products.find(p => p.id === productId);
const existingItem = cart.find(item => item.id === productId);
if (existingItem) {
existingItem.count += 1;
} else {
cart.push({ ...product, count: 1 });
}
renderCart();
updateCartBadge();
}
find方法在数组里查找符合条件的第一个元素。先看看商品是否存在,如果购物车里已经有了同款,就把数量加1,否则作为新条目push进购物车。展开运算符...product把商品对象的所有字段拷贝一份,再额外加一个count字段表示数量。
购物车弹窗的渲染逻辑:
javascript复制const cartModal = document.getElementById('cartModal');
const cartList = document.getElementById('cartList');
function renderCart() {
if (cart.length === 0) {
cartList.innerHTML = '<p class="empty-cart">购物车空空如也,快去逛逛吧</p>';
return;
}
cartList.innerHTML = cart.map(item => `
<li class="cart-item" data-id="${item.id}">
<img src="${item.img}" alt="${item.name}">
<div class="cart-item-info">
<h4>${item.name}</h4>
<span>¥${item.price} × ${item.count}</span>
</div>
<div class="cart-item-control">
<button class="minus-btn" data-id="${item.id}">-</button>
<span>${item.count}</span>
<button class="plus-btn" data-id="${item.id}">+</button>
<button class="remove-btn" data-id="${item.id}">删除</button>
</div>
</li>
`).join('');
}
function updateCartBadge() {
const total = cart.reduce((sum, item) => sum + item.count, 0);
document.getElementById('cartCount').textContent = total;
}
reduce方法用来累加:把每个商品的数量加起来,得到购物车总件数,更新到导航栏的角标上。角标数字变化是这个项目最直观的反馈,一定不能漏。
计算总价也很简单:
javascript复制function getTotalPrice() {
return cart.reduce((sum, item) => sum + item.price * item.count, 0);
}
4.3 localStorage:刷新页面数据不丢失
如果你的购物车数据只存在JS变量里,刷新页面就回到空车状态。用localStorage可以把数据持久化到浏览器里,刷新后数据还在。实现非常简单:
javascript复制// 保存
function saveCart() {
localStorage.setItem('cart', JSON.stringify(cart));
}
// 读取
function loadCart() {
const saved = localStorage.getItem('cart');
if (saved) {
cart = JSON.parse(saved);
}
}
// 每次更新购物车后都调用 saveCart()
// 页面加载时调用 loadCart()
localStorage只能存字符串,所以存之前要用JSON.stringify把数组变成字符串,取出来时再用JSON.parse还原成数组。这是新手最容易漏的一步——有人存了之后取的直接是个字符串,然后调用cart.find报错说不是函数,百思不得其解。
在页面加载时,先调用loadCart()恢复数据,再执行renderCart()和updateCartBadge()把界面刷新好。注意这两个渲染函数,还可以考虑再做一层防御:item.originalPrice可能是undefined,显示的时候写¥undefined就尴尬了,所以模板里用条件判断或者先给数据一个缺省值。
4.4 事件绑定:事件委托解决动态元素难题
事件绑定是这个项目里最容易出问题的环节。如果你直接用:
javascript复制document.querySelector('.add-btn').addEventListener('click', ...)
大概率会翻车。原因有两类:一是页面里有很多个.add-btn,querySelector只能选中第一个;二是如果商品列表是JS动态渲染出来的,初始化事件绑定的时候这些按钮根本还没存在,绑定就成了空绑。
解决这两个问题的最佳方案是事件委托。原理很简单:事件有冒泡机制,点击子元素时事件会一层层往上冒泡到父元素。我们把监听器绑定在父元素上,然后判断触发事件的target是不是我们关心的那个按钮:
javascript复制document.querySelector('.product-list').addEventListener('click', function(e) {
const addBtn = e.target.closest('.add-btn');
if (addBtn) {
const id = parseInt(addBtn.dataset.id);
addToCart(id);
}
});
e.target是事件真正的触发源。closest方法会从触发源开始向上找,看是否存在匹配选择器的祖先元素。如果找到了.add-btn,就说明用户点击了加购按钮。这样不管列表是静态的还是后来动态渲染的,事件都能正常响应。这是实战中非常高频的技巧,答辩时值得单独提。
购物车弹窗内的加减、删除按钮同理,给弹窗列表容器绑一个事件委托。
4.5 轮播图:setInterval+索引切换
轮播图的核心逻辑有两种。一种是用transform: translateX横向平移,一种是我前面说的opacity切换。这里讲透明切换版的最简实现:
javascript复制const slides = document.querySelectorAll('.banner-slide');
const dots = document.querySelectorAll('.dot');
let currentIndex = 0;
let timer = null;
function showSlide(index) {
slides.forEach((slide, i) => {
slide.classList.toggle('active', i === index);
});
dots.forEach((dot, i) => {
dot.classList.toggle('active', i === index);
});
currentIndex = index;
}
function nextSlide() {
const next = (currentIndex + 1) % slides.length;
showSlide(next);
}
function startAutoPlay() {
timer = setInterval(nextSlide, 3000);
}
function stopAutoPlay() {
clearInterval(timer);
}
// 左右按钮
document.getElementById('prevBtn').addEventListener('click', () => {
showSlide((currentIndex - 1 + slides.length) % slides.length);
stopAutoPlay();
startAutoPlay();
});
document.getElementById('nextBtn').addEventListener('click', () => {
nextSlide();
stopAutoPlay();
startAutoPlay();
});
// 圆点切换
dots.forEach((dot, i) => {
dot.addEventListener('click', () => showSlide(i));
});
startAutoPlay();
注意取模运算%的用武之地:(currentIndex + 1) % slides.length会让索引在0、1、2、0、1、2之间循环,实现从头开始的自动轮播。鼠标移入轮播图时可以停掉自动播放,移出再恢复,这个交互细节很显用心。
轮播图有了自动播放和手动切换,整个页面的动态感就起来了。
5. 常见问题与调试技巧实录
5.1 页面样式全乱了:八成是Reset没写
症状是页面一打开,所有元素之间都有奇怪的间距,列表前面有默认圆点,a标签带着下划线蓝色。原因就是没有引入reset.css,浏览器默认样式在作祟。
解决方法是把reset.css写好,并且在index.html的head里用link引入。引入顺序也重要:reset.css在前,style.css在后,这样后面的主样式才能覆盖底层重置。如果你发现自己的样式不生效,检查是不是主样式文件写在了reset前面——后加载的样式会覆盖先加载的,顺序反了,你的自定义样式就会先被覆盖掉一次。
另一个常见原因是CSS选择器优先级问题。比如你写了.product-card .product-name { color: red; },但另一个地方又写了.product-name { color: blue; },前者优先级更高,后者失效。排查优先级问题时,最简单的办法是浏览器F12打开开发者工具,选中那个元素,右侧样式面板会显示所有正在生效的样式规则,被划掉的说明被更高优先级覆盖了。
5.2 JS报错"xxx is not a function"或"无法读取属性"
这类报错几乎都是两个原因:变量名写错,或者对象不是你以为的那个类型。
比如getElementById('cart')返回null,后面调用cart.addEventListener就会报"无法读取null的属性"。原因通常是HTML里元素的id不是cart,或者script标签放在了元素前面导致脚本执行时元素还没加载。
"xxx is not a function"则多半是把undefined当函数调用。比如localStorage取出来的数据是字符串,你直接调用cart.find,字符串没有find方法,就报错了。解决方法是先console.log(cart)看类型,确认是数组后再调用。
我强烈建议你在代码里多打console.log调试。比如在addToCart函数里,先console.log(productId)再console.log(find查到的product),一步步确认数据流走到哪一步断掉了。前端调试完全没有魔法,就是靠这招"沿着数据流打日志"。
5.3 图片不显示:路径永远是老坑
图片不显示可能的原因很多。最常见的是路径错误。用相对路径时,index.html在根目录,图片在images目录下,应该写images/xx.jpg而不是./images/xx.jpg了,但如果你把相对路径从另一个目录里的HTML文件引用,就要按那个目录来规划。
建议在项目的根目录下建一个测试用的临时HTML文件,再对照浏览器的Network面板看到底是哪个图片请求404了。开发者工具的Network(网络)面板能看到每个资源的加载状态,红色的就是失败项,点开能看到具体的请求URL,对比一下就知道路径哪里写错了。
另一个坑是图片格式问题。有些图片素材后缀是.webp或.avif,浏览器对格式的支持有差异,老师电脑上如果用的是旧版浏览器,可能就显示不出来。稳妥起见统一用jpg或png,图片尺寸控制在几百KB内,太大加载太慢也会造成"白屏很久然后突然出现"的效果。
5.4 加了localStorage后,旧购物车数据污染新逻辑
这是个很隐蔽的问题。你在本地调试时往localStorage里存过购物车数据,后来改了商品数据结构(比如把price改成了newPrice),再刷新页面时loadCart读出来的是旧数据结构的数组,页面就显示异常甚至报错。
解决方法是在调试阶段经常清理localStorage。在浏览器控制台执行localStorage.clear()一键清空。或者在loadCart函数里加一个数据校验,发现字段对不上就重置:
javascript复制function loadCart() {
const saved = localStorage.getItem('cart');
if (saved) {
try {
cart = JSON.parse(saved);
if (!Array.isArray(cart)) cart = [];
} catch (e) {
cart = [];
}
}
}
try...catch能把JSON.parse解析失败的情况兜住,避免整个脚本崩溃。这个防御式编程的思想,在答辩时提出来会显得你有工程意识。
5.5 最终自检清单
提交之前,对照这份清单过一遍,能避免很多不必要的扣分:
- 所有页面标题是否统一规范
- 三个核心技术栈是否都有明确体现
- 图片能否正常显示
- 轮播图能否自动播放、手动切换
- 加入购物车后角标是否更新
- 购物车弹窗能否正常增删改查
- 页面在窄屏下是否可读
- 代码缩进和命名是否统一
我个人在实际操作里的体会是,这份作业最值钱的不是代码量,而是"每块代码都能回答出为什么"。你为什么用Grid不用Flex?因为你了解两者分别适合什么场景。你为什么用事件委托?因为你知道动态渲染的元素不能直接绑事件。老师随便挑一个问题,你能接住,高分就跑不掉。
最后再分享一个小技巧:交作业前把CSS文件里的颜色值统一抽到CSS变量里,比如:root { --primary-color: #ff6700; --text-color: #333; }。这不会让你的页面外观有任何变化,但当你展示代码时,这一段会让老师觉得你关注到了模块化设计。好的项目不需要炫技,把基础做扎实,把每一步的原理说清楚,就已经是优秀了。
