期末作业做购物商城,几乎是每个学过 HTML、CSS、JavaScript 的同学都要经历一遍的事。我这两年帮不少学弟学妹看过这类项目,也模拟当过几次“严苛答辩评委”,一个很直观的感受是:很多人把时间花在套模板、换图片、堆页面上,结果一问“购物车怎么实现的”“为什么用事件委托”,当场卡壳。其实这个题目不复杂,它考的就是你有没有把三件套配合起来,做出一个流程完整的网页应用。
这篇文章想把“大学生HTML期末大作业——HTML+CSS+JavaScript购物商城”一次说透。从需求梳理到 HTML 骨架,从 CSS 布局和视觉效果到 JavaScript 的购物车、搜索、轮播逻辑,再到提交前常见的坑和调试技巧,都会讲到。无论你是零基础想照着做一个,还是已经写完想排查问题,都能找到直接能用的方法。
1. 动手之前想明白:这题到底在考什么
1.1 核心不是页面数量,而是流程闭环
很多同学觉得购物商城就是“首页长得像商城”。于是从网上拖一个模板下来,改个标题、换几张图片,五个页面凑齐就交了。这种作业最大的问题不是代码烂,而是功能是断的:首页的“加入购物车”按钮点了没有任何反应,购物车页面是空表格,结算按钮点了只是刷新页面。老师打开一看,十个按钮九个不能点,分数自然不高。
真正的购物商城至少要跑通一条完整的用户路径:浏览商品 → 点击商品加入购物车 → 在购物车中修改数量或删除 → 看到合计金额 → 填写结算表单 → 提交后给出反馈。不需要复杂的后端,不需要登录注册,只要这六步在页面上是连贯的,就比一堆漂亮但没逻辑的静态页面强太多。
所以动手之前先列一个功能清单,哪怕只是写在纸上:
- 顶部导航:logo、搜索框、购物车入口
- 首页核心区:轮播图、商品列表
- 商品列表:每个商品有图、名称、价格、加入购物车按钮
- 购物车区域:展示已加商品、支持数量加减和删除、实时计算总额
- 结算表单:需要对输入内容做基础的非空校验
- 提交反馈:点击提交后给出“下单成功”之类的提示
评分维度一般也就这几点:结构是否清晰、样式是否规范、交互是否合理、代码是否可读、答辩能不能讲清楚。页面数量反而不是最关键的,三四个功能完整的页面好过十个硬凑出来的页面。
1.2 三件套分工:别让 JavaScript 干 CSS 的活
有些代码我一眼就能看出作者没理解三件套的分工:HTML 里塞了一堆 style 属性,CSS 用 display:none 控制业务逻辑,JS 又用 alert 代替界面反馈。结果就是“功能其实有,但代码没法看”。
用一句大白话记住:HTML 是毛坯房,它负责把房间隔出来;CSS 是装修,负责刷墙、铺地板、布置家具位置;JavaScript 是水电和物业,负责让灯能开、水能流、门铃能响。
对应到购物商城项目:
- HTML 只负责结构:页面有哪些区块、商品卡片里有什么内容、表单包含哪些字段
- CSS 只负责视觉:布局、间距、颜色、字体、hover 效果
- JavaScript 只负责行为:点击按钮后购物车数量变了、搜索框过滤商品列表、轮播图自动切换
违反这个分工的典型表现是:用表格布局页面、用 onclick 直接在标签里写代码、用 CSS 强行拼出交互效果。期末作业可以不优雅,但至少要让答辩老师看出你理解每一层该干什么。
1.3 先定功能清单,再碰代码
做购物商城建议先画一个简单结构图,不用画得多专业,纸上写清楚都行。这个动作能帮你避免“写一步看一步”,越写越乱。
我个人推荐的页面结构是:单页应用(Single Page Application)思路。也就是说,一个 index.html 页面里分成几个模块,用 JavaScript 控制显示哪个模块。比如顶部导航有“首页”“购物车”“结算”三个入口,点击后通过切换区块的 display 属性来换内容。
这种做法的好处有三点。第一,你只需要处理一个页面的样式和脚本,逻辑集中,不容易出现多页面之间数据无法共享的问题。第二,期末答辩时演示操作非常流畅,因为你不需要来回跳转页面。第三,数据放内存里,购物车直接用一个对象处理,比跨页面传参简单得多。
别觉得“单页应用”这个说法很高端,其实底层就是 display:none 和 display:block 的切换。先把功能闭环跑通,比纠结页面数量重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. HTML 骨架:把头部、语义化和表单写规范
2.1 页面头部的五件事,一件都别漏
很多同学复制作业时会把 head 里的一大堆 meta 标签全部删掉,只留一个 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>XX购物商城</title>
<meta name="description" content="XX购物商城——精选好物,在线选购">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- 页面内容 -->
<script src="js/main.js"></script>
</body>
</html>
DOCTYPE 的作用是告诉浏览器用标准模式渲染,不写的话页面容易进入怪异模式,盒模型、边距计算全都会不一样。html 标签上的 lang="zh-cn" 表示页面主要内容是简体中文,浏览器翻译和屏幕阅读器都会用到。charset 必须放在 head 靠前的位置,否则中文大概率乱码。
viewport 标签很多人忽略,但它直接影响你用手机浏览器演示时的效果。没有它,移动端会把整个页面按 PC 宽度缩放,字小得看不清。加上之后配合 CSS 的响应式写法,手机演示会舒服很多。
还要说一句:新手写 HTML 经常标签不闭合、属性不加引号,或者中文符号混进代码里。写完后把代码结构折叠一下,看看有没有明显的标签缺失,再在浏览器里按 F12 看 Console 有没有错误,能省很多时间。
2.2 用语义化标签划分区域,别从头到尾都是 div
“一个 div 走天下”是很多作业的通病:整个页面从上到下全是 div,class 名从 box1 取名到 box30。这样写不是不能运行,但老师看着累,你自己后期维护也累。
购物商城的页面区域其实非常固定,用语义化标签套上去就清晰了:
html复制<header>
<div class="logo">XX商城</div>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#mall">全部商品</a></li>
<li><a href="#cart">购物车</a></li>
</ul>
</nav>
</header>
<main>
<section id="home">
<h2>热门推荐</h2>
<!-- 商品列表会丢到这里 -->
</section>
<section id="mall">
<h2>全部商品</h2>
<!-- 商品卡片区 -->
</section>
<section id="cart">
<h2>购物车</h2>
<!-- 购物车表格 -->
</section>
</main>
<footer>
<p>页脚信息:联系方式、版权说明</p>
</footer>
header 放顶部导航,nav 放导航链接,main 放页面主体内容,section 表示一个独立内容区块,footer 放底部信息。这套结构一摆出来,哪怕样式还没写,别人也能看出你页面有哪些部分。
商品列表我建议用 ul > li 而不是一堆裸 div。理由很简单:商品列表本质上是一组并列条目,ul/li 天然表达“一组东西”的语义。给 li 加 class 设置成卡片样式,里面再放图片、标题、价格、按钮,结构层次会更清楚。
2.3 商品卡片、购物车和结算表单怎么组织
一个商品卡片的标准 HTML 大概是这样的:
html复制<ul class="goods-list">
<li class="goods-card" data-id="1">
<img src="images/goods-1.jpg" alt="无线蓝牙耳机">
<h3 class="goods-name">无线蓝牙耳机</h3>
<p class="goods-price">¥199.00</p>
<button type="button" class="btn-add">加入购物车</button>
</li>
</ul>
几个细节要注意:button 一定要写 type="button",因为 button 在表单里默认是 submit 类型,点了会提交表单并刷新页面,这是“点击按钮页面刷新”最常见的元凶之一。data-id 用于在 JavaScript 里拿到商品编号,这样点击事件里就知道用户加的是哪个商品。
购物车部分用表格展示会很清楚:
html复制<table class="cart-table">
<thead>
<tr>
<th>商品</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody id="cartBody">
<!-- JS 渲染购物车行 -->
</tbody>
</table>
<p>合计:<span id="cartTotal">¥0.00</span></p>
购物车是典型的数据表格,用 table 反而比 div 布局语义更准确。thead 放表头,tbody 放动态生成的商品行,比用 div 拼容易理解。
结算表单控制在十个字段以内就够了:收货人姓名、手机号、收货地址、备注。手机号用 tel 类型,地址用 text 类型加一个 select 选择省份,样式简单但看起来很完整。所有输入项在 JavaScript 里做非空校验,能跑通“填错提示 → 填对提交 → 提交成功”这条链路就算合格。
3. CSS 样式:从“能看”到“加分”
3.1 引样式表的方式,作业和项目都推荐外链
CSS 有三种引用方式:行内样式、内嵌样式、外部样式表。行内样式就是直接在标签里写 style 属性,内嵌样式是在 head 里写 style 标签,外部样式表通过 link 标签引入单独的 css 文件。
三种方式对比:
| 方式 | 写法 | 优缺点 |
|---|---|---|
| 行内 | <div style="color:red"> |
方便但难以复用,代码混乱 |
| 内嵌 | <style>...</style> |
适合单页,但多页无法共用 |
| 外链 | <link rel="stylesheet" href="css/style.css"> |
推荐,结构清晰、方便统一维护 |
期末作业强烈建议用外链。你把 css 文件夹和 js 文件夹一拆,文件结构是:
code复制project/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── main.js
└── images/
├── goods-1.jpg
└── banner-1.jpg
这个结构本身就是加分项。老师扫一眼文件就能看出你有没有工程意识,比答辩时费劲解释高效得多。
引入 link 的 href 用相对路径,比如 css/style.css,意思是“当前 HTML 文件同级的 css 目录下的 style.css”。如果你从别处复制代码,图片路径也要一起复制,不然图片全裂。
3.2 布局实战:Flex 和 Grid 怎么选
布局是 CSS 的重头戏。老式做法是 float 加手动 clear 清浮动,表格布局是更早的笨办法,现在主流就是 Flexbox 和 Grid 两种。
Flexbox 适合一维排列,要么横着排,要么竖着排。导航栏、按钮组、商品卡片内部的对齐基本都是 Flex 的事。比如商品列表要一行放四个卡片:
css复制.goods-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.goods-card {
width: 220px;
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
flex-wrap: wrap 表示一行放不下自动换行,gap: 20px 统一控制间距。不需要手动算 margin,也不需要用 nth-child 去处理边距,这是 Flex 最实用的一点。goods-card 再用 flex-direction: column 把内容变成纵向排列,align-items: center 让子元素居中。
Grid 适合二维布局,页面的整体区域或者商品网格这种“行列都固定”的场景用它更直观:
css复制.goods-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
repeat(4, 1fr) 的意思是重复四列,每列均分剩余空间。你不需要关注每个卡片的宽度,列会自动计算。对比一下:如果页面里只有一行卡片,用 Flex;如果有明确的网格行列结构,用 Grid。这个选型标准到今天也没过时。
3.3 居中、字体和文字位置,一次说清
购物商城里最多的样式问题,其实就是“为什么我的内容不在我想让它待的位置”。我给你按场景拆开讲。
第一,文字居中。text-align: center 只对文本和行内元素有效,它不能把块元素居中。想让按钮里的字居中,直接在按钮上写 text-align: center 加 line-height 等于按钮高度就行。
第二,整块内容居中。一个容器想水平居中,用 margin: 0 auto,但这个容器必须有明确宽度,比如 width: 1200px。没有宽度的话 auto 无法计算两侧间距。
第三,让整个页面的登录区域或结算卡片在屏幕中央。这是最高频的“body 居中”问题:
css复制.login-wrapper {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
min-height: 100vh 让这个容器至少占满浏览器可视高度,display: flex 激活弹性布局,justify-content: center 水平居中,align-items: center 垂直居中。这一套是老牌解决方案,至今仍然好用。
第四,容器里文本位置怎么调整。文字在容器里太靠上,一般是 line-height 不够或者容器 padding 有问题。比如一个按钮高度 40px,想让文字垂直居中,可以写 line-height: 40px。多行文本则用 padding 控制上下间距,配合 flex 的 align-items: center 更好使。
文字垂直位置还有一个小知识点:vertical-align 只对同一行内的行内元素和表格单元格生效。你想把图片和文字对齐,比如购物车里的商品缩略图和商品名,可以在 img 上写 vertical-align: middle。它不是用来垂直居中整个块的。
字体本身也值得花一点时间。font-family 定义字体系列;font-size 控制大小;font-weight 控制粗细,normal 是 400,bold 是 700。购物商城价格可以用红色加粗突出。
渐变字体是个低成本加分项,适合做 logo 或标题:
css复制.logo {
background-image: linear-gradient(90deg, #ff6a00, #ee0979);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
background-clip: text 让背景只显示在文字内部,color: transparent 把默认文字颜色变成透明,视觉上就出现了渐变字。注意背景图是作用于元素背景,所以文字需要透明才能露出渐变。这个效果放在 logo 上很亮眼,期末作品一下子没那么素了。
3.4 三个小效果:hover、涟漪光圈和梯形标签
作业想拿高分,除了布局规整,还要有点“设计感”。不用做得太花哨,三个基础效果足够了。
第一个是鼠标移入商品卡片时卡片轻轻上浮加阴影:
css复制.goods-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.goods-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
transition 指定哪些属性变化时要做动画,鼠标移入时卡片往上移动 4 像素,同时阴影变深。看起来就像卡片被“提起来”,成本低效果好。
第二个是按钮点击时的涟漪光圈扩散效果。很多人看到“涟漪”两个字觉得很难,其实核心代码不长:
css复制.btn-ripple {
position: relative;
overflow: hidden;
}
.btn-ripple::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.5s ease-out, opacity 0.5s ease-out;
}
.btn-ripple:active::after {
transform: translate(-50%, -50%) scale(10);
opacity: 0;
}
实现原理是利用伪元素画一个圆形,点击时 scale 放大并让透明度归零,形成一个向外扩散又消失的光圈。position: relative 给伪元素定位基准,overflow: hidden 防止光圈跑出按钮边界。
第三个是梯形标签,适合做“限时秒杀”“新品首发”这类角标。所谓梯形,其实不是真的画梯形,而是把一个矩形用 transform 变换切割出来:
css复制.tag {
display: inline-block;
padding: 6px 16px;
background: #ff6a00;
color: #fff;
transform: skewX(-20deg);
border-radius: 4px 4px 0 0;
}
.tag span {
display: inline-block;
transform: skewX(20deg);
}
skewX(-20deg) 是让元素沿 X 轴倾斜 20 度,视觉上就成了梯形。里面的文字会被一起拉斜,所以用内层 span 再反向 skewX(20deg) 把文字扶正。这是很经典的“斜标签”实现思路。
不过我要提醒一句:动画和特效是给内容加分的,不是主角。如果每个按钮都在转、每张图都在跳、颜色乱七八糟,答辩老师只会觉得你审美失控。挑两三个效果集中做,比遍地开花效果好。
3.5 选择器优先级和 * 通配符
CSS 里最容易被问到但总有人答不清楚的,就是选择器优先级。记一条规则:!important > 行内样式 > id 选择器 > class 选择器 > 标签选择器 > 通配符 *。
| 类型 | 示例 | 权重 |
|---|---|---|
| !important | color: red !important; | 最高 |
| 行内样式 | style="color:red" | 1000 |
| id | #nav | 100 |
| class/属性/伪类 | .nav / [type] / :hover | 10 |
| 标签/伪元素 | div / ::after | 1 |
| 通配符 | * | 0 |
很多人对 * 选择器有疑问,因为它能一次性给所有元素设样式,特别方便。比如:
css复制* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
这段代码把页面初始边距清零、统一盒模型,确实能解决很多浏览器默认样式的干扰。但 * 的缺点是:它会匹配页面上所有元素,如果里面写太多复杂样式,浏览器要为每个元素计算一次,性能会受影响。更大风险是覆盖问题,比如你明明只给特定容器设置了 padding,全局 * 的规则可能把局部规则搅乱。
期末项目建议:可以把 margin、padding、box-sizing 这三样放在 * 里做基础清零,其他属性不要往上堆。想设置某个区域的通用样式时,用无前缀的标签选择器如 main、section 去写,避免全局污染。
4. JavaScript:把购物流程跑通
4.1 商品数据用数组和对象维护,别写死在 HTML 里
到这一步,页面的壳已经做好了,接下来最关键的环节是让数据流动起来。新手常犯的错误是把商品硬编码在 HTML 里,比如把十个商品卡片一个个写在页面上,然后 JavaScript 又要从这些卡片中到处找数据。这种做法的痛点是:改一个商品价格要改 HTML 和 JS 两处,加一个商品又要复制一大段代码,逻辑稍微复杂一点就开始失控。
正确的做法是用数组加对象维护商品数据,再用 JavaScript 渲染到页面。商品列表本质是一个数据集合,每个商品就是一个对象。
javascript复制const products = [
{ id: 1, name: '无线蓝牙耳机', price: 199, img: 'images/goods-1.jpg' },
{ id: 2, name: '智能手环', price: 149, img: 'images/goods-2.jpg' },
{ id: 3, name: '便携充电宝', price: 89, img: 'images/goods-3.jpg' }
];
这个数组就是你的“数据库”。要加商品,往数组里塞一个对象就行,页面会自动生成。渲染代码可以这样写:
javascript复制const goodsList = document.querySelector('.goods-list');
function renderGoods() {
let html = '';
products.forEach(item => {
html += `
<li class="goods-card" data-id="${item.id}">
<img src="${item.img}" alt="${item.name}">
<h3 class="goods-name">${item.name}</h3>
<p class="goods-price">¥${item.price.toFixed(2)}</p>
<button type="button" class="btn-add">加入购物车</button>
</li>
`;
});
goodsList.innerHTML = html;
}
renderGoods();
这里我们用到了对象、数组的 forEach 遍历、模板字符串、以及 toFixed(2) 保留两位小数。整个流程把 HTML 生成交给 JavaScript,而不是手动写死每个商品。以后再增加商品、做搜索筛选,都只需要操作 products 这个数组,非常轻量。
如果还不太熟悉数组和对象,把这段代码拆开看:products.forEach 就是遍历数组中的每个商品,item 代表当前商品对象;模板字符串允许我们直接把 item.name 的值插进 HTML 片段里。这个“数据驱动视图”的思路是前端开发最核心的心智模型,做一次购物车就懂了。
4.2 购物车:增删改查和事件委托
购物车功能是整个项目的重头戏。它需要四件事:加入购物车、修改数量、删除商品、计算总价。逻辑不复杂,但代码组织如果太乱,写着写着就会晕。
我推荐用一个对象来充当购物车,对象的键是商品 id,值是数量:
javascript复制const cart = {};
function addToCart(id) {
cart[id] = (cart[id] || 0) + 1;
renderCart();
}
这行 cart[id] = (cart[id] || 0) + 1 是购物车逻辑里最经典的写法。如果这个 id 还没有出现过,cart[id] 是 undefined,undefined || 0 就等于 0,加 1 后变成 1;如果已经加过,就拿到原数量再加 1。一句话搞定“首次添加”和“重复添加”两种情况。
渲染购物车需要遍历 cart 对象:
javascript复制function renderCart() {
const cartBody = document.querySelector('#cartBody');
let html = '';
let total = 0;
Object.entries(cart).forEach(([id, count]) => {
const product = products.find(p => p.id === Number(id));
if (!product) return;
const subtotal = product.price * count;
total += subtotal;
html += `
<tr>
<td>${product.name}</td>
<td>¥${product.price.toFixed(2)}</td>
<td>
<button class="btn-minus" data-id="${id}">-</button>
<span>${count}</span>
<button class="btn-plus" data-id="${id}">+</button>
</td>
<td>¥${subtotal.toFixed(2)}</td>
<td><button class="btn-del" data-id="${id}">删除</button></td>
</tr>
`;
});
cartBody.innerHTML = html;
document.querySelector('#cartTotal').textContent = '¥' + total.toFixed(2);
}
Object.entries 把购物车对象变成可遍历的数组,每一项是 [id, count]。用 find 从商品数组里查到商品信息,然后计算小计和总价。这段代码同时用到了对象、数组、函数、模板字符串,几乎把 JavaScript 基础语法全过了一遍,答辩时也特别好讲。
难点在于,页面上商品列表和购物车的按钮都是动态生成的,怎么给它们绑定事件?如果给每个按钮加监听,代码会很多,而且每次渲染完要重新绑定,容易造成事件重复绑定。正确的方案是事件委托:利用事件冒泡,把监听挂到父容器上,然后通过目标元素判断用户点的是谁。
javascript复制document.querySelector('.goods-list').addEventListener('click', (e) => {
const btn = e.target.closest('.btn-add');
if (!btn) return;
const card = btn.closest('.goods-card');
const id = card.dataset.id;
addToCart(Number(id));
});
e.target 是真正被点击的元素,closest('.btn-add') 会向父级查找最近的匹配元素。如果点的是加入购物车按钮,btn 就是那个按钮;如果点的其他地方,btn 是 null,函数直接返回。这种写法最大的好处是:就算之后再往列表里加一百个商品,也不需要重新绑定事件。
购物车区域同样用事件委托,通过 data-action 区分加减和删除:
javascript复制document.querySelector('.cart-box').addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (!btn) return;
const id = Number(btn.dataset.id);
const action = btn.dataset.action;
if (action === 'plus') {
cart[id] = (cart[id] || 0) + 1;
} else if (action === 'minus') {
cart[id]--;
if (cart[id] <= 0) delete cart[id];
} else if (action === 'del') {
delete cart[id];
}
renderCart();
});
数量减到 0 的时候直接 delete,把这条商品从购物车对象里移除,这样总价计算也会自动排除它。使用 data-action 属性让按钮职责一目了然,比在 class 名里做判断清晰。
4.3 搜索筛选、轮播图、一键返回顶部
购物车跑通之后,再补三个常用交互就算完整了。第一个是搜索框筛选商品,第二个是首页轮播图,第三个是回到顶部按钮。
搜索框的输入事件要用 input 而不是 change。change 在输入框失焦时才触发,input 是每敲一个字符就触发。搜索体验当然是即时触发:
javascript复制const searchInput = document.querySelector('#searchInput');
searchInput.addEventListener('input', () => {
const keyword = searchInput.value.trim().toLowerCase();
const filtered = products.filter(p => p.name.toLowerCase().includes(keyword));
renderGoods(filtered);
});
filter 方法把数组中符合条件的商品筛出来,includes 判断商品名是否包含关键词。这里有个小细节:先把关键词和商品名都转成小写,这样搜索时不会因为中文和英文大小写出错。renderGoods 函数需要改造一下,允许传入一个自定义商品数组,如果不传就默认全部商品。
轮播图的实现思路也很经典。准备一个数组存放图片地址,用一个变量记录当前页码,左右按钮切换页码并重绘图片,再加一个定时器自动切换。
javascript复制let current = 0;
const bannerImgs = ['images/b1.jpg', 'images/b2.jpg', 'images/b3.jpg'];
function showBanner() {
document.querySelector('#bannerImg').src = bannerImgs[current];
}
function nextBanner() {
current = (current + 1) % bannerImgs.length;
showBanner();
}
function prevBanner() {
current = (current - 1 + bannerImgs.length) % bannerImgs.length;
showBanner();
}
current = (current + 1) % bannerImgs.length 是个经典写法。current 从 0 加到 2 以后,再 +1 变成 3,取模 3 得到 0,自动回到第一张。prevBanner 加了个小心机:先加上数组长度再取模,避免 current 变成负数导致数组越界。定时器用 setInterval(nextBanner, 3000),记得在页面隐藏时清除,但这个对期末项目可以不做,能跑就行。
一键返回顶部算法比较简单,核心是 window.scrollTo:
javascript复制function goTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
top: 0 表示滚动到页面最上方,behavior: 'smooth' 是平滑滚动。老浏览器不认 smooth 参数,可以退而求其次写成 document.documentElement.scrollTop = 0,但现代浏览器基本都支持 smooth。回到顶部的按钮可以加上显示时机:滚轮滚动超过 300 像素才显示,在首页顶部时不打扰用户。
4.4 BOM 和调试辅助的小技巧
BOM 指的是浏览器对象模型,简单说就是 window 这一层提供的 API。window.scrollTo、window.location.reload()、window.setTimeout、window.alert 都是 BOM 的一部分。期末答辩时老师很爱问“location 和 history 用过吗”,所以最好提前看一下。
比如结算提交成功后,想模拟“重新加载页面”的效果,可以用:
javascript复制function submitOrder() {
// 校验表单逻辑...
alert('下单成功');
location.reload();
}
location.reload() 就是刷新页面。这在小项目中用来模拟“提交后重置”很好用,不需要去学后端接口。再比如自动跳转页面可以用 location.href = 'https://...',这也是 BOM 的经典用法。
调试方面说一个实用技巧:在浏览器控制台里模拟用户输入。比如你想测搜索框,但不想手动敲键盘,可以直接执行:
javascript复制const input = document.querySelector('#searchInput');
input.value = '耳机';
input.dispatchEvent(new Event('input'));
这里有个非常关键的坑,我见过很多人踩:直接修改 input.value 不会触发 input 事件,所以页面上的列表不会跟着搜索框变化。必须手动 dispatchEvent(new Event('input')),把 input 事件派发出去,我们之前绑定的监听函数才会执行。这个技巧在自动化测试和调试时特别有用。
另一个调试技巧是检查静态资源是否加载完成。比如图片加载不出来,可以在控制台执行:
javascript复制performance.getEntriesByType('resource').map(item => item.name)
这会列出页面加载的所有资源 URL。如果你想快速找出图片有没有 404,还可以直接用 Network 面板,把刷新按钮勾上“Preserve log”,然后看红色的失败请求。不过对期末作业来说,记住一个更简单的:看到图片裂了,第一时间检查相对路径是否正确。
如果页面里有商品视频,想测试“视频播放结束后的页面状态”,可以在控制台手动触发结束事件:
javascript复制document.querySelector('video').dispatchEvent(new Event('ended'))
这样视频播放器会认为视频已经放完,触发 ended 相关的逻辑。注意这只是调试辅助,别把它写进正式代码里。
5. 常见问题与排查技巧实录
5.1 控制台报错别慌,先看这几类错误
JavaScript 报错是期末作业里最常见的噩梦。很多同学看到 Console 一片红就晕了,其实错误类型就那几类,逐个排除就行。
| 错误信息 | 含义 | 常见原因 |
|---|---|---|
| Cannot read properties of null | 操作了不存在的元素 | 选择器写错,元素还没渲染就执行 JS |
| xxx is not defined | 变量或函数没有定义 | 拼写错误、变量未声明、文件引入顺序不对 |
| Unexpected token | 语法错误 | 少括号、少引号、中英文符号混用 |
| Cannot read property of undefined | 数据是空的 | 数组越界、对象里没有该字段 |
| Failed to load resource | 资源加载失败 | 路径错误、文件不存在 |
排查时记住一个节奏:先看报错信息里的文件路径和行号,然后去 Console 里打 console.log 确认数据和元素是否存在。比如报错说 Cannot read properties of null,第一反应是 document.querySelector 返回了 null,也就是页面上根本没有这个元素。先检查 script 标签是不是放在 body 末尾了,如果 script 放在 head 里,body 还没渲染完,querySelector 自然找不到元素。
推荐直接把 script 写在 body 结束标签前面,或给 script 加 defer 属性。对于期末项目,最简单就是放在 body 末尾,等整个 DOM 都解析好了再执行,能避开很多坑。
另外,代码的括号配对是新手重灾区。建议把代码格式化成规范的缩进,用编辑器自带的自动换行和括号高亮能明显减少这类问题。VS Code 里按 Shift+Alt+F 可以格式化文件,非常实用。
5.2 CSS 不生效的 5 个原因
样式不生效比 JS 报错还让人头疼,因为它不报错,就是不对。我总结出五个高频原因,遇到问题按顺序排查。
第一,选择器没匹配上。CSS 里写 .goods-card,但 HTML 里 class 是 goods_card,差了横杠就找不到。还有属性选择器要严格区分大小写,class 名尽量用全小写加短横线。
第二,优先级不够。你写的 rules 被更具体的规则覆盖了。比如想改 .goods-name 字号,但已经没有其他样式,可页面还是不变,那很可能是 class 没写对,或者 CSS 文件引入顺序有问题。
第三,外部样式没引进来。检查 link 标签的 href,重点看路径。如果你的 HTML 在 project 根目录,CSS 在 project/css/style.css,href 应该是 css/style.css。如果你把 HTML 放在 project/html/ 下面,CSS 就要写成 ../css/style.css。
第四,浏览器缓存。改了 CSS 但页面不变,按住 Ctrl+F5 强制刷新,macOS 上按 Cmd+Shift+R。也可以把 CSS 文件路径后面加个 ?v=2 强制浏览器重新加载。
第五,语法错误。少写一个分号或者大括号虽然不报错,但后面的规则可能全部失效。把 CSS 文件整个折叠看一下大括号是否配对,很快能发现问题。
复杂样式不生效时,还有一个排查好方法:在浏览器开发者工具的 Elements 面板选中那个元素,看右侧 Computed 样式。所有 CSS 属性最后算出来的值都在这里,如果这里没有你设置的属性,八成是选择器或引入的问题。
5.3 图片加载不出来和静态资源路径
图片裂在购物商城里比 JS 报错更尴尬。出现这种情况,99% 是路径问题。
相对路径是从“当前文件所在位置”开始计算的。你在 index.html 里写 images/goods-1.jpg,浏览器会先找到 index.html 的目录,再去它的 images 子目录找图片。如果图片实际放在 assets/images/,路径就要改成 assets/images/goods-1.jpg。
还有一个坑是大小写和扩展名。Windows 上文件系统不区分大小写,但部署到服务器、或者老师用其他系统打开时,图片名大小写不一致就会 404。所以从一开始就严格要求:文件名全小写,扩展名写完整,比如 .jpg 不要写成 .jpeg 或者 .JPG。
处理图片加载失败,可以在 img 标签里加 onerror 事件,加载失败时给一张占位图:
html复制<img src="images/goods-1.jpg" alt="商品图片"
onerror="this.src='images/placeholder.png'">
onerror 并不优雅,但这种兜底策略在期末项目里非常实用,至少不会让页面出现一大片丑陋的裂图图标。
如果你在本地双击 index.html 用 file:// 协议打开时一切正常,但换到 Live Server 或别的环境就乱了,多半是因为代码里用了绝对路径,比如 C:/Users/xxx/images/...。这种路径只有你自己电脑能用。正确做法是改成相对路径,让整个文件夹拷到哪里都能跑。
5.4 提交和演示之前,这几件事一定要做
做完项目别急着交,下面这些事我每次帮人检查作业都会过一遍,顺序按重要性排。
第一,整包测试。把整个项目文件夹压缩、复制到另一个目录,比如 D 盘或者桌面,然后从复制出来的文件夹里打开 index.html。这样能验证是不是有绝对路径依赖。只复制 index.html 不复制 css、js、images 是很明显的低级失误,老师一打开就全乱。
第二,用 Live Server 跑一遍。很多同学直接双击 HTML 文件用 file:// 协议打开,这样只要不涉及模块化或跨域请求,大部分功能都能跑,但不建议。用 VS Code 装一个 Live Server 插件,右键 Open with Live Server,更接近真实部署环境,还能避开一些浏览器本地限制。如果老师现场要求展示,这个方法也更快。
第三,检查关键交互点。不要只看首页好看,而是要模拟用户从头到尾走一遍:看一个人能不能顺利从首页点进商品、加入购物车、修改数量、清除商品、看到总价变化、填完表单、提交成功。中间任何一个环节点了没反应,都是明显的 BUG。
第四,准备好答辩时的代码讲解点。哪怕代码是你自己写的,隔两天也可能忘记。建议挑三个最值得讲的地方提前组织语言,比如“我用对象来存购物车数据,因为用 id 做键比数组方便”“我用事件委托是因为商品列表是动态生成的”。这三个点能讲清楚,很多问题都会迎刃而解。
第五,导出几张项目截图。做期末数据备份或者写文档时会用到。可以用浏览器截图工具直接截长图,也可以按 F12 调成手机尺寸截一张移动端的图,展示你做了响应式适配。这个动作花不了两分钟,但对整体印象提升很有帮助。
根据我的经验,期末购物商城项目最大的失分点往往不是“不会做”,而是“忘了交全套”“路径写死”“个别按钮失灵”。只要把上面的流程走一遍,这些问题全都能提前拦下来。
