简介:这是一套面向前端初学者与进阶学习者的HTML5静态购物网站完整源码,围绕登录注册、商品展示、详情页与购物车等核心模块展开,帮助读者理解如何用HTML5、CSS3、JavaScript与jQuery搭建一个可运行的电商前端原型。压缩包共48个文件,约5.28MB,其中4个html页面承载首页、登录、详情与购物车结构,5个css文件负责响应式布局与视觉美化,8个js文件实现轮播、三级菜单、购物车增删改查等交互逻辑,另有16个png与14个jpg图片资源及1个json配置文件,覆盖页面素材与开发环境设置。目前已有1041人学习下载。资源目录按页面、样式、脚本与图片分层组织,便于对照理解登录状态保持、轮播切换、三级导航展开及购物车金额实时计算等实现思路,适合作为课程设计、毕业设计或前端练习的参考案例。
1. 从一份能直接跑起来的静态购物站说起
前阵子帮一个刚入行的朋友看简历项目,他丢过来一个压缩包,说“哥你帮我看看这个能不能写进简历”。解压之后我愣了一下——一个完整的购物网站,首页、商品列表、详情页、购物车、登录注册全都有,但整个项目里没有一行后端代码,没有数据库,没有 npm install,双击 index.html 就能在浏览器里跑起来。这就是典型的 html5 购物网站带登录 的静态实现:用 html 搭骨架,css 做视觉,jQuery 处理交互,localStorage 存登录态和购物车数据。
这类项目特别适合两类人:一是刚学完 html、css、js 想找个完整项目练手的同学,二是需要快速交付一个纯前端演示站、给客户看交互效果的开发者。它不依赖任何服务器环境,丢到任何静态托管上都能访问,改起来也直观——想换配色改 css,想加商品改一个 js 数组,想调交互逻辑就在 jQuery 事件里动手。下面我就按“这项目怎么组织的、怎么跑起来、核心代码长什么样、怎么改成自己的”这条线,把它拆开讲一遍。
2. 项目结构与技术选型:为什么用 jQuery 而不是框架
2.1 目录组织与文件职责
拿到这个项目,第一件事是看目录。静态购物站的目录结构通常长这样,我按实际拆包经验还原一份典型的:
shop-static/ ├── index.html # 首页,商品推荐 + 轮播 ├── list.html # 商品列表页,支持分类筛选 ├── detail.html # 商品详情页,加购入口 ├── cart.html # 购物车页,数量增减、结算 ├── login.html # 登录 / 注册页 ├── css/ │ ├── reset.css # 浏览器默认样式重置 │ ├── common.css # 公共样式:头部、底部、按钮 │ └── pages.css # 各页面专属样式 ├── js/ │ ├── jquery.min.js # jQuery 库文件 │ ├── data.js # 商品数据(模拟后端返回) │ ├── common.js # 公共逻辑:登录态、购物车读写 │ └── pages.js # 各页面交互逻辑 ├── images/ # 商品图、图标、banner └── lib/ # 可能放 swiper 等轮播插件这个分法的核心思路是“数据、公共逻辑、页面逻辑”三层分离。data.js 只负责吐数据,common.js 管登录态和购物车这种跨页面共享的状态,pages.js 按页面路径判断当前在哪个页面、绑定对应事件。新手最容易犯的错是把所有 js 塞进一个文件,改到后面自己都找不到哪段代码管哪个按钮。
2.2 技术选型:jQuery 在这个场景下为什么还成立
很多人一看 jQuery 就皱眉,觉得 2024 年了还用这玩意儿。但放到“静态购物站 + 新手练手 + 快速演示”这个具体场景里,jQuery 反而有它的道理。第一,它不需要构建工具,一个 script 标签引入就能用,省掉了 webpack、vite 那一整套配置,对刚学完基础语法的人友好。第二,DOM 操作和事件绑定是它的强项,购物站里大量的“点击加购”“数量增减”“切换分类”本质就是 DOM 操作,jQuery 的$('.btn').on('click', fn)比原生addEventListener写起来短。第三,ajax 请求封装得简单,后面想接真实后端,$.ajax或$.get改起来成本低。
当然,如果这个项目要往大了做,比如商品上千、需要组件复用、要上路由,那确实该换 Vue 或 React。但作为一份“能跑起来、能看懂、能改”的静态站,jQuery 的性价比是成立的。我一般会跟朋友说:先把这个跑通,理解购物车的状态怎么流转,再去上框架,你会知道框架帮你解决了什么问题。
2.3 数据层设计:用 js 数组模拟后端接口
静态站没有数据库,商品数据只能写在前端。常见做法是在 data.js 里定义一个全局数组,每个对象代表一个商品:
// js/data.js // 模拟后端返回的商品列表,字段命名尽量贴近真实接口 var goodsList = [ { id: 1001, // 商品唯一标识,加购和详情页靠它定位 name: '纯棉短袖T恤', // 商品名 price: 89.00, // 单价,用数字方便计算 img: 'images/goods/1001.jpg',// 主图路径 category: 'clothing', // 分类标识,列表页筛选用 stock: 50, // 库存,加购时校验 desc: '夏季新款,透气舒适' // 详情页描述 }, { id: 1002, name: '帆布休闲鞋', price: 199.00, img: 'images/goods/1002.jpg', category: 'shoes', stock: 30, desc: '百搭款,适合日常通勤' } // ... 更多商品 ];这里有几个细节值得说。id 用数字而不是字符串,是为了后面购物车合并同款商品时方便比较;price 存数字不存'89.00'这种字符串,因为结算时要累加,字符串相加会变成拼接;category 用英文标识而不是中文,是为了筛选时做等值判断不受编码影响。这些看着小,但都是实际写的时候容易翻车的地方。数据层定好了,后面所有页面都从这个数组里取数据,改商品只需要改这一个文件。
3. 登录态与购物车:两个核心状态的读写实现
3.1 登录注册:localStorage 存 token 的完整流程
静态站没有真正的后端鉴权,登录逻辑本质是“把用户信息存到本地,其他页面读出来判断是否登录”。注册和登录都走 localStorage。先看注册:
// js/common.js // 注册:把用户信息存进 localStorage 的 users 列表 function register(username, password) { // 取出已有用户,没有就初始化空数组 var users = JSON.parse(localStorage.getItem('users') || '[]'); // 检查用户名是否已存在,避免重复注册 var exist = users.some(function (u) { return u.username === username; }); if (exist) { return { code: 1, msg: '用户名已存在' }; } // 实际项目里密码不能明文存,这里演示用简单处理 users.push({ username: username, password: password }); localStorage.setItem('users', JSON.stringify(users)); return { code: 0, msg: '注册成功' }; }注册的核心是把用户数组序列化后写回 localStorage。JSON.parse(localStorage.getItem('users') || '[]')这个写法很关键——第一次访问时 localStorage 里没有 users,getItem 返回 null,null || '[]'会得到'[]',parse 出来就是空数组,避免了 null 报错。这是静态站里处理“首次访问”的通用套路。
登录则是校验后写入当前登录态:
// 登录:校验账号密码,成功后写入 currentUser function login(username, password) { var users = JSON.parse(localStorage.getItem('users') || '[]'); var target = users.find(function (u) { return u.username === username && u.password === password; }); if (!target) { return { code: 1, msg: '账号或密码错误' }; } // 写入当前登录用户,其他页面靠这个判断登录态 localStorage.setItem('currentUser', JSON.stringify({ username: target.username, loginTime: Date.now() })); return { code: 0, msg: '登录成功' }; }登录成功后写的是currentUser而不是直接改 users,这样退出登录只需要removeItem('currentUser'),不会影响注册数据。判断是否登录就一行:var isLogin = !!localStorage.getItem('currentUser')。页面加载时调这个判断,没登录就跳 login.html,登录了就渲染用户名。
3.2 购物车:增删改查与同款合并
购物车是静态站里逻辑最密的部分。数据结构一般是一个数组,每项包含商品 id、数量、加购时的单价快照:
// 加入购物车:同款商品数量累加,新款则追加 function addToCart(goodsId, count) { count = count || 1; var cart = JSON.parse(localStorage.getItem('cart') || '[]'); // 找到同款商品 var item = cart.find(function (g) { return g.id === goodsId; }); if (item) { item.count += count; // 已存在则累加数量 } else { // 从商品数据里查出完整信息,存一份价格快照 var goods = goodsList.find(function (g) { return g.id === goodsId; }); if (!goods) return { code: 1, msg: '商品不存在' }; cart.push({ id: goods.id, name: goods.name, price: goods.price, // 价格快照,防止后续改价影响已加购订单 img: goods.img, count: count }); } localStorage.setItem('cart', JSON.stringify(cart)); return { code: 0, msg: '已加入购物车' }; }这里有个设计点:加购时把 price 存了一份快照,而不是每次渲染时去 goodsList 里现查。原因是商品价格可能被改,如果购物车里存的是引用,改价后购物车金额会跟着变,用户会觉得“我加购时不是这个价”。存快照是电商里的常规做法,静态站虽然简单,但这个意识要有。
数量增减和删除就是对这个数组做修改:
// 修改购物车某商品数量,count 为 0 时删除 function updateCartCount(goodsId, count) { var cart = JSON.parse(localStorage.getItem('cart') || '[]'); if (count <= 0) { // 数量减到 0 直接移除 cart = cart.filter(function (g) { return g.id !== goodsId; }); } else { var item = cart.find(function (g) { return g.id === goodsId; }); if (item) item.count = count; } localStorage.setItem('cart', JSON.stringify(cart)); return cart; }filter用来删除,find用来定位,这两个数组方法在购物车逻辑里出现频率极高。渲染购物车时遍历数组,每项算price * count,最后 reduce 出总价。整个购物车不依赖任何后端,刷新页面数据还在,这就是 localStorage 的价值。
3.3 页面渲染:数据怎么变成 DOM
数据有了,得渲染到页面上。以商品列表页为例,常见做法是拼字符串再塞进容器:
// js/pages.js // 渲染商品列表,category 为空则显示全部 function renderGoodsList(category) { var list = goodsList; if (category) { // 按分类筛选 list = goodsList.filter(function (g) { return g.category === category; }); } var html = ''; list.forEach(function (g) { // 用模板字符串拼卡片,data-id 挂商品 id 供点击时读取 html += '<div class="goods-card">// 事件委托:绑在容器上,动态生成的按钮也能响应 $('#goodsContainer').on('click', '.add-cart', function () { var id = $(this).closest('.goods-card').data('id'); var res = addToCart(id, 1); if (res.code === 0) { alert('已加入购物车'); updateCartBadge(); // 更新头部购物车角标 } });委托绑定的原理是事件冒泡:点击按钮冒泡到容器,容器再根据选择器匹配。这样无论商品卡片怎么重新渲染,事件都不会丢。新手常犯的错是把 click 直接绑在.add-cart上,第一次渲染能用,筛选后重新渲染就失效了,然后到处找原因——这就是典型的踩坑点。
4. 避坑与常见问题:静态站里那些容易翻车的地方
4.1 刷新后登录态丢失或误判
现象:登录后跳转到首页,刷新一下又变成未登录,或者明明没登录却显示用户名。原因通常是登录态判断写在了页面加载之后,或者判断逻辑用了localStorage.getItem('currentUser')直接当布尔值——它返回的是字符串'null'时也是真值。解决:统一封装isLogin()函数,用!!localStorage.getItem('currentUser')判断,并在每个页面<head>里尽早执行跳转逻辑,别等 DOM 加载完。
4.2 购物车数量显示不同步
现象:加了商品,头部角标还是 0,刷新才对。原因是角标只在页面加载时算了一次,加购后没重新计算。解决:把角标更新抽成独立函数updateCartBadge(),每次购物车数据变动后都调一次。这个函数遍历 cart 数组累加 count,写到角标元素里。养成“数据变、视图跟着变”的习惯,后面上框架会顺很多。
4.3 商品 id 类型不一致导致合并失败
现象:同一件商品加购两次,购物车里出现两条而不是数量变 2。原因是第一次加购时 id 从 DOM 的>/* css/common.css */ :root { --primary: #ff5000; /* 主色,按钮、价格、高亮 */ --bg: #f5f5f5; /* 页面背景 */ --text: #333; /* 主文字 */ --radius: 8px; /* 圆角统一 */ } .btn-primary { background: var(--primary); border-radius: var(--radius); }
改配色只动--primary一个值,全站按钮、价格、角标跟着变。这是把静态站做成“可换肤”的最低成本方案。如果项目里还没用变量,改造时顺手抽出来,后面维护省事。
5.3 接后端:把 localStorage 换成接口调用
静态站要变成真项目,核心是把 common.js 里的读写换成 ajax。改造思路是保持函数签名不变,只换内部实现:
// 改造前:从 localStorage 读购物车 function getCart() { return JSON.parse(localStorage.getItem('cart') || '[]'); } // 改造后:从后端接口读,返回 Promise function getCart() { return $.get('/api/cart').then(function (res) { return res.data; // 约定后端返回 { code, data, msg } }); }调用处从var cart = getCart()改成getCart().then(function(cart){ ... })。登录同理,把 login 函数里的 localStorage 写入换成$.post('/api/login', {...}),成功后把后端返回的 token 存起来,后续请求带上。改造时建议一个模块一个模块来,先改登录,跑通再改购物车,别一次性全换,否则出问题不知道是哪块的。
5.4 一个我踩过的坑:别在渲染函数里读 localStorage
早期我写这类项目,喜欢在渲染函数里直接JSON.parse(localStorage.getItem('cart')),图省事。结果页面一多,同一个数据在五六个地方各读各的,改了一处忘了另一处,购物车金额和角标对不上,排查了半天。后来我强制自己:所有 localStorage 读写只允许出现在 common.js 的数据层函数里,页面逻辑一律调函数,不直接碰 localStorage。这个习惯让我后面接后端时几乎没改页面代码,只换了数据层实现。从那以后我每次开新项目,第一件事就是把数据读写收口到一个文件,哪怕它只是个静态 demo。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取