HTML注册登录实现指南:从localStorage到真实API对接
简介这是一份面向前端初学者的HTML注册登录演示工程以简单直观的方式展示用户信息填写、单选多选、下拉框选择以及用户名与密码正则校验等常见表单交互。无论是文本输入、性别或爱好选择还是通过下拉框完成职业或城市选择页面都提供了完整的前端实现。凡是存在未通过校验或空白项页面都会阻止注册提交并在校验成功后跳转到注册成功界面稍后再自动返回注册页完整演示了前端表单从验证到跳转的闭环流程。压缩包共4个文件含2个HTML页面和2张JPG图片分别对应表单页面、成功反馈页面及背景视觉素材整体仅559KB结构简单适合直接下载学习。目前已有8690人学习或下载受到不少初学者关注。通过阅读代码可以理解表单元素布局、事件处理与正则校验的配合方式也可基于现有逻辑扩展手机号、邮箱等更多校验项或调整成功页停留时长是个易上手、可改造的入门练习。1. 只做一个 HTML 注册登录先看清边界再动手搜索html实现用户简单的注册登录的人往往不是产品经理而是被课程设计、毕设或交付 demo 逼到跟前的开发者。真正动手会发现这个简单里藏着三个要决策的点用户数据存哪、页面刷新后怎么记住登录状态、以及后面到底接不接后端。这篇文章按我平时做演示页面的顺序来。先用一个 HTML 文件配合 localStorage 跑通注册、登录、下线把最小闭环完整放到浏览器里然后把表单校验和按钮反馈做扎实再讲清楚静态 HTML 怎么对接真实登录接口最后写五个我在这个需求上实际踩过的坑。读完后你能拿到一份可以直接跑的注册登录页面同时知道它离生产环境还差多远。适合人群很明确前端刚入门的新手、被课程设计催着交 demo 的学生以及需要快速给后端接口配一个测试页面的开发。下面所有代码都以可复现为准你直接建一个 HTML 文件也能跑。2. 用 localStorage 跑通注册、登录、下线完整代码与参数说明在没有后端的情况下浏览器 localStorage 是存用户数据最常用的方案。它对同源页面开放按 key-value 存储JSON 序列化后可以放数组和对象刷新页面不会丢。登录态我习惯放 sessionStorage好处是关闭标签页后会话自然失效不会出现关掉浏览器再打开还是登录状态的奇怪体验。2.1 页面结构两个 form 切换还是同屏展示常见做法是同一个容器里放登录、注册两个 form上面用 tab 按钮切换而不是做两个 HTML 页面。后者在真实项目里更常见但 demo 阶段用 tab 切换可以让评审一眼看到全部功能。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title注册登录演示/title /head body div idstatus-bar classhidden 当前用户span idcurrent-user/span button typebutton idlogout-btn下线/button /div div classauth-box button typebutton classtab active>const USERS_KEY auth_demo_users; const SESSION_KEY auth_demo_session; function getUsers() { const raw localStorage.getItem(USERS_KEY); try { return JSON.parse(raw) || []; } catch (e) { return []; } }JSON.parse(raw)必须放进 try/catch。因为 localStorage 内容可能被用户手抖改坏或上一版本写入的数据不是标准 JSON。一旦解析失败直接返回空数组不要让页面白屏。|| []处理的是key 存在但值为 null的情况。注册表单的提交事件是整个 demo 的核心document.getElementById(register-form).addEventListener(submit, function (e) { e.preventDefault(); const msg document.getElementById(register-message); const username document.getElementById(reg-username).value.trim(); const password document.getElementById(reg-password).value; const confirm document.getElementById(reg-confirm).value; if (username.length 3) { msg.textContent 用户名至少 3 个字符; return; } if (password.length 6) { msg.textContent 密码至少 6 位; return; } if (password ! confirm) { msg.textContent 两次密码不一致; return; } const users getUsers(); if (users.some(u u.username username)) { msg.textContent 用户名已存在; return; } users.push({ username: username, password: password, createdAt: Date.now() }); localStorage.setItem(USERS_KEY, JSON.stringify(users)); msg.textContent 注册成功请切到登录页; });三个关键点用户名先trim()再去校验和存储否则用户注册admin登录时输入admin永远匹配不上密码不做 trim因为用户密码可能真的包含首尾空格虽然少见但过滤掉反而制造新问题用some做用户名查重比 indexOf 直观找到第一个重复就阻止注册。users.push的对象里除了用户名和密码我习惯加一个createdAt时间戳。后面做用户列表、会话过期时间都用得上现在不存的话之后只能把旧数据全清掉重来这是我自己吃过亏的地方。2.3 登录与下线sessionStorage 管理会话状态登录逻辑和注册是对应的。区别在于登录成功时要把当前用户写进 sessionStorage并立刻更新页面 UI。function renderBySession() { const raw sessionStorage.getItem(SESSION_KEY); if (!raw) return; try { const session JSON.parse(raw); document.getElementById(status-bar).classList.remove(hidden); document.getElementById(current-user).textContent session.username; } catch (e) { sessionStorage.removeItem(SESSION_KEY); } } document.getElementById(login-form).addEventListener(submit, function (e) { e.preventDefault(); const msg document.getElementById(login-message); const username document.getElementById(login-username).value.trim(); const password document.getElementById(login-password).value; const users getUsers(); const matched users.find(u u.username username u.password password); if (!matched) { msg.textContent 用户名或密码不对; return; } sessionStorage.setItem(SESSION_KEY, JSON.stringify({ username: matched.username, loginAt: Date.now() })); renderBySession(); msg.textContent 登录成功; }); document.getElementById(logout-btn).addEventListener(click, function () { sessionStorage.removeItem(SESSION_KEY); location.reload(); }); renderBySession();登录态放在 sessionStorage 而不是 localStorage是因为 sessionStorage 的生命周期是标签页。用户关掉页面再打开登录态自动消失这符合大多数 demo 对下线的预期。如果需求是七天免登录再换 localStorage 并在登录时写过期时间后面第 6 章会讲。下线按钮用removeItem加location.reload()是最省事的做法。刷新后状态栏回退表单回到初始状态所有 JS 变量也被清掉不会出现页面残留已登录痕迹的尴尬。tab 切换也不能少document.querySelectorAll(.tab).forEach(function (tab) { tab.addEventListener(click, function () { document.querySelectorAll(.tab).forEach(t t.classList.remove(active)); this.classList.add(active); document.getElementById(login-form).classList.toggle(hidden, this.dataset.tab ! login); document.getElementById(register-form).classList.toggle(hidden, this.dataset.tab ! register); }); });到这里注册、登录、下线三个动作已经闭环。如果只是交课程 demo这份代码加一点 CSS 就够用了。但如果你把它当成一个正经项目的起点还需要把表单校验和错误提示做得更细这就是下一章的内容。3. 表单校验做扎实正则、原生属性和输入防呆第 2 章的校验放在 JS 里提交事件内够用但不够专业。合格的做法是前端双层校验HTML 原生属性拦第一层JS 正则和关系判断拦第二层。第一层让浏览器给出即时提示第二层保证逻辑正确。3.1 HTML5 原生校验required、minlength、pattern 的参数设计原生校验可以直接写在 input 标签上浏览器会接管空值、格式、长度等检查不通过时表单不触发 submit 事件。属性作用推荐参数required必填空值阻止提交无参数minlength最小长度6maxlength最大长度20pattern正则校验看业务规则autocomplete控制自动填充new-password给注册表单加上原生校验后长这样input typetext idreg-username required pattern[\u4e00-\u9fa5A-Za-z0-9_]{2,20} title用户名2-20位支持中文、字母、数字、下划线 input typepassword idreg-password required minlength6 maxlength20 pattern(?.*\d)(?.*[A-Za-z])[\S]{6,20} title密码6-20位必须包含字母和数字 input typepassword idreg-confirm required minlength6 maxlength20pattern里的[\u4e00-\u9fa5A-Za-z0-9_]意思是一到多个中文字符、英文字母、数字或下划线。HTML 的 pattern 属性在浏览器底层用的是 JS 正则所以\u4e00写法有效。(?.*\d)是正向预查表示密码里至少要有一个数字(?.*[A-Za-z])表示至少要有一个字母[\S]{6,20}表示 6 到 20 位非空白字符。注意minlength只限制长度限制不了密码必须包含字母和数字所以要和pattern配合。maxlength一定要写否则 1 万位密码也能被塞进 JSON 并写入 localStorage直接卡死页面。3.2 前端校验的边界确认密码、中英文用户名和空白字符HTML 原生校验处理不了两次密码是否一致因为它看到的只是两个独立的密码框。这个判断必须放到 JS 里在提交前比较两个值。常见的校验函数可以统一收敛function validateRegister(username, password, confirm) { if (!/^[\u4e00-\u9fa5A-Za-z0-9_]{2,20}$/.test(username)) { return 用户名需为2-20位中文、字母、数字或下划线; } if (password.length 6 || password.length 20) { return 密码长度需在6到20位之间; } if (!/[A-Za-z]/.test(password) || !/\d/.test(password)) { return 密码必须同时包含字母和数字; } if (!/\S/.test(password)) { return 密码不能包含空格; } if (password ! confirm) { return 两次输入的密码不一致; } return ; }用户名是否允许中文取决于你的业务。课程 demo 通常允许中文所以正则里用\u4e00-\u9fa5这一段 Unicode 中文字符区间。\S判断的是非空白字符它会把普通空格、全角空格\u3000都挡在外面。这样注册时把空格封死比登录时再trim()更省事。这里有一个边界要讲清楚前端正则只是体验层不是安全层。用户完全可以绕过页面直接给后端发请求所以这些规则在后端接口里必须再校验一遍。前端做得再漂亮也只是减少后端无效请求和服务端压力。3.3 给按钮加一个处理中状态体验上的最后一公里很多人做完注册登录发现一个隐患用户连点两次注册按钮localStorage 里出现了两个相同名字的用户。第二次写入会被some挡住但按钮没有任何反馈用户以为没点上。常见的处理方式是在提交时禁用按钮并改成注册中...等逻辑执行完再恢复const btn this.querySelector(button[typesubmit]); btn.disabled true; btn.textContent 注册中...; try { // 这里执行第 2.2 节里的注册校验和写入逻辑 } finally { btn.disabled false; btn.textContent 注册; }finally保证代码无论正常执行还是抛出异常按钮都会恢复不会出现按钮永远灰色的坑。在模拟接口的场景里你还可以用setTimeout包一层再执行注册逻辑模拟 300 毫秒网络延迟让评审看到状态变化。如果后面接了真实接口这里应该配合async/await等接口返回再恢复按钮。注意接口失败时要把错误信息message.textContent显示在表单下方不能只把按钮恢复否则用户根本不知道发生了什么。这块代码不多但对 demo 质感的提升最明显。4. 从演示到真实接口fetch 对接注册登录 API 的常见做法第 3 章做完你已经有一个能在浏览器里自娱自乐的注册登录系统。但把它部署到服务器上让不同电脑上的用户都能登录就必须接后端接口把用户数据从 localStorage 搬到数据库。4.1 为什么纯 HTML 还要区分本地存储与后端存储localStorage 的数据只在当前浏览器的当前源下可见。你在自己电脑上注册的用户换一台电脑根本不存在更别提同一个用户在多台设备上共享数据。真实业务的注册登录永远是前端 HTML 负责采集和展示后端接口负责存储和校验。所以标题里的html实现注册登录更准确的理解是用 HTML 把注册登录的表单和交互实现出来。数据层要么用 localStorage 做本地 demo要么用 fetch 对接远程接口。两种方案不是互斥的我的习惯是在代码里留一个开关const USE_API false; async function registerUser(username, password) { if (!USE_API) { // 走 localStorage 逻辑 return true; } // 走接口逻辑 const res await fetch(/api/register, { ... }); return res.ok; }这样同一个页面既能本地演示也能在后端就绪后切换到接口模式。比写两套 HTML 好维护得多。4.2 用 fetch 提交登录:请求体、Content-Type 与错误处理后端接口最常见的登录协议是 POST 一个 JSON 体服务端校验后返回登录态。前端用 fetch 发起的请求长这样async function apiLogin(username, password) { const response await fetch(/api/login, { method: POST, headers: { Content-Type: application/json }, credentials: include, body: JSON.stringify({ username: username, password: password }) }); const data await response.json().catch(() ({})); if (!response.ok) { const message data.message || HTTP ${response.status}; throw new Error(message); } return data; }method: POST是登录接口的铁律不要用 GET 把用户名密码拼在 URL 上。Content-Type: application/json告诉后端你发的是 JSON 而不是表单格式没有这个头很多后端框架会把整个 body 解析成空对象。credentials: include表示跨域请求也要携带 Cookie。如果登录成功依赖服务端种 Cookie 来维持会话这一行必须写。注意同源请求不写也默认带上但一旦前端页面和后端接口不同端口不带这行就会遇到明明返回了 Set-Cookie浏览器就是不存的怪问题。response.json().catch(() ({}))是防呆写法。后端如果返回的不是 JSON 而是 502 页面直接await response.json()会抛异常导致真正的错误信息被吞掉。4.3 401 未登录与登录态失效前端要统一处理的场景真实项目里请求接口经常遇到{code:401,message:未登录,请登录!}这种返回。它出现在两种场景一是你压根没登录就请求了需要登录的接口二是登录状态过期。前端不能只在登录页里处理 401而是要在所有需要登录态的接口请求里统一处理。常见做法是封装一个请求函数async function request(url, options {}) { const response await fetch(url, { headers: { Content-Type: application/json }, credentials: include, ...options }); if (response.status 401) { sessionStorage.removeItem(SESSION_KEY); location.href login.html; const data await response.json().catch(() ({})); throw new Error(data.message || 未登录,请登录!); } return response; }这里的关键是401 时清理本地登录态并跳转登录页。很多翻车现场是接口返回 401前端却只把错误信息 alert 出来用户点掉弹窗后又在一个假登录状态下操作接着又收到 401形成死循环。如果后端用 token 而不是 Cookie登录成功时返回的data.token要放进 sessionStorage之后每次请求加Authorization: Bearer token头。这和 401 处理是同一套逻辑只是把凭证从 Cookie 换成了显式 token。5. 注册登录避坑记录5 个让我翻车又爬出来的细节这一章是我在这个标题上最有体会的部分。看似简单的注册登录坑全在细节里。下面每条都按现象、原因、解决来写。5.1 localStorage 读取返回值 null/解析异常不能想当然现象页面第一次打开时localStorage.getItem(USERS_KEY)返回 null直接用null.length或null.map直接抛错注册按钮点一下页面就没反应。原因本地存储没有数据时getItem返回的就是 null。另外如果之前有人用手改过 localStorage里面存了不是 JSON 格式的内容JSON.parse也会抛异常。解决读取函数里必须同时做 null 判断和异常捕获。第 2 章的getUsers已经演示了标准写法JSON.parse(raw) || []加try/catch。任何从 localStorage 读取的数据都要当成不可信数据处理因为你控制不了用户的浏览器环境。5.2 浏览器自动填充密码带来的假已登录状态现象刷新登录页面用户名和密码被浏览器自动填上用户没输入任何东西直接点登录结果真的登录成功了。这本身没问题但到了注册页面浏览器会把你自己的密码填到确认密码框里导致注册反复提示两次密码不一致。原因浏览器密码管理器会匹配typepassword的输入框自动填充。它分不清你的表单是登录还是注册。解决登录表单的密码框设置autocompletecurrent-password注册表单的密码和确认密码框统一设置autocompletenew-password。这比autocompleteoff更受浏览器尊重。如果还不行在页面加载时强制清空注册表单的两个密码框值用 JS 防止填充残留。5.3 用户名明明一样却登录不上空白字符和全角字符现象注册时输入测试用户登录时输入测试用户中间多了个肉眼几乎看不出来的空格或全角空格\u3000结果用户名或密码不对。原因用户在手机输入法里带出中文全角空格或首尾不小心敲了空格。第 2 章只在注册和登录时trim()还不够因为trim()只会去掉普通空格去不掉全角空格。解决把所有输入先统一规范化再比较。我习惯写一个函数function normalizeUsername(name) { return name.replace(/[\u3000\s]/g, ).toLowerCase(); }这里把全角空格\u3000和所有空白符删掉同时转小写。注册时存入规范化后的用户名登录时也用同样规则处理两边永远一致。注意如果业务允许用户名区分大小写就不要toLowerCase()否则会把Admin和admin当成同一个人。5.4 两个标签页登录状态不同步storage 事件的正确姿势现象开两个标签页同进登录页A 标签页登录成功B 标签页还显示未登录在 B 标签页下线A 标签页依然是登录状态。原因sessionStorage 是标签页级隔离的A 写的 sessionStorage 在 B 根本读不到。storage事件可以跨标签页同步 localStorage但触发storage事件的窗口不包括当前写入的标签页而且 sessionStorage 不触发这个事件。解决如果需求是多个标签页共享登录状态就把登录态从 sessionStorage 移到 localStorage然后监听storage事件window.addEventListener(storage, function (e) { if (e.key SESSION_KEY) { location.reload(); } });A 标签页写入 localStorage 后B 标签页会收到事件并刷新页面自动更新登录 UI。注意storage事件在 A 标签页自身不会触发所以不要依赖这个事件更新当前标签页的 UI写入后主动调用renderBySession()就行。5.5 file:// 协议访问接口报跨域换个本地服务器就解决现象直接在文件管理器里双击 HTML用 Chrome 打开控制台报Access to fetch at http://localhost:8080/api/login from origin null has been blocked by CORS policy或 fetch 直接 404。原因当 HTML 是以file://协议打开时浏览器把它当作一个null源发送请求时Origin头是null后端一般不会允许这种来源。即便后端允许相对路径/api/login也会指向本地文件系统根目录而不是你的项目目录。解决不要用file://预览带接口请求的页面启动一个本地静态服务器。最简单的两种方式# 方式一VS Code 装 Live Server右键 Open with Live Server # 方式二用 Python 自带服务器 python3 -m http.server 8080然后在浏览器访问http://localhost:8080/login.html。页面和后端接口同源后credentials和相对路径就都正常了。这个坑极其隐蔽不写出来能让人卡一下午。6. 注册登录写完之后3 个验证技巧和一个进阶方向代码写完后别急着交付花十分钟验证一遍能省掉很多我这里明明是好的啊的争议。6.1 用开发者工具完成一次完整验收打开浏览器 F12切到 Network 面板勾选 Preserve log然后依次做注册一个用户、切登录、刷新页面、下线。每一步看三件事Console 里有没有红色报错Network 里有没有请求失败Application 面板里 localStorage 和 sessionStorage 的键值是否符合预期。特别要验证的是刷新后登录态是否还在。如果下线后刷新又变成登录状态多半是SESSION_KEY没清干净或者renderBySession读到了旧缓存。用 Application 面板手动删掉 sessionStorage 里的数据再刷新能快速复现和排查这类问题。6.2 把多个 HTML 打包成单文件分发的两种做法如果你的页面包含多个 HTML比如 login.html 和 register.html要发给别人演示时常见做法是把 CSS 和 JS 内联到一个 HTML 里做成单文件。内联时注意保留完整的!doctype htmlhtml langzh-cn和meta charsetutf-8否则中文可能乱码。另一个做法是保持多文件结构把整个文件夹压缩成 zip 发给对方让对方本地启动静态服务器访问。没有服务器时就让对方双击 index.html但页面里不能用 fetch 和 ES6 module因为file://协议下这些能力受限。打包前检查一下所有相对路径别漏了图标或图片文件这是最常出现的交付翻车点。6.3 一个值得做的加强密码哈希与记住我localStorage 里存明文密码只能算是 demo。稍微认真一点可以用 Web Crypto API 做哈希async function hashPassword(password) { const data new TextEncoder().encode(password); const digest await crypto.subtle.digest(SHA-256, data); return Array.from(new Uint8Array(digest)) .map(b b.toString(16).padStart(2, 0)) .join(); }注册时把hashPassword(password)的结果存进 localStorage登录时也先哈希再比对。注意crypto.subtle只在安全上下文里可用也就是localhost或 HTTPS 页面file://打开时会直接报错这也是我经常强调用本地服务器预览的原因之一。记住我的常见做法是登录成功后在 localStorage 存一个带过期时间的标记const expires Date.now() 7 * 24 * 60 * 60 * 1000; localStorage.setItem(remember_me, JSON.stringify({ username: matched.username, expires: expires }));页面加载时判断expires是否大于当前时间没过期就自动恢复登录态。这里要注意别把密码存进去只存用户名和过期时间否则记住我功能反而成为安全隐患。我最初写这类 demo 时最常翻车的地方就是忘了统一trim和全角空格处理导致用户注册成功却登不进去一度以为密码学出了问题。后来所有输入在进入校验前先过一层规范化翻车明显少了很多。做注册登录前端大部分功力不在炫技而在这些细枝末节。希望这些实现和排查思路能帮你在课程设计或实际项目里少走一段弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →