纯前端实现注册登录:从表单到localStorage的完整指南

📅 发布时间:2026/10/9 10:41:55
纯前端实现注册登录:从表单到localStorage的完整指南
简介这是一份面向前端初学者与Web开发学习者的HTML注册登录示例聚焦用户交互与前端表单校验特别适合用来理解正则匹配、控件绑定与页面跳转等基础知识。资源共4个文件包含2个HTML页面和2张JPG背景图片压缩包仅559KB轻量易用可离线打开运行也能直接作为课堂练习或实验模板。功能上实现了用户信息填写、单选/多选操作、下拉框选择用户名与密码均通过正则表达式进行校验任何栏目未通过校验或空白都无法注册、提交注册成功后先进入成功提示页随后自动跳转回注册界面便于循环演示。代码覆盖了文本输入、下拉列表、选项按钮、事件绑定与表单提交拦截等典型环节配合提供的背景图片页面效果直观适合初学者对照拆解快速搭出可复用的前端注册登录原型。目前已有8690人学习/下载对于需要现成示例来练习HTML表单与校验逻辑的开发者来说实用价值较高。1. HTML 实现用户注册登录一个表单两条核心链路做过前端的人十有八九被问过一句话“先帮我搞个注册登录样式不难。”这需求听起来简单但真正落地的核心不在页面上那几个输入框而在于两条链路注册的写入链路和登录的校验链路。用纯 HTML 配合原生 JavaScript再借助 localStorage 与 sessionStorage完全可以交付一个能跑、能演示、能继续迭代的注册登录 demo。本文的目标读者就是三类人刚开始学 htmlcssjs 基础语法的新手、要交课设或毕设演示页的学生、以及需要在项目前期快速出一版可点流程的从业者。先把一个小结论放在前面纯前端做注册登录的价值在于跑通流程别把它当成生产级鉴权方案。2. 搭建注册与登录表单字段设计、前端校验与提交拦截2.1 注册字段怎么定账号、密码、确认密码的默认取舍一个“简单”的注册页我一般只放三个核心字段用户名、密码、确认密码。很多初学者喜欢加上邮箱、手机号、性别、头像但在纯前端记忆的模型下那些字段只会增加校验复杂度和存储体积。邮箱和手机号属于“找回密码”的基础设施纯前端没有邮件服务和短信网关加了反而没法闭环。用户名建议限制为 212 个字符密码至少 6 位、最多 20 位——这两个上限不是为了刁难用户而是避免后期在 localStorage 里塞进超大字符串。确认密码字段可以不要但对新人友好也能在提交前提前拦掉大部分手误所以保留是最稳妥的选择。2.2 表单结构与内置校验一个 html 表单的后端在哪先看注册页的表单结构。这里我刻意没有引入任何框架方便你直接复制到本地跑。form idregisterForm autocompleteon label forusername用户名/label input typetext idusername nameusername required minlength2 maxlength12 placeholder2-12 个字符 autocompleteusername label forpassword密码/label input typepassword idpassword namepassword required minlength6 maxlength20 placeholder至少 6 位 autocompletenew-password label forconfirm确认密码/label input typepassword idconfirm nameconfirm required minlength6 maxlength20 autocompletenew-password button typesubmit注册/button /form这段 html 表单的写法有讲究。required、minlength、maxlength是浏览器原生校验属性不满足条件时压根不会触发 submit 事件能省掉一部分手写判断。autocomplete属性容易被忽略用户名填username密码填new-password这样浏览器密码管理器不会把“注册密码”误当成“登录旧密码”去自动填充减少现场的玄学问题。2.3 提交拦截与校验逻辑为什么必须用 submit 事件表单按钮如果写成typebutton意味着你没有走浏览器原生的提交通道后面所有 Enter 键触发、表单语义全会乱掉。正确的做法是让按钮保持typesubmit然后在 JavaScript 里监听 submit 事件并e.preventDefault()。const form document.getElementById(registerForm); form.addEventListener(submit, function (e) { e.preventDefault(); const username document.getElementById(username).value.trim(); const password document.getElementById(password).value; const confirm document.getElementById(confirm).value; if (username.length 2) { alert(用户名至少 2 个字符); return; } if (password.length 6) { alert(密码至少 6 位); return; } if (password ! confirm) { alert(两次输入的密码不一致); return; } saveUser(username, password); });这段代码做的事很简单但有三个参数和时机值得注意。第一trim()只用在用户名上密码不 trim因为密码里的空格是合法字符盲目去空格会改变用户原始输入。第二先做长度判断再做一致性判断顺序不要倒过来——如果两个密码都是空字符串长度校验已经拦住了不会走到后面的全等判断。第三e.preventDefault()必须在同步代码开头执行如果写成异步后再阻止默认行为页面可能已经刷新了这是典型的表单翻车点。2.4 把用户数据写进 localStorage键名设计与重复账号拦截用户点注册之后数据落哪我通常维护一个统一键名users整表存储为 JSON 数组。每个用户是一条记录至少要包含username和password两个字段后面要加注册时间或盐值再扩展。function saveUser(username, password) { const users JSON.parse(localStorage.getItem(users) || []); const exists users.some((u) u.username username); if (exists) { alert(该用户名已被注册); return; } users.push({ username, password }); localStorage.setItem(users, JSON.stringify(users)); location.href login.html; }localStorage.getItem(users) || []是必写的兜底第一次注册时整个键不存在返回null直接传给JSON.parse会报错导致白屏。Array.some()负责查重这里暴露了纯前端实现的一个局限——注册页和登录页必须同源否则读到的不是同一份 localStorage。你把它部署到公网可能被绕过但在本地演示的语境里这就是最直接可靠的数据持久层。3. 登录比对与会话状态sessionStorage 模拟登录态3.1 登录页数据比对从 localStorage 读到账号再匹配登录页和注册页是两个文件但它们共享浏览器同源下的 localStorage。登录表单结构与注册页几乎一致只是少一个确认密码字段按钮文字换成“登录”。真正的差异在提交逻辑const loginForm document.getElementById(loginForm); loginForm.addEventListener(submit, function (e) { e.preventDefault(); const username document.getElementById(username).value.trim(); const password document.getElementById(password).value; const users JSON.parse(localStorage.getItem(users) || []); const user users.find((u) u.username username); if (!user || user.password ! password) { alert(账号或密码错误); return; } sessionStorage.setItem(currentUser, JSON.stringify({ username: user.username, loginAt: Date.now() })); location.href index.html; });这里用find先按用户名找记录找不到直接进错误分支。注意我刻意把“用户不存在”和“密码错误”合并成同一条提示避免通过回包差异遍历出已注册账号这对纯前端拷进本地时可能显得防御过度但代码习惯一旦养成以后接后端接口时不容易漏。登录成功后的关键动作是把当前用户写入sessionStorage键名我固定为currentUser。存loginAt时间戳是低成本的做法方便以后实现“登录超过 N 小时要求重新登录”之类的伪过期机制。3.2 sessionStorage 与 localStorage 怎么分工一张表说清楚很多新手分不清这两个存储 API经常写混。它们的差异直接决定产品行为对比项localStoragesessionStorage生命周期手动清除才消失标签页关闭即失效跨标签页同源下多个标签页共享每个标签页独立典型用途users 用户表长期保留currentUser 登录态临时会话隐私模式可能不可用写入可能抛异常关闭窗口后清空一句话记忆用户表这类“注册过就一直在”的数据放 localStorage登录状态这类“关页就该没”的数据放 sessionStorage。反过来的话会出现刷新页面就掉登录或者关了窗口再打开还显示已登录的诡异现象。3.3 页面加载时恢复登录态导航栏与用户区联动登录成功后跳转到的index.html在页面加载时要主动读取登录态再决定导航栏右边显示“登录 / 注册”还是显示用户名和退出按钮。否则每个页面打开都显示未登录用户会认为登录白做了。function getCurrentUser() { const raw sessionStorage.getItem(currentUser); if (!raw) return null; try { return JSON.parse(raw); } catch (e) { return null; } } const user getCurrentUser(); const navRight document.getElementById(navRight); if (user) { navRight.innerHTML 欢迎${user.username} button idlogoutBtn退出/button; document.getElementById(logoutBtn).addEventListener(click, function () { sessionStorage.removeItem(currentUser); location.reload(); }); } else { navRight.innerHTML a hreflogin.html登录/a / a hrefregister.html注册/a; }JSON.parse包进try...catch是必要的用户或旧代码可能往currentUser里写过非法字符串解析失败时直接当作未登录处理而不是让整个页面崩溃。退出按钮只是删掉sessionStorage里的键然后刷新页面让导航栏重新渲染成未登录状态。整套流程没有网络请求但交互闭环是完整的。4. 密码存储别用明文SHA-256 与盐值的落地写法4.1 明文存储为什么是黑匣子风险如果按第 2 章的写法直接把password明文推进数组整个系统就是一键可脱库的状态。你可能会说“这不就是个课设吗”但就算演示用的音箱旁边坐着甲方他看到你打开 DevTools 的 Application 面板里面的密码清清楚楚这个项目在信任层面就已经扣分了。更现实的风险是用户注册时如果沿用了常用密码意味着你把用户在其他平台的凭证也一起泄露了。纯前端没有真正的绝对安全——代码里的算法全部暴露在浏览器端攻击者完全可以逆向出哈希逻辑去伪造。但我们要的不是“无法破解”而是“泄露了也不能直接拿明文去撞库”。存 SHA-256 摘要能让 localStorage 被导出一瞬间不至于直接交出可读密码。这是成本最低、也最负责任的方案。4.2 用 window.crypto.subtle 计算 SHA-256现代浏览器内置了 Web Crypto API不需要再引第三方库。关键点是crypto.subtle.digest是异步方法返回值是ArrayBuffer要转成十六进制字符串才能方便地存进 localStorage。async function sha256(text) { const data new TextEncoder().encode(text); const digest await crypto.subtle.digest(SHA-256, data); return Array.from(new Uint8Array(digest)) .map((b) b.toString(16).padStart(2, 0)) .join(); }TextEncoder是用来把字符串转成 UTF-8 字节序列的直接用crypto.subtle.digest会报错“DataCloneError”这是新手最常见的一步卡壳。输出端padStart(2, 0)确保每个字节都补成两位十六进制比如字节值 15 转出来是f补位后才是0f否则拼出来的摘要长度不稳定后续比对必出问题。4.3 加盐的简单约定用户名做盐再拼接单纯 SHA-256 扛不住彩虹表。给密码加一点盐是常识性做法但纯前端语境里盐也得存下来。我的做法是不额外生成随机盐字段直接用“用户名 分隔符 密码”作为哈希输入。async function hashPassword(username, password) { return await sha256(username | password); }注册时把返回值存进对象{ username, password: hashed }。登录时重新拼一次把计算出的摘要和存储的摘要做字符串全等比较。用用户名当盐有个缺点不同用户如果密码相同哈希结果仍不同这个没问题但同一用户在两个系统用相同用户名和密码时哈希仍然相同。它只提升了“全表撞库”的门槛并不是完美的盐值方案。要更严谨就在用户对象里存一个salt字段用crypto.getRandomValues生成随机串。不过那会让代码复杂度上一个台阶简单项目用用户名加盐性价比已经足够。4.4 各种存储写法的成本对比方案存储内容示例导出 localStorage 后的风险可逆性明文password: 123456直接泄露完全可逆自写异或/替换password: y6z5s5容易被猜出算法弱加密可逆SHA-256password: a665a459...可撞库弱密码单向彩虹表可查SHA-256 盐password: 9f6c...撞库成本明显上升单向基本不可查我在实际项目里从明文迁到哈希后改动点只有三处注册写入前、登录比对前、密码字段的长度放宽一点。其余存储和会话逻辑完全不动。这个迁移成本低到你没有理由继续明文存。5. 注册登录模块高频踩坑清单5 条翻车现场5.1 iframe 里的 localStorage 静默失效现象把注册登录页放进后台管理系统的 iframe 里用户填写完点注册页面没有报错但跳转登录后账号不存在。原因第三方 iframe 若无allow-same-origin或浏览器开启严格隐私设置localStorage 的存取会被拦截。更隐蔽的是localStorage.getItem在隐私模式下可能直接抛SecurityError而你没做异常捕获导致后面所有逻辑没有执行。解决把所有的 storage 读写包进函数内部统一try...catch出错时降级为内存对象。同时给 iframe 标签补上allow-same-origin属性。判断当前环境能不能用 localStorage不要靠猜靠执行结果说话。5.2 刷新之后登录态说丢就丢现象用户登录成功进入首页按 F5 刷新后页面又变回未登录状态。去 sessionStorage 里看currentUser这个键还在。原因这大概率不是丢失而是登录状态存进了 localStorage刷新不丢然后你在导航栏逻辑里读的是 sessionStorage两边键名不一致读到null就渲染成未登录。如果反过来登录态存进 sessionStorage打开新标签页时那个新页面读不到旧标签页的登录态也会“看起来像丢了一样”。解决明确约定——登录态只存 sessionStorage读的时候从同一个键currentUser取。新开标签页的“不同步”是 sessionStorage 的正常行为不能算 bug真的要让多个标签页共享登录态就改用 localStorage并接受“关浏览器再开仍在线”的代价。5.3 中文账号和首尾空格引发比对失败现象用户在用户名里输入“ 张三 ”前后有空格注册成功。登录时输入“张三”提示账号或密码错误。或者注册时用中文全角空格登录时用半角空格怎么都对不上。原因注册时没做trim()或者只在注册时做了、登录时没做两条链路处理不一致就会造成“隐形双份账号”。中文全角空格和英文半角空格是不同字符肉眼分不出来但字符串比较是严格按码点走的。解决统一定义一个normalizeUsername函数把trim()和全角空格替换写在一起u.username.trim().replace(/\u3000/g, )。注册、登录、查重、渲染四处的用户名都先经过它确保进入比对流程的值是干净的。5.4 哈希比对时类型不一致导致的相等性翻车现象用户注册时存的是十六进制字符串登录时你拿到的ArrayBuffer没经过转换直接用比较结果永远不相等所有密码都提示错误。原因crypto.subtle.digest返回的是ArrayBuffer转成十六进制字符串是必须的。如果你在某个版本里转成了 Base64又在另一处用十六进制比较或者只转了一边比对一定失败。这种 bug 有点黑匣子因为页面不报错就是登不进去。解决把转换过程收敛成一个公共函数比如本章第 4 节的sha256全项目只允许通过它产出摘要。另外在登录比对前打一条临时console.log把两边值的类型和一眼前 8 位都打出来能快速发现是格式问题还是密码本身错误。5.5 preventDefault 失效导致表单提交后页面刷新现象表单填完点提交页面闪了一下然后刷新注册的数据没存入 localStorage反而 URL 上多了一串参数。原因最常见的是监听事件写成了click而不是submit或者按钮类型是typesubmit但语法上事件绑定在了一个不存在的变量上浏览器默认提交行为照常执行。还有一个隐蔽原因JS 文件在表单 HTML 之前被加载document.getElementById(registerForm)拿到null后面的addEventListener直接抛错默认动作没有任何拦截。解决把整个script放到/body前或者用DOMContentLoaded包一层。提交事件一律监听submit而非click并且在函数第一行写e.preventDefault()不给默认刷新留机会。调试时看 Network 面板如果提交动作产生了 document 请求说明拦截根本没挂上。6. 进阶一步把纯前端注册登录封装成可替换的小模块到这步整个流程已经能跑了但代码还散在页面里以后要接后端时会想骂人。常见做法是把用户表和登录态操作收敛成一个独立模块我习惯命名为auth.js对外只暴露四个方法register、login、logout、current。页面层永远只调接口不直接读写 storage这样将来替换成 axios 请求时页面代码一行都不用动。const AuthStore { _readUsers() { return JSON.parse(localStorage.getItem(users) || []); }, async register(username, password) { const users this._readUsers(); if (users.some((u) u.username username)) return { ok: false, msg: 用户名已存在 }; users.push({ username, password: await sha256(${username}|${password}) }); localStorage.setItem(users, JSON.stringify(users)); return { ok: true }; }, async login(username, password) { const user this._readUsers().find((u) u.username username); if (!user || user.password ! await sha256(${username}|${password})) { return { ok: false, msg: 账号或密码错误 }; } sessionStorage.setItem(currentUser, JSON.stringify({ username: user.username, loginAt: Date.now() })); return { ok: true }; }, logout() { sessionStorage.removeItem(currentUser); }, current() { const raw sessionStorage.getItem(currentUser); return raw ? JSON.parse(raw) : null; } };验证这套东西是否健壮我的习惯是开两个浏览器窗口一个跑注册流程另一个跑登录流程专门测会话隔离再用隐身窗口跑一遍确认隐私模式下 localStorage 的表现。至于要不要继续往上加“记住我”“修改密码”“找回密码”我的建议是按需加——纯前端方案每多一个功能就多一层安全隐患做到“注册、登录、退出、状态恢复”四件事就足够交付了。最后说一个自己踩过的教训早期我把用户表存进了 sessionStorage结果每次刷新浏览器都丢账号当时以为是代码写错了搞了大半天才发现是生命周期选错了。后来养成了习惯凡是“关了页面还要存在”的数据一律进 localStorage凡是“会话结束就该失效”的状态一律进 sessionStorage写完就再没出过这毛病。希望帮到你。本文还有配套的精品资源点击获取