简介:这是一套面向前端初学者与进阶练习者的HTML5静态购物网站完整源码,围绕登录注册、商品展示、详情页与购物车等核心模块展开,帮助读者理解从页面搭建到交互逻辑的完整实现路径。压缩包共48个文件,约5.28MB,包含4个html页面、5个css样式表、8个js脚本,以及16个png与14个jpg图片素材和1个json配置文件,涵盖页面结构、样式美化、轮播与三级菜单交互、购物车增删改查等模块,图片资源可直接用于页面展示。已有1041人学习下载,说明其结构清晰、上手门槛适中。读者可据此掌握HTML5表单验证与本地存储、CSS3过渡动画与响应式布局、jQuery事件处理与DOM动态操作等知识点,并参考现有目录组织方式快速搭建自己的购物网站原型,适合课程设计、毕业设计或前端练手项目参考。
1. 从零手搓一个带登录的 HTML5 购物网站:这套静态页面源码能帮你省下多少试错时间
如果你正在找一份能直接跑起来、结构清晰、还带完整登录流程的 HTML5 购物网站源码,这套资源大概率能让你少熬两个晚上。它不是那种只有首页和几个死链接的“演示壳子”,而是把商品列表、详情页、购物车、结算页、登录注册、订单确认这些电商核心页面都串了起来。技术栈走的是最经典的 HTML + CSS + JavaScript + jQuery 组合,没有构建工具,没有 npm install,浏览器双击就能看效果。适合谁?前端新手拿来练手完整项目流程,熟手拿来当静态站模板快速改造成活动页或产品展示站。我拆过不少类似源码,很多号称“购物网站”的压缩包解压后只有三个 HTML 文件,这套的页面完整度算是能打的。
2. 项目结构拆解:从文件目录看一个静态电商站该怎么组织
2.1 目录层级与文件职责划分
拿到源码包后,我习惯先看目录结构,这比读代码更快判断一个项目的成熟度。这套购物网站的目录组织遵循了静态站点的经典分层思路,没有用 webpack 或 vite 那套构建体系,所有资源都是浏览器直接可读的格式。
典型的目录结构长这样:
shopping-site/ ├── index.html # 首页,商品推荐与导航入口 ├── login.html # 登录页,表单验证与跳转逻辑 ├── register.html # 注册页,与登录页共享校验逻辑 ├── product-list.html # 商品列表页,支持分类筛选 ├── product-detail.html # 商品详情页,含规格选择与加购 ├── cart.html # 购物车页,数量增减与金额计算 ├── checkout.html # 结算页,收货信息与订单确认 ├── css/ │ ├── reset.css # 浏览器默认样式重置 │ ├── common.css # 公共样式:头部、底部、按钮、表单 │ └── pages.css # 各页面专属样式 ├── js/ │ ├── jquery.min.js # jQuery 库文件 │ ├── common.js # 公共逻辑:导航、登录态检查 │ ├── login.js # 登录注册表单校验与提交 │ ├── cart.js # 购物车增删改查与本地存储 │ └── product.js # 商品渲染与筛选 ├── images/ # 商品图、图标、占位图 └── data/ └── products.json # 商品模拟数据(部分版本内联在 JS 中)这个结构的好处是职责边界清楚。common.css管全站复用的按钮、表单、卡片样式,pages.css只管单个页面的特殊布局。JS 也是同理,common.js里放登录态检查、导航栏渲染这类每个页面都要跑的逻辑,login.js只处理登录注册页的表单。我见过不少新手把几千行 CSS 全塞一个文件里,改一个按钮颜色要翻半天,这套源码的分层方式值得借鉴。
2.2 页面间的跳转关系与登录态传递
静态购物网站最容易被忽略的就是页面间的状态传递。没有后端 Session,登录态怎么保持?这套源码用的是localStorage存用户信息和登录令牌,每个页面加载时由common.js统一检查。
核心逻辑是这样的:
// common.js - 登录态检查与页面守卫 (function checkAuth() { // 从 localStorage 读取登录令牌 var token = localStorage.getItem('shop_token'); var userInfo = localStorage.getItem('shop_user'); // 需要登录才能访问的页面列表 var protectedPages = ['cart.html', 'checkout.html']; var currentPage = window.location.pathname.split('/').pop(); // 如果当前页在保护列表里且没有登录态,跳转到登录页 if (protectedPages.indexOf(currentPage) > -1 && !token) { // 记录来源页,登录后跳回 localStorage.setItem('redirect_after_login', currentPage); window.location.href = 'login.html'; return; } // 已登录则更新页面上的用户信息展示 if (token && userInfo) { var user = JSON.parse(userInfo); $('.js-username').text(user.nickname || user.phone); $('.js-login-link').hide(); $('.js-user-info').show(); } })();这段代码做了三件事:第一,从localStorage读取登录令牌和用户信息;第二,判断当前页面是否属于需要登录才能访问的页面,如果是且未登录就跳转;第三,已登录状态下更新页面头部的用户信息展示区域。
参数说明:shop_token是登录成功后写入的模拟令牌,实际项目中应该由后端下发;shop_user存的是用户昵称、手机号等展示信息;redirect_after_login用来记录用户是从哪个页面被踢到登录页的,登录成功后跳回去,这个细节很多简易源码都漏了。
注意:静态站的登录态完全依赖浏览器本地存储,清缓存或换浏览器就会丢失,这是纯前端方案的天然限制,不是代码 bug。
2.3 商品数据的组织方式
商品数据这块,不同版本的源码处理方式不一样。有的把数据直接内联在product.js里,有的单独抽了products.json。我更推荐后者,因为改商品不用翻 JS 逻辑。
// product.js - 从 JSON 加载商品并渲染列表 function loadProducts(category) { $.getJSON('data/products.json', function(data) { var list = data.products; // 按分类筛选,category 为 'all' 时展示全部 if (category && category !== 'all') { list = list.filter(function(item) { return item.category === category; }); } // 渲染商品卡片到列表容器 var html = ''; $.each(list, function(index, item) { html += '<div class="product-card"><!-- login.html 登录表单核心结构 --> <form id="login-form" class="form-box" novalidate> <div class="form-item"> <label for="login-phone">手机号</label> <input type="tel" id="login-phone" name="phone" placeholder="请输入11位手机号" maxlength="11" autocomplete="off"> <span class="error-tip" id="phone-error"></span> </div> <div class="form-item"> <label for="login-password">密码</label> <input type="password" id="login-password" name="password" placeholder="请输入密码" maxlength="20"> <span class="error-tip" id="password-error"></span> </div> <button type="submit" class="btn-submit">登 录</button> <p class="form-link">还没有账号?<a href="register.html">立即注册</a></p> </form>结构上用了novalidate关掉浏览器默认校验,因为默认提示样式不可控,而且不同浏览器表现不一致。每个输入框下面配一个error-tip的span,用来显示自定义错误信息。maxlength限制输入长度,autocomplete="off"防止浏览器自动填充干扰测试。
3.2 jQuery 表单校验与登录逻辑
校验逻辑写在login.js里,用 jQuery 绑定提交事件,逐项校验后决定是否放行。
// login.js - 登录表单校验与提交 $(function() { var $form = $('#login-form'); var $phone = $('#login-phone'); var $password = $('#login-password'); // 手机号校验:11位数字,1开头 function validatePhone() { var val = $.trim($phone.val()); var reg = /^1\d{10}$/; if (!val) { $('#phone-error').text('手机号不能为空'); return false; } if (!reg.test(val)) { $('#phone-error').text('手机号格式不正确'); return false; } $('#phone-error').text(''); return true; } // 密码校验:6-20位,不能纯数字 function validatePassword() { var val = $password.val(); if (!val) { $('#password-error').text('密码不能为空'); return false; } if (val.length < 6 || val.length > 20) { $('#password-error').text('密码长度需在6-20位之间'); return false; } if (/^\d+$/.test(val)) { $('#password-error').text('密码不能为纯数字'); return false; } $('#password-error').text(''); return true; } // 失焦时即时校验,提升体验 $phone.on('blur', validatePhone); $password.on('blur', validatePassword); // 提交时统一校验 $form.on('submit', function(e) { e.preventDefault(); // 阻止表单默认提交 var isPhoneOk = validatePhone(); var isPwdOk = validatePassword(); if (!isPhoneOk || !isPwdOk) { return false; } // 模拟登录请求:实际项目应替换为 AJAX 调用后端接口 var phone = $.trim($phone.val()); var password = $password.val(); // 从 localStorage 读取已注册用户列表 var users = JSON.parse(localStorage.getItem('shop_users') || '[]'); var matched = users.filter(function(u) { return u.phone === phone && u.password === password; }); if (matched.length === 0) { $('#password-error').text('手机号或密码错误'); return false; } // 登录成功:写入令牌和用户信息 var user = matched[0]; localStorage.setItem('shop_token', 'token_' + Date.now()); localStorage.setItem('shop_user', JSON.stringify({ phone: user.phone, nickname: user.nickname || ('用户' + phone.slice(-4)) })); // 跳回来源页或首页 var redirect = localStorage.getItem('redirect_after_login') || 'index.html'; localStorage.removeItem('redirect_after_login'); window.location.href = redirect; }); });逻辑说明:校验分两层,失焦时即时提示,提交时统一拦截。手机号用正则/^1\d{10}$/校验,密码要求 6-20 位且不能纯数字。登录验证从localStorage里读已注册用户列表做匹配,匹配成功写入shop_token和shop_user,然后跳转。
参数说明:shop_users是注册时写入的用户数组,每个元素包含phone、password、nickname;shop_token用时间戳模拟,实际项目应由后端生成;redirect_after_login在页面守卫里写入,登录成功后读取并清除。
注意:把密码明文存在 localStorage 里是纯前端演示的权宜之计,真实项目必须走后端接口,密码在传输和存储时都要加密。
3.3 注册模块与登录的联动
注册页和登录页共享大部分校验逻辑,区别在于注册需要多存一份用户数据,并且要检查手机号是否已注册。
// register.js - 注册核心逻辑(省略与登录重复的校验函数) $('#register-form').on('submit', function(e) { e.preventDefault(); // 复用登录页的校验函数(实际项目中可抽到 common.js) if (!validatePhone() || !validatePassword()) { return false; } var phone = $.trim($('#reg-phone').val()); var password = $('#reg-password').val(); var nickname = $.trim($('#reg-nickname').val()) || ('用户' + phone.slice(-4)); // 读取已有用户列表 var users = JSON.parse(localStorage.getItem('shop_users') || '[]'); // 检查手机号是否已注册 var exists = users.some(function(u) { return u.phone === phone; }); if (exists) { $('#phone-error').text('该手机号已注册'); return false; } // 写入新用户 users.push({ phone: phone, password: password, nickname: nickname }); localStorage.setItem('shop_users', JSON.stringify(users)); // 注册成功后自动登录并跳转 localStorage.setItem('shop_token', 'token_' + Date.now()); localStorage.setItem('shop_user', JSON.stringify({ phone: phone, nickname: nickname })); window.location.href = 'index.html'; });注册逻辑比登录多了一步查重,用Array.some判断手机号是否已存在。注册成功后直接写入登录态并跳首页,省去用户再登录一次的操作。这里有个细节:nickname如果用户没填,就用手机号后四位拼一个默认昵称,避免页面显示空白。
4. 购物车与结算:本地存储驱动的增删改查和金额计算
4.1 购物车数据结构设计
购物车是静态电商站最核心的交互模块,数据怎么存直接决定了后续逻辑好不好写。这套源码用localStorage存一个数组,每个元素是一条购物车记录。
// cart.js - 购物车数据结构与初始化 // 单条购物车记录结构: // { // id: '1001', // 商品ID // name: '商品名称', // price: 99.00, // 单价 // image: 'images/1.jpg', // count: 2, // 购买数量 // spec: '红色' // 规格(可选) // } // 读取购物车数据,不存在则返回空数组 function getCart() { return JSON.parse(localStorage.getItem('shop_cart') || '[]'); } // 写入购物车数据 function saveCart(cart) { localStorage.setItem('shop_cart', JSON.stringify(cart)); updateCartBadge(); // 更新头部购物车角标 } // 更新导航栏购物车数量角标 function updateCartBadge() { var cart = getCart(); var total = cart.reduce(function(sum, item) { return sum + item.count; }, 0); $('.js-cart-count').text(total); }数据结构里id是商品唯一标识,加购时如果已存在相同id的记录就增加count,不存在就 push 新记录。updateCartBadge用reduce算总件数,更新头部角标,这个函数在加购、删除、修改数量后都要调用。
4.2 加购、修改数量与删除的实现
加购逻辑绑定在商品列表和详情页的按钮上,通过事件委托处理动态渲染出来的按钮。
// cart.js - 加购逻辑 $(document).on('click', '.btn-add-cart', function() { var id = $(this).data('id'); // 从商品数据里找到对应商品(实际项目中可能从缓存或接口取) $.getJSON('data/products.json', function(data) { var product = data.products.filter(function(p) { return String(p.id) === String(id); })[0]; if (!product) return; var cart = getCart(); var existing = cart.filter(function(item) { return String(item.id) === String(id); })[0]; if (existing) { existing.count += 1; // 已存在则数量加一 } else { cart.push({ id: product.id, name: product.name, price: product.price, image: product.image, count: 1, spec: '' }); } saveCart(cart); alert('已加入购物车'); }); }); // 修改数量:加号 $(document).on('click', '.js-count-plus', function() { var id = $(this).data('id'); var cart = getCart(); cart.forEach(function(item) { if (String(item.id) === String(id)) { item.count += 1; } }); saveCart(cart); renderCart(); // 重新渲染购物车列表 }); // 修改数量:减号(数量为1时不再减) $(document).on('click', '.js-count-minus', function() { var id = $(this).data('id'); var cart = getCart(); cart.forEach(function(item) { if (String(item.id) === String(id) && item.count > 1) { item.count -= 1; } }); saveCart(cart); renderCart(); }); // 删除商品 $(document).on('click', '.js-remove-item', function() { var id = $(this).data('id'); var cart = getCart().filter(function(item) { return String(item.id) !== String(id); }); saveCart(cart); renderCart(); });逻辑说明:加购用事件委托绑定在document上,因为商品卡片是动态渲染的,直接绑在按钮上会失效。修改数量和删除都是先读购物车数组,操作后再写回并重新渲染。减号操作加了count > 1的判断,防止数量减到 0 或负数。
参数说明:>// cart.js - 金额计算(避免浮点数精度问题) function calcTotal(cart) { // 用整数分计算,最后再转回元 var totalFen = cart.reduce(function(sum, item) { return sum + Math.round(item.price * 100) * item.count; }, 0); return (totalFen / 100).toFixed(2); } // 渲染购物车列表 function renderCart() { var cart = getCart(); var $list = $('#cart-list'); if (cart.length === 0) { $list.html('<p class="empty-tip">购物车还是空的,去逛逛吧</p>'); $('#cart-total').text('0.00'); return; } var html = ''; cart.forEach(function(item) { html += '<div class="cart-item" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />