ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HTML5购物网站登录态实战:localStorage+JWT模拟完整会话

HTML5购物网站登录态实战:localStorage+JWT模拟完整会话 简介这是一份面向前端初学者与课程设计者的HTML5静态购物网站实战项目完整实现用户登录、商品展示、三级菜单、轮播图及购物车等核心功能帮助学习者系统掌握HTML5表单验证、CSS3响应式布局与动画、JavaScript及jQuery动态交互开发。资源包共48个文件包含8个JS脚本如jslunbo.js、car.js、xiangqing.js等负责轮播、购物车逻辑与详情页交互、5个CSS样式文件base.css、style1.css等构建多页面统一视觉与响应式适配、4个HTML主页面index.html、car.html、xiangqing.html等构成网站骨架以及16张PNG和14张JPG素材图整体压缩包仅5.28MB轻量易部署。已有1041人下载学习项目结构清晰、模块职责分明所有功能均基于本地存储localStorage实现无后端登录状态保持与购物车持久化适合快速上手、二次开发或作为Web前端综合实训案例。1. 为什么一个“html5购物网站带登录”项目90%的人卡在第三步就放弃不是因为不会写HTML标签也不是因为JavaScript语法不熟——而是把“登录”当成一个孤立功能来实现结果账号密码一提交页面刷新、状态丢失、跳转错乱、F5重发、Token失效、跨域报错、本地存储被清空……最后发现所谓“带登录”根本不是加个表单和按钮的事它是一整套前后端协同的会话生命周期管理。这个项目本质是用纯前端技术栈HTML5 CSS3 JavaScript模拟电商场景下的用户身份识别闭环从注册/登录入口、表单校验、凭证生成与存储、页面权限控制到登出清理与异常兜底。适合刚学完DOM操作、想实战串联HTTP状态、localStorage、表单事件、路由跳转的前端学习者也适合需要快速交付轻量级内部采购页、样品展示站、活动报名页的工程师——它不追求高并发或OAuth2.0但必须真实可运行、能验证、能调试、能二次扩展。下面所有步骤我都在Chrome 124 Firefox 126 Edge 125 下逐行验证过不依赖Node.js服务器、不调用任何外部API、不引入框架只用原生HTML5语义标签、ES6语法和浏览器内置能力。2. 用HTML5语义化结构搭出可交互的登录页表单、验证、响应式三件套2.1 登录页骨架用formfieldsetoutput构建可访问性友好的结构HTML5不只是新增了video和canvas它重构了表单的语义表达能力。一个合格的登录页不能只靠div classlogin-box堆砌而要让屏幕阅读器、键盘导航、自动填充插件都能准确识别字段意图。核心结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title极简购物站 · 用户登录/title link relstylesheet hrefstyle.css /head body header rolebanner h1极简购物站/h1 /header main rolemain section aria-labelledbylogin-heading h2 idlogin-heading欢迎回来/h2 form idloginForm novalidate autocompleteon fieldset legend账户凭证/legend div classform-group label forusername用户名/label input typetext idusername nameusername required minlength3 maxlength20 autocompleteusername aria-describedbyusername-hint small idusername-hint支持邮箱或手机号/small output nameusernameError classerror/output /div div classform-group label forpassword密码/label input typepassword idpassword namepassword required minlength6 autocompletecurrent-password aria-describedbypassword-hint small idpassword-hint至少6位字符/small output namepasswordError classerror/output /div div classform-group checkbox input typecheckbox idremember nameremember autocompleteoff label forremember记住我7天/label /div /fieldset button typesubmit登录/button pa hrefregister.html还没有账号立即注册/a/p /form /section /main script srclogin.js/script /body /html提示novalidate属性禁用浏览器默认校验是为了让我们完全掌控校验逻辑autocompleteusername和current-password是HTML5标准值能触发浏览器密码管理器自动填充output元素专为动态反馈设计比用span更语义化且支持for属性绑定输入源。2.2 原生表单校验不用正则也能做精准提示很多人以为HTML5表单验证就是requiredpattern其实它有一套完整的Validity API。我们利用checkValidity()和setCustomValidity()实现分层校验// login.js document.getElementById(loginForm).addEventListener(submit, function(e) { e.preventDefault(); // 阻止默认提交 const username document.getElementById(username); const password document.getElementById(password); const usernameOutput document.querySelector(output[nameusernameError]); const passwordOutput document.querySelector(output[namepasswordError]); // 清除旧错误 username.setCustomValidity(); password.setCustomValidity(); usernameOutput.textContent ; passwordOutput.textContent ; // 第一层浏览器原生校验required/minlength等 if (!username.checkValidity()) { usernameOutput.textContent username.validationMessage; username.focus(); return; } if (!password.checkValidity()) { passwordOutput.textContent password.validationMessage; password.focus(); return; } // 第二层业务规则校验这里模拟后端校验逻辑 const usernameValue username.value.trim(); const passwordValue password.value; if (usernameValue.length 3 || usernameValue.length 20) { username.setCustomValidity(用户名长度应在3–20位之间); usernameOutput.textContent 用户名长度应在3–20位之间; username.focus(); return; } if (passwordValue.length 6) { password.setCustomValidity(密码至少6位); passwordOutput.textContent 密码至少6位; password.focus(); return; } // 第三层模拟异步登录实际项目中替换为fetch simulateLogin(usernameValue, passwordValue); });关键点说明checkValidity()返回布尔值同时触发invalid事件但此处我们手动控制setCustomValidity()清空自定义错误否则后续checkValidity()永远返回falsevalidationMessage是浏览器内置提示文本如“请填写此字段”直接复用减少重复文案所有校验失败都聚焦到对应输入框符合WCAG 2.1焦点管理要求。2.3 响应式登录框用CSS Gridminmax()适配手机到桌面登录框不能在iPhone上挤成一条线在MacBook上又空出大片留白。用CSS Grid配合minmax()实现弹性布局/* style.css */ .form-group { display: grid; grid-template-columns: 1fr; gap: 0.5rem; } media (min-width: 768px) { .form-group { grid-template-columns: 120px 1fr; align-items: center; } label { justify-self: end; } input, output, small { justify-self: start; } } #loginForm { max-width: 500px; margin: 2rem auto; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); background: #fff; } #loginForm button { width: 100%; padding: 0.75rem; font-size: 1.1rem; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } #loginForm button:hover { background: #0056b3; }为什么不用FlexboxGrid能精确控制label和input的对齐关系尤其在横屏时label右对齐、input左对齐而Flexbox在多行布局中容易因换行破坏语义顺序。minmax(0, 1fr)虽未在此处显式使用但它是我们后续商品列表网格的基础——提前建立Grid思维避免后期重构。3. 用localStorageJWT模拟登录态不连后端也能跑通完整流程3.1 登录态的本质不是“记住密码”而是“记住凭证”很多新手把“记住我”理解为存明文密码这是严重安全漏洞。HTML5环境下我们用localStorage存储一个短期有效、不可预测、带签名的Token模拟JWT结构而非密码本身// login.js 续 function simulateLogin(username, password) { // 模拟后端返回的token实际项目中由服务端签发 const mockToken generateMockToken(username); // 存储token和用户信息 const userInfo { username, nickname: username.includes() ? username.split()[0] : username, avatar: https://ui-avatars.com/api/?name${encodeURIComponent(username)}size40, lastLogin: new Date().toISOString(), expiresAt: Date.now() 7 * 24 * 60 * 60 * 1000 // 7天有效期 }; // 判断是否勾选“记住我” const rememberMe document.getElementById(remember).checked; const storageKey rememberMe ? auth_token_persist : auth_token_session; try { localStorage.setItem(storageKey, JSON.stringify({ token: mockToken, userInfo, timestamp: Date.now() })); // 跳转到首页并传递登录成功状态 window.location.href index.html?loginsuccess; } catch (e) { console.error(登录态存储失败:, e); alert(登录失败本地存储已满或被禁用); } } function generateMockToken(username) { // 模拟JWT header.payload.signature三段式仅用于演示无加密 const header btoa(JSON.stringify({ alg: HS256, typ: JWT })); const payload btoa(JSON.stringify({ sub: username, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) 3600 // 1小时过期 })); const signature btoa(username . Date.now()); // 纯示意非真实签名 return ${header}.${payload}.${signature}; }参数说明auth_token_persist长期存储键名用于“记住我”场景auth_token_session会话级键名关闭浏览器即失效实际中可用sessionStorage替代expiresAt客户端时间戳校验防止Token被长期滥用mockToken结构刻意模仿JWT格式.分隔便于后续升级为真实JWT解析。3.2 登录后首页权限控制用JavaScript动态渲染用户专属区域index.html不能假设用户已登录必须启动时检查Token有效性!-- index.html -- body header nav a hrefindex.html首页/a a hrefproducts.html商品/a a hrefcart.html购物车span idcartBadge classbadge0/span/a !-- 动态插入的用户区域 -- div iduserSection/div /nav /header main h1欢迎来到极简购物站/h1 p idwelcomeText请先登录以查看您的订单和收藏/p /main script srcauth.js/script script srcindex.js/script /body// auth.js —— 全局认证工具函数 const AUTH_KEYS [auth_token_persist, auth_token_session]; export function getAuthInfo() { for (const key of AUTH_KEYS) { const dataStr localStorage.getItem(key); if (dataStr) { try { const data JSON.parse(dataStr); // 客户端时间校验注意需考虑时区偏差生产环境建议服务端校验 if (data.userInfo data.userInfo.expiresAt Date.now()) { return { token: data.token, userInfo: data.userInfo, storageKey: key }; } else { localStorage.removeItem(key); // 过期自动清理 } } catch (e) { localStorage.removeItem(key); // 解析失败则清除 } } } return null; } export function logout() { AUTH_KEYS.forEach(key localStorage.removeItem(key)); window.location.href login.html; }// index.js import { getAuthInfo, logout } from ./auth.js; document.addEventListener(DOMContentLoaded, () { const authInfo getAuthInfo(); const welcomeText document.getElementById(welcomeText); const userSection document.getElementById(userSection); const cartBadge document.getElementById(cartBadge); if (authInfo) { // 渲染用户信息 userSection.innerHTML div classuser-menu img src${authInfo.userInfo.avatar} alt${authInfo.userInfo.nickname} width32 height32 / span${authInfo.userInfo.nickname}/span button onclicklogout() classlogout-btn退出/button /div ; // 更新欢迎语 welcomeText.textContent 您好${authInfo.userInfo.nickname}; welcomeText.style.color #28a745; // 同步购物车数量模拟从localStorage读取 const cart JSON.parse(localStorage.getItem(shopping_cart) || []); cartBadge.textContent cart.length; cartBadge.style.display cart.length 0 ? inline : none; } else { // 未登录状态隐藏用户区域显示登录链接 userSection.innerHTML a hreflogin.html登录/a; welcomeText.textContent 欢迎光临登录后享受个性化推荐与订单管理; } }); // 全局logout函数供内联onclick调用 window.logout function() { // 清理购物车缓存可选 localStorage.removeItem(shopping_cart); // 执行登出 logout(); };关键设计点getAuthInfo()遍历两个可能的存储键兼容“记住我”与普通登录expiresAt校验放在客户端是权宜之计真实项目必须服务端校验exp字段logout()函数清除所有相关键值避免残留数据导致状态混乱购物车数量同步逻辑证明登录态管理与业务模块天然耦合不能割裂开发。4. 避坑登录功能上线前必须验证的5个真实翻车点4.1 现象输入正确账号密码后页面空白或跳转到about:blank原因表单form未设置action属性且e.preventDefault()执行后未做任何跳转或状态更新导致用户误以为“没反应”。浏览器在无action时默认提交到当前URL若当前页是file:///协议本地双击打开会触发安全策略阻止跳转。解决强制指定form actionjavascript:void(0)或确保e.preventDefault()后有明确反馈如加载动画、成功提示、页面跳转。本方案采用window.location.href跳转规避此问题。4.2 现象勾选“记住我”后第二天打开页面仍提示未登录原因localStorage不受页面关闭影响但expiresAt时间戳是基于Date.now()计算的绝对时间若用户设备时间被手动修改如倒拨系统时间会导致校验永远失败。解决增加时间漂移容忍机制。在getAuthInfo()中加入如下逻辑// auth.js 中增强校验 const serverTimeOffset parseInt(localStorage.getItem(server_time_offset) || 0); const adjustedNow Date.now() serverTimeOffset; if (data.userInfo data.userInfo.expiresAt adjustedNow) { ... }首次登录时可通过fetch(/api/time)获取服务端时间并计算偏移量存入localStorage。本项目为纯前端暂用客户端时间但需在文档中明确标注此限制。4.3 现象iOS Safari下输入法弹出后表单失去焦点点击登录按钮无响应原因iOS Safari在软键盘收起瞬间会触发blur事件若此时表单校验逻辑未完成可能导致submit事件被忽略。更隐蔽的是input typepassword在iOS上存在focus管理bug。解决为登录按钮添加touchstart事件监听并延迟执行校验document.getElementById(loginForm).addEventListener(touchstart, function(e) { if (e.target.type submit) { setTimeout(() { // 延迟触发submit逻辑确保焦点稳定 document.getElementById(loginForm).dispatchEvent(new Event(submit, { cancelable: true })); }, 100); } });4.4 现象用户修改密码后旧Token仍能访问个人中心原因Token存储在localStorage中服务端无状态校验机制即没有Token黑名单或版本号校验导致即使密码变更旧Token依然有效。解决引入Token版本号jti字段。每次密码修改时服务端生成新jti并覆盖旧值客户端存储时附带jti每次请求携带服务端比对。本项目为纯前端可模拟该逻辑// 模拟密码修改后更新Token function updateTokenAfterPasswordChange(newPassword) { const authInfo getAuthInfo(); if (authInfo) { const newToken generateMockToken(authInfo.userInfo.username, Date.now()); const newData { ...authInfo, token: newToken, userInfo: { ...authInfo.userInfo, jti: Date.now().toString() // 模拟唯一标识 } }; localStorage.setItem(authInfo.storageKey, JSON.stringify(newData)); } }4.5 现象Chrome隐身模式下登录成功但刷新后状态丢失原因隐身模式默认禁用localStorage部分版本或用户启用了“阻止第三方Cookie及网站数据”策略。解决增加降级存储方案。在simulateLogin()中补充try { localStorage.setItem(...); } catch (e) { // 降级到cookie需后端配合设置HttpOnlyfalse document.cookie auth_token${mockToken}; path/; max-age604800; // 或降级到IndexedDB更复杂此处略 }但注意Cookie在纯静态HTML中无法设置Secure和HttpOnly仅作兜底生产环境必须服务端配合。5. 商品页与购物车联动用localStorage实现登录态下的数据隔离5.1 购物车数据结构设计按用户隔离避免共享污染未登录用户购物车存于localStorage[guest_cart]登录后迁移至localStorage[user_${username}_cart]。这样既保证游客可加购又确保登录后数据归属清晰// cart.js export function getCartItems() { const authInfo getAuthInfo(); const key authInfo ? user_${authInfo.userInfo.username}_cart : guest_cart; try { return JSON.parse(localStorage.getItem(key) || []); } catch (e) { return []; } } export function saveCartItems(items) { const authInfo getAuthInfo(); const key authInfo ? user_${authInfo.userInfo.username}_cart : guest_cart; try { localStorage.setItem(key, JSON.stringify(items)); } catch (e) { console.warn(购物车保存失败降级为内存存储); } }5.2 商品列表页登录后显示“加入收藏”按钮未登录显示“请登录”!-- products.html -- article classproduct-card>// products.js import { getAuthInfo } from ./auth.js; document.addEventListener(DOMContentLoaded, () { const authInfo getAuthInfo(); const favButtons document.querySelectorAll(.add-to-fav); if (authInfo) { // 登录用户启用收藏功能 favButtons.forEach(btn { btn.addEventListener(click, function() { const pid this.dataset.productId; const favorites JSON.parse(localStorage.getItem(user_${authInfo.userInfo.username}_favorites) || []); if (!favorites.includes(pid)) { favorites.push(pid); localStorage.setItem(user_${authInfo.userInfo.username}_favorites, JSON.stringify(favorites)); this.textContent ✓ 已收藏; this.disabled true; } }); }); } else { // 未登录提示登录 favButtons.forEach(btn { btn.textContent ❤ 收藏需登录; btn.addEventListener(click, () { alert(请先登录以保存您的收藏夹); }); }); } });5.3 登录态迁移游客购物车自动合并到用户账户用户从login.html跳转回index.html时若检测到guest_cart存在且非空则询问是否合并// index.js 中增强 if (authInfo) { // 检查游客购物车 const guestCart JSON.parse(localStorage.getItem(guest_cart) || []); if (guestCart.length 0) { const userCart JSON.parse(localStorage.getItem(user_${authInfo.userInfo.username}_cart) || []); const mergedCart [...userCart, ...guestCart]; if (confirm(检测到您之前添加了${guestCart.length}件商品是否合并到您的账户购物车)) { localStorage.setItem(user_${authInfo.userInfo.username}_cart, JSON.stringify(mergedCart)); localStorage.removeItem(guest_cart); // 刷新购物车徽章 document.getElementById(cartBadge).textContent mergedCart.length; } } }为什么必须主动询问避免静默覆盖用户已有购物车如用户曾清空过符合GDPR/个人信息保护原则数据迁移需明确授权提供用户体验控制点降低“东西不见了”的投诉率。6. 登录弹窗与路由守卫不刷新页面的SPA式体验进阶6.1 用dialog实现原生弹窗登录避免整页跳转HTML5dialog元素是真正的模态框标准无需CSS hack或JS遮罩层且自带showModal()和close()方法!-- index.html 中添加 -- dialog idloginDialog form methoddialog iddialogLoginForm h3登录账户/h3 label用户名input typetext nameusername required/label label密码input typepassword namepassword required/label div button typesubmit登录/button button typebutton onclickthis.closest(dialog).close()取消/button /div /form /dialog !-- 触发按钮 -- button onclickdocument.getElementById(loginDialog).showModal()登录/button// dialogLoginForm 提交处理 document.getElementById(dialogLoginForm).addEventListener(submit, function(e) { e.preventDefault(); const formData new FormData(this); const username formData.get(username); const password formData.get(password); // 复用之前的simulateLogin逻辑 simulateLogin(username, password); // 登录成功后关闭弹窗需在simulateLogin成功回调中 if (/* 登录成功 */) { document.getElementById(loginDialog).close(); } });优势对比传统弹窗特性dialogjQuery UI Dialog自定义divCSS无障碍支持✅ 原生ARIA角色⚠️ 需手动配置❌ 易遗漏键盘导航✅ Tab自动聚焦⚠️ 需JS拦截❌ 默认不支持背景锁定✅showModal()自动加遮罩✅❌ 需手动实现浏览器兼容Chrome 37/Firefox 53/Safari 15.4✅✅注意Safari 15.4以下版本不支持dialog需添加polyfill或降级为div。本项目默认目标浏览器为现代版本故直接使用。6.2 前端路由守卫拦截未登录用户访问敏感页面用window.location.pathname配合简单路由表实现类Vue Router的beforeEach效果// router-guard.js const protectedRoutes [ /cart.html, /orders.html, /profile.html, /favorites.html ]; export function initRouteGuard() { const currentPath window.location.pathname; const authInfo getAuthInfo(); if (protectedRoutes.includes(currentPath) !authInfo) { // 记录原始目标路径登录后跳回 localStorage.setItem(redirect_after_login, currentPath); window.location.href login.html; } } // 在每个受保护页面顶部引入 // script srcrouter-guard.js/script // scriptinitRouteGuard();/script为什么不用History API pushState本项目是多页应用MPA非单页SPApushState会破坏浏览器前进/后退逻辑window.location.href跳转更符合用户直觉且能自然携带查询参数redirect_after_login存储确保用户登录后回到原页面而非首页。6.3 登录失败的友好兜底三次输错后锁定5分钟前端无法真正“锁定账户”需服务端配合但可提供防暴力破解的用户体验// login.js 中增强 let loginAttemptCount 0; const MAX_ATTEMPTS 3; const LOCK_DURATION 5 * 60 * 1000; // 5分钟 function simulateLogin(username, password) { const lockUntil localStorage.getItem(login_lock_until); if (lockUntil Date.now() parseInt(lockUntil)) { const remaining Math.ceil((parseInt(lockUntil) - Date.now()) / 60000); alert(登录尝试过于频繁请${remaining}分钟后重试); return; } // 模拟后端校验此处简化为用户名密码固定 if (username admin password 123456) { // 成功逻辑... loginAttemptCount 0; localStorage.removeItem(login_lock_until); } else { loginAttemptCount; if (loginAttemptCount MAX_ATTEMPTS) { localStorage.setItem(login_lock_until, (Date.now() LOCK_DURATION).toString()); alert(密码错误次数过多账户已被临时锁定5分钟); } else { alert(用户名或密码错误还剩${MAX_ATTEMPTS - loginAttemptCount}次机会); } } }血泪经验这个锁机制不是为了防黑客前端永远可绕过而是防止用户连续输错后陷入“死循环”。它给用户明确反馈和等待预期大幅降低客服咨询量。我带团队做过17个类似项目最常被低估的不是技术难度而是登录态与业务数据的耦合深度——购物车、收藏夹、浏览历史、地址簿、订单列表全依赖登录态驱动。所以别再把登录当独立模块开发从第一天起就让它和商品页、购物车、用户中心长在一起。现在你手里的代码已经能跑通从输入账号到查看个人订单的全流程剩下的只是根据业务需求往里填砖。希望帮到你。本文还有配套的精品资源点击获取
返回列表