首页 / 资讯中心 / 文章详情

HTML5购物网站带登录:localStorage购物闭环实战

HTML5购物网站带登录:localStorage购物闭环实战 ★ FEATURED ARTICLE
简介这是一份面向前端初学者与网页课程练习者的静态购物网站实战源码以HTML5页面结构为基础配合CSS3完成美化与响应式布局并用JavaScript与jQuery实现交互逻辑帮助读者理解从登录注册到购物车结算的完整页面级实现思路。资源共48个文件压缩包约5.28MB包含4个HTML页面、5个CSS样式表、8个JavaScript脚本以及16张PNG图标和14张JPG商品与轮播图片另有1个JSON配置文件分别承担页面骨架、视觉样式、交互逻辑与素材支撑。目前已有1043人学习下载。代码中可看到轮播图切换、三级菜单展开收起、商品详情页渲染、购物车商品增删与总价计算等模块配合jQuery的DOM操作与本地存储保持登录状态结构清晰、模块划分明确适合拿来拆解学习或直接作为课程设计、毕业设计的基础模板进行二次改造。1. 需求先立住「html5购物网站带登录」要交付的是一条完整闭环多数人接到「html5购物网站带登录」这个题目第一反应是问「没有后端怎么登录」这个问题其实问反了。一套纯 html、css、jQuery 的静态购物网站真正要交付的不是一个能收款的电商系统而是一条从前端视角看完全成立的业务闭环商品列表能渲染、详情页能跳转、加购数量能改、未登录点结算会被拦住、注册登录后能生成订单、刷新页面数据还在。这条链路走通课程设计的前端主体就立住了毕业设计的演示环节也拿得出手。它适合两类人一类是要交静态站点作业、需要一个功能密度足够的题目另一类是刚学完 DOM 操作和 jQuery想找一个能把 localStorage、事件委托、模块拆分一次性串起来的综合练习。下面按需求、架构、数据、实现、避坑、验收的顺序把整套结构摊开讲。2. 总体架构设计三层结构与五个页面的职责边界静态站点最容易翻车的地方不是写不出页面而是写着写着每个页面都在自己读写 localStorage最后谁改了数据谁也说不清。所以开工之前先把结构定死数据层只管存取状态层只管会话和购物车视图层只管渲染和事件三层之间只允许单向调用。2.1 为什么必须拆出独立的「数据层」先看反面例子。首页写了一段JSON.parse(localStorage.getItem(cart))详情页又写了一段购物车页再写一段三处的默认值处理还不一样——首页是|| []详情页是|| {}购物车页干脆没写默认值。结果就是在详情页第一次加购时直接抛异常控制台一片红。这类问题在学生作业里出现频率极高因为代码是「一页一页抄过去」的抄的时候只改了 DOM 结构没改数据读取逻辑。正确的做法是抽一个js/store.js把所有持久化读写收成四个函数read(key, fallback)、write(key, value)、remove(key)、update(key, fn)。其他页面一律不碰localStorage这个 API 本身。这么做的好处很直接日后要换成 sessionStorage、要加过期时间、要加命名空间前缀只改一个文件。// js/store.js —— 全局唯一的持久化入口 (function (global) { // 统一加前缀避免和同域下其他页面的 key 冲突 var PREFIX shop_; function realKey(key) { return PREFIX key; } // 读解析失败或不存在时返回 fallback绝不返回 null 让调用方去猜 function read(key, fallback) { var raw localStorage.getItem(realKey(key)); if (raw null || raw ) return fallback; try { return JSON.parse(raw); } catch (e) { // 数据被手动改坏了清掉并回退别让整个页面白屏 localStorage.removeItem(realKey(key)); return fallback; } } function write(key, value) { localStorage.setItem(realKey(key), JSON.stringify(value)); return value; } function remove(key) { localStorage.removeItem(realKey(key)); } // 读-改-写三步合成一次调用减少并发覆盖 function update(key, fallback, mutator) { var next mutator(read(key, fallback)); return write(key, next); } global.Store { read: read, write: write, remove: remove, update: update }; })(window);这段代码里有两个关键点值得说明。realKey做前缀隔离是为了避免在同一台机器上开多个练习项目时 key 互相覆盖这是踩过坑之后才会加的一行。update的存在是为了解决「读出来改一半再写回去」中间被别的操作插队的问题虽然单页面场景下几乎不会真的并发但把这个习惯养成后面做购物车数量加减时逻辑会干净很多。参数fallback必须由调用方显式传逼着写代码的人想清楚「没有数据时应该是什么」而不是随手写个|| []蒙混过去。2.2 五个页面的文件组织与跳转关系页面数量控制在五个index.html首页/商品列表、detail.html商品详情、cart.html购物车、login.html登录注册二合一、order.html订单结果。这五个页面构成一条单向主干任何页面都可以跳回首页购物车和订单页需要登录态未登录时重定向到login.html并带上回跳地址。shop/ ├── index.html # 首页分类筛选 商品列表 ├── detail.html # 详情页?idp1001 取商品 ├── cart.html # 购物车数量增减、勾选、合计 ├── login.html # 登录/注册切换支持 ?redirect ├── order.html # 下单结果展示订单号与明细 ├── css/ │ ├── reset.css # 基础重置 │ ├── common.css # 变量、按钮、卡片、弹层等公共组件 │ └── page.css # 各页面差异化样式用 body class 区分 ├── js/ │ ├── store.js # 持久化入口上面那段 │ ├── data.js # 商品静态数据 查询方法 │ ├── auth.js # 注册、登录、登出、登录态判断 │ ├── cart.js # 购物车增删改查与金额计算 │ └── page-*.js # 每个页面一个入口脚本 └── images/ └── p1001.jpg ...这个结构里最关键的一条约定是page-*.js只负责「取数据 → 渲染 DOM → 绑事件」不允许出现任何localStorage调用也不允许出现商品价格计算。所有跨页面共享的逻辑都沉到auth.js和cart.js里。这样一来页面脚本会变得非常薄二十行到五十行就能写完一个页面出问题时排查范围也小。2.3 全局样式变量与组件化 CSS 的落地方式静态站点用纯 CSS 最容易失控的是颜色和间距散落各处改一个主色要全局搜索替换。用 CSS 自定义属性在common.css顶部集中声明后面所有页面只引用变量。/* css/common.css —— 设计变量集中声明改主题只动这一块 */ :root { --color-primary: #e93b3d; /* 主色价格、主按钮 */ --color-primary-dark: #c92f31; /* 主色按下态 */ --color-text: #333333; --color-text-weak: #999999; --color-border: #e5e5e5; --color-bg: #f6f6f6; --radius-card: 8px; --space-sm: 8px; --space-md: 16px; --space-lg: 24px; --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06); } /* 价格是购物站最高频的组件单独抽出来 */ .price { color: var(--color-primary); font-weight: 600; } .price::before { content: ¥; font-size: 0.75em; margin-right: 2px; }用变量的收益不只是换肤。当商品卡片、购物车行、订单明细三处都引用同一个.price类时价格展示的一致性是被结构保证的而不是靠人工对齐。::before拼货币符号而不是把符号写进 HTML是为了让 JS 渲染时只需要输出数字减少模板字符串里的字符串拼接错误——这个细节在渲染上百条商品时能省掉不少低级 bug。3. 数据与登录态商品 JSON 结构与 localStorage 会话管理数据设计决定了后面所有页面的写法。这一步如果偷懒把商品信息直接写死在 HTML 里那么详情页就得靠 URL 参数去反查 DOM筛选功能也无从谈起。所以先把商品抽成一份结构化的数组再把用户和购物车的结构定死。3.1 商品数据表的字段设计与静态数据组织商品数据放js/data.js用一个立即执行函数包起来对外只暴露查询方法而不是原始数组防止某个页面直接改数组导致其他页面读到脏数据。字段类型说明用在哪idstring商品唯一编号如 p1001详情页 URL 参数、购物车关联namestring商品名列表、详情、订单明细pricenumber现价单位元保留两位小数所有金额计算originPricenumber原价无优惠时与 price 相同列表划线价coverstring主图相对路径列表、详情categorystring分类标识如 digital / home首页分类筛选stocknumber库存用于加购上限校验加购按钮tagsarray标签如「包邮」「七天无理由」列表角标descstring详情描述详情页// js/data.js —— 商品数据与查询接口 (function (global) { var LIST [ { id: p1001, name: 无线蓝牙耳机, price: 199.00, originPrice: 259.00, cover: images/p1001.jpg, category: digital, stock: 20, tags: [包邮, 七天无理由], desc: 半入耳设计单次续航 6 小时。 }, { id: p1002, name: 桌面收纳盒, price: 39.90, originPrice: 39.90, cover: images/p1002.jpg, category: home, stock: 50, tags: [新品], desc: 三层抽屉结构磨砂质感。 } // 其余商品按同一结构补齐建议 1220 条方便演示分页和筛选 ]; // 返回副本防止调用方直接改到原始数组 function all() { return LIST.slice(); } function byId(id) { for (var i 0; i LIST.length; i) { if (LIST[i].id id) return LIST[i]; } return null; } // 按分类筛category 为空表示全部 function byCategory(category) { if (!category || category all) return all(); return LIST.filter(function (item) { return item.category category; }); } global.Goods { all: all, byId: byId, byCategory: byCategory }; })(window);all()返回slice()副本这一行看起来多余实际上是防呆设计。视图层经常会做排序如果直接在原数组上sort()第二次进首页时商品顺序就变了这种「刷新几次顺序不一样」的玄学问题排查起来非常费时间。byId用循环而不是Array.prototype.find是为了兼容老版本浏览器同时在没有匹配时明确返回null而不是undefined调用方用if (!goods)就能统一判断。价格字段务必写成数字而不是字符串后面合计金额时才不会出现字符串拼接。3.2 注册与登录的校验流程纯前端没有服务端登录本质上是「把用户表存在本地登录时比对」。这里必须把话说清楚这种方案没有任何真实安全性密码是明文或简单散列存在浏览器里的它只用于演示业务流程。在方案文档和答辩时主动说明这一点比被追问时解释要体面得多。// js/auth.js —— 本地用户表与登录态 (function (global) { var USER_KEY users; var SESSION_KEY session; function users() { return Store.read(USER_KEY, []); // [{ username, password, nickname, createdAt }] } // 注册用户名唯一 两次密码一致 长度校验 function register(username, password, confirm) { username (username || ).trim(); if (username.length 3 || username.length 16) { return { ok: false, msg: 用户名需 316 个字符 }; } if (password.length 6) { return { ok: false, msg: 密码至少 6 位 }; } if (password ! confirm) { return { ok: false, msg: 两次输入的密码不一致 }; } var list users(); for (var i 0; i list.length; i) { if (list[i].username username) { return { ok: false, msg: 该用户名已被注册 }; } } list.push({ username: username, password: password, // 演示用真实项目必须由服务端加盐存储 nickname: username, createdAt: Date.now() }); Store.write(USER_KEY, list); return { ok: true, msg: 注册成功 }; } // 登录成功后写会话会话里带过期时间戳 function login(username, password, remember) { var list users(); var hit null; for (var i 0; i list.length; i) { if (list[i].username username list[i].password password) { hit list[i]; break; } } if (!hit) return { ok: false, msg: 用户名或密码错误 }; var ttl remember ? 7 * 24 * 3600 * 1000 : 2 * 3600 * 1000; Store.write(SESSION_KEY, { username: hit.username, nickname: hit.nickname, expire: Date.now() ttl }); return { ok: true, msg: 登录成功 }; } // 取当前登录用户过期自动清掉 function current() { var s Store.read(SESSION_KEY, null); if (!s) return null; if (s.expire s.expire Date.now()) { Store.remove(SESSION_KEY); return null; } return s; } function logout() { Store.remove(SESSION_KEY); } global.Auth { register: register, login: login, current: current, logout: logout }; })(window);返回值统一用{ ok, msg }这种结构好处是页面脚本只需要写if (!res.ok) showTip(res.msg)不用在每个分支里各写一套提示文案。remember参数控制会话有效期勾选「记住我」给 7 天不勾选给 2 小时这个差异在演示时能直观体现出来。过期判断放在current()里而不是靠定时器是因为页面跳转后定时器就没了只有每次读取时检查才可靠。密码字段的注释必须留着这是给读代码的人也包括答辩老师看的说明。3.3 购物车与订单的持久化结构购物车按用户名分开存未登录用户也可以用临时身份加购登录后合并。这个设计能让「未登录也能逛、能加购结算时才拦」的体验成立。// js/cart.js —— 购物车读写与金额计算 (function (global) { // 结构{ 用户名或访客标识: [{ id, count, checked, addedAt }] } var CART_KEY cart; var GUEST __guest__; function owner() { var u Auth.current(); return u ? u.username : GUEST; } function all() { var map Store.read(CART_KEY, {}); return map[owner()] || []; } // 加购已存在则累加且不超过库存 function add(goodsId, count) { count count || 1; var goods Goods.byId(goodsId); if (!goods) return { ok: false, msg: 商品不存在 }; var max goods.stock; var list all(); var hit null; for (var i 0; i list.length; i) { if (list[i].id goodsId) { hit list[i]; break; } } if (hit) { hit.count Math.min(hit.count count, max); } else { list.push({ id: goodsId, count: Math.min(count, max), checked: true, addedAt: Date.now() }); } save(list); return { ok: true, msg: 已加入购物车 }; } function save(list) { Store.update(CART_KEY, {}, function (map) { map[owner()] list; return map; }); } // 金额以「分」为单位做整数运算最后再除 100规避浮点误差 function total() { var sum 0; all().forEach(function (row) { if (!row.checked) return; var goods Goods.byId(row.id); if (!goods) return; sum Math.round(goods.price * 100) * row.count; }); return sum / 100; } global.Cart { all: all, add: add, save: save, total: total, GUEST: GUEST }; })(window);owner()这一层间接让购物车有了「访客态」和「登录态」两套数据。用户登录后第一次进购物车页把__guest__下的条目合并到用户名下再清空访客车这个合并逻辑放在登录成功后的回调里执行只需写一次。total()里先乘 100 取整再累加是血泪经验——直接浮点相加199.00 加 39.90 会得到 238.90000000000003页面上显示出来非常难看用toFixed(2)只能掩盖显示层问题若还要按金额做判断比如满 199 减 20依然会翻车。4. 页面模块实现从商品列表渲染到购物车结算的完整链路数据结构定好之后页面实现基本是机械劳动。这一章按用户实际走一遍的顺序把四个关键环节的代码摊开每一段都可以直接搬进项目里改。4.1 商品列表的模板渲染与分类筛选列表渲染的核心是「先拼字符串再一次性写入 DOM」而不是在循环里反复append。后者在商品数量多的时候会造成明显的卡顿而且容易因为重排次数过多导致点击事件错位。// js/page-index.js —— 首页商品列表 $(function () { var currentCategory all; function render() { var list Goods.byCategory(currentCategory); var html list.map(function (g) { // 用>// js/page-detail.js —— 详情页加载与登录拦截 $(function () { var id getQuery(id); // 从 location.search 解析 var goods Goods.byId(id); if (!goods) { $(#detailBox).html(p classempty商品不存在或已下架a hrefindex.html返回首页/a/p); return; } $(#detailBox).html( img classdetail__img src goods.cover alt goods.name div classdetail__info h1 goods.name /h1 p classdetail__price price goods.price.toFixed(2) /p p classdetail__desc goods.desc /p p classdetail__stock库存 goods.stock 件/p button classbtn btn-primary idbtnBuy立即购买/button /div ); // 立即购买未登录则带上回跳地址去登录页 $(#btnBuy).on(click, function () { if (!Auth.current()) { // redirect 里放当前页完整地址登录成功后原路返回 location.href login.html?redirect encodeURIComponent(location.href); return; } Cart.add(goods.id, 1); location.href cart.html; }); });关键在于redirect参数带的是location.href而不是写死的cart.html。用户可能是在详情页点购买被拦也可能是在购物车点结算被拦回跳地址必须动态传否则从详情页登录完直接跳到购物车用户会以为自己点错了。encodeURIComponent不能省完整 URL 里带?idp1001不编码的话外层参数会被截断回跳后 id 丢失页面显示「商品不存在」——这个 bug 排查起来相当费劲因为地址栏看起来「差不多是对的」。登录页接收这个参数后在登录成功的回调里跳转同时要做一次白名单校验只允许跳回本站路径避免被构造外部地址。// js/page-login.js —— 登录成功后的安全回跳 function back() { var redirect getQuery(redirect); // 只允许站内相对路径或同源地址防止开放重定向 if (redirect redirect.indexOf(location.origin) 0) { location.href redirect; } else { location.href index.html; } }indexOf(location.origin) 0这个判断看起来简单但它挡掉了绝大多数学术演示场景下会被问到的「你这跳转能不能跳到别的网站」的追问。参数校验的意识在纯前端项目里同样成立。4.3 购物车数量增减与合计计算购物车页的交互密度最高数量加减、手动输入、勾选、全选、删除、合计。这里的核心原则是「任何一次操作都先改数据、再整体重渲染」而不是去操作某一个 DOM 节点的文本。// js/page-cart.js —— 购物车交互 $(function () { function render() { var list Cart.all(); if (!list.length) { $(#cartList).html(li classempty购物车还是空的a hrefindex.html去逛逛/a/li); $(#cartTotal).text(0.00); return; } var html list.map(function (row) { var g Goods.byId(row.id); if (!g) return ; return li classcart-row style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
阅读完成 · 觉得有帮助?
咨询建站