多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

登录注册界面模板开发:HTML+CSS+JS 表单校验与避坑指南

登录注册界面模板开发:HTML+CSS+JS 表单校验与避坑指南 简介简单登录注册界面模板是一套基于原生 HTML、CSS 与 JavaScript 编写的前端界面资源适合前端初学者、Web 课程设计者以及需要快速搭建账号表单页面的开发者使用。压缩包内含登录页与注册页两个核心页面配套独立的 CSS 样式表、JS 交互脚本、字体图标文件与页面图片素材整体目录清晰、便于二次修改。资源共 36 个文件除多个 url 快捷方式外主要是 html 页面、css 样式、js 脚本、字体文件与图片素材等类型打包为 rar 压缩包大小约 4.51MB。目前已有 32735 人学习/下载热度较高。解压后可直接在浏览器预览登录与注册页效果也可围绕表单校验、按钮交互和样式定制进一步练习页面布局与配色可快速套用适合作为课程作业或项目初始框架是一份轻量实用、上手门槛低的前端练手模板。1. 拿到登录注册界面需求时别急着找现成组件库接到一个活儿做一张登录注册界面。对方给的参考是一个演示动画要求交付 HTMLCSSJavaScript 代码不要套现成组件库。这类需求在活动页、后台管理、课程作业里都很常见看起来简单真写起来却有不少坑表单一按回车就刷新、注册校验和登录校验逻辑互相纠缠、密码框被浏览器自动填充后样式错乱。这篇笔记会把一个简单登录注册界面模板从结构到校验完整拆开给出能直接改着用的代码以及参数、样式、校验规则背后的取舍。适合刚接触前端三件套的人照着敲也适合接小项目的人快速搭一个能交差的登录注册页面。2. HTML 骨架先定结构再谈样式2.1 登录和注册写两个表单还是一套表单先给结论模板建议写两个form而不是一个form用 JS 改标题和字段。理由有三点。第一登录和注册的校验规则不同一个 form 里做分支判断会让代码越改越乱。第二提交逻辑不同将来对接后端是两个接口拆开写接口地址更直观。第三浏览器密码管理器对登录表单的识别依赖 form 结构登录 form 里混着注册字段自动填充会出问题。如果觉得重复代码多可以抽一个公共的输入框组件用 JS 渲染两套配置。但对简单登录注册界面模板来说两个 form 直接写在 HTML 里最稳也最好懂。切换通过控制.visible/.hidden类完成不涉及路由和状态管理。登录注册各干各的事后端接口哪天变了也只需要改对应表单和 JS 监听器不会牵一发动全身。2.2 字段选型从登录流程反推每个输入框我会至少留这些字段。登录表单账号邮箱或用户名、密码、记住我勾选、忘记密码链接。注册表单昵称、邮箱、密码、确认密码、用户协议勾选。这组字段能覆盖最常见的前后端对接又不至于让模板页面显得空。昵称放注册不放登录是因为很多系统登录只认邮箱或手机号昵称只是展示用。autocomplete属性值得单独说。浏览器密码管理器靠它识别身份字段用户名用username现有密码用current-password新密码用new-password邮箱用email。不写或写错Chrome 有时会把你页面当普通表单跳过自动填充用户的密码管理器就用不了。注册表单的密码要写new-password登录表单写current-password这两个值不要混。还要注意required和novalidate一起用。保留required是为了让读屏器能识别必填字段对无障碍有帮助。同时 form 上加novalidate关闭内建提示气泡把校验统一交给 JavaScript不然浏览器英文提示和自写的错误提示会同时出现观感很差。这算是一个模板必须做对的细节。2.3 直接复用的 HTML 结构下面的结构可以直接保存成index.html同目录再放style.css和script.js就能跑起来。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title登录注册界面模板/title link relstylesheet hrefstyle.css /head body main classauth-container div classauth-card !-- 顶部切换 -- div classauth-tabs button typebutton classtab-btn active>.hidden { display: none; }3. CSS 样式让登录页脱离裸奔状态3.1 布局三板斧flex 居中、max-width、box-shadow布局的核心就三件事上下左右居中、卡片宽度可控、让卡片和背景有层次。body上用display: flex加align-items: center; justify-content: center;实现居中这个组合是最省事的方案不用再去算 margin。关键点是min-height: 100vh而不是height: 100vh。移动端浏览器地址栏会动态收缩用height: 100vh时键盘弹起或地址栏收起页面会被压缩卡片容易滚出视口。min-height加 flex 的方式更稳内容超过一屏时页面还可以正常滚动。卡片宽度用max-width: 420px而不是固定width: 420px。手机竖屏 375px 宽时卡片加上左右 padding 还能留出边距桌面端看起来也不显宽。如果写死width小屏就会溢出还得再额外写媒体查询属于自己给自己挖坑。box-shadow用大模糊、低透明度的组合比如0 10px 30px rgba(0, 0, 0, 0.08)。这个值近看有悬浮感但不抢输入框的视觉焦点。阴影的透明度不要直接给 0.2浅色背景下 0.08 到 0.1 就够给重了整个页面会发灰。:root { --primary: #4f46e5; --primary-dark: #4338ca; --error: #dc2626; --text: #1f2937; --text-light: #6b7280; --border: #d1d5db; --bg: #f3f4f6; --card-bg: #ffffff; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, Segoe UI, Roboto, PingFang SC, Microsoft YaHei, sans-serif; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg); } .auth-container { width: 100%; padding: 16px; } .auth-card { width: 100%; max-width: 420px; margin: 0 auto; background: var(--card-bg); border-radius: 12px; padding: 32px 28px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); }字体栈里把系统字体放前面中文环境会自动落到苹方和微软雅黑比引外部字体库快得多也不存在加载延迟。box-sizing: border-box必须全局设置不然输入框的width: 100%加上 padding 和 border实际宽度会超出容器卡片被撑破就是因为它。3.2 输入框三态默认、聚焦、错误输入框需要区分三种状态默认边框、聚焦高亮、错误提示。默认态用 1px 边框聚焦态把边框变成品牌色并叠加一层box-shadow外发光。这里有个细节聚焦时不要用border-width加粗的方式。边框从 1px 变 2px会把输入框下面的元素向下推 1px整个卡片轻微跳动看起来很业余。用box-shadow做外发光不占布局空间过渡也平滑。外发光默认是0 0 0 3px rgba(79, 70, 229, 0.15)颜色和边框一致透明度压低不会显得刺眼。过渡只写在border-color和box-shadow上别加all减少无用渲染。错误态我单独用了一个.invalid类由 JS 动态加。加了之后边框变红错误文字也变红。min-height: 18px是专门给错误提示留的位置这条在避坑章节会再展开讲。.auth-tabs { display: flex; gap: 12px; margin-bottom: 24px; border-bottom: 1px solid var(--border); padding-bottom: 12px; } .tab-btn { flex: 1; padding: 8px 0; border: none; background: none; font-size: 16px; color: var(--text-light); cursor: pointer; border-bottom: 2px solid transparent; } .tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; } .form-group { margin-bottom: 16px; } .form-group label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; color: var(--text); } .form-group input[typetext], .form-group input[typepassword], .form-group input[typeemail] { width: 100%; height: 42px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 15px; outline: none; transition: border-color 0.15s, box-shadow 0.15s; background: var(--card-bg); color: var(--text); } .form-group input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); } .form-group input.invalid { border-color: var(--error); } .error-msg { font-size: 12px; color: var(--error); min-height: 18px; margin-top: 4px; }输入框高度统一设 42px这是兼顾触摸和鼠标点击的适中值。小于 36px 在手机上点起来费劲大于 48px 又会显得笨重。outline: none要去掉不然聚焦时浏览器默认的蓝框和自写的box-shadow叠在一起观感很差。但去掉 outline 之后必须自己提供替代的聚焦样式这就是那层box-shadow的作用。3.3 响应式和深色模式两行变量的事小屏适配只需要压缩卡片内边距。max-width: 480px的媒体查询里把auth-card的 padding 从 32px 28px 降到 24px 20px剩下的交给卡片本身的max-width: 420px自动处理。大屏不用额外写flex 居中已经解决水平位置问题。深色模式我用prefers-color-scheme: dark。它跟随系统设置不依赖手动开关模板阶段最省事。做法是只覆盖:root里的 CSS 变量选择器一行都不用改。这里有个常见坑深色模式只改背景和文字容易漏掉input的background和color。浏览器默认的白底黑字会与深色卡片形成白色补丁非常扎眼。所以输入框样式里要显式设置background: var(--card-bg)和color: var(--text)让这两个值跟着变量走。.form-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .checkbox-label { display: flex; align-items: center; gap: 6px; font-size: 14px; color: var(--text); cursor: pointer; } .link-forgot { font-size: 14px; color: var(--primary); text-decoration: none; } .btn-primary { width: 100%; height: 44px; border: none; border-radius: 8px; background: var(--primary); color: #fff; font-size: 16px; cursor: pointer; transition: background 0.15s, transform 0.1s; } .btn-primary:hover { background: var(--primary-dark); } .btn-primary:active { transform: scale(0.98); } media (max-width: 480px) { .auth-card { padding: 24px 20px; } } media (prefers-color-scheme: dark) { :root { --bg: #111827; --card-bg: #1f2937; --text: #f9fafb; --text-light: #9ca3af; --border: #374151; } }主按钮我给:active加了个scale(0.98)按下去的瞬间有轻微下沉感。这个效果很微妙但会让页面比纯静态模板多一分能动的反馈对演示场景很有用。4. JavaScript 校验把假模板变成能交差的界面4.1 校验策略什么时候校验校验什么字段触发时机选提交时统一校验 输入时清除错误。不要在首次blur时校验用户刚进页面还没输入一移开焦点就冒出一片红字体验非常差。正确做法是提交时把该查的查一遍用户修改时只把对应字段的错误清掉让用户自己决定什么时候重新触发校验。字段规则要按用户体验优先、逻辑不重复来定。账号先查非空再判断是否包含包含才校验邮箱格式。这样设计是避免强行要求用户用邮箱也给将来接入手机号登录留了余地。密码统一要求最少 6 位这个值可以按项目需求调整但前端规则和后端规则必须一致否则会出现前端能过、后端拒绝的尴尬情况。字段校验规则错误提示登录账号非空若包含 则校验邮箱格式请输入邮箱或用户名 / 邮箱格式不正确登录密码非空长度 ≥ 6请输入密码 / 密码长度不能少于 6 位注册昵称非空长度 2~16请输入昵称 / 昵称长度需在 2~16 位之间注册邮箱非空邮箱正则校验请输入邮箱 / 请输入正确的邮箱地址注册密码非空长度 ≥ 6请输入密码 / 密码长度不能少于 6 位确认密码与注册密码完全一致两次输入的密码不一致用户协议必须勾选请先阅读并同意用户协议前端校验的定位是防用户输错不是防攻击。所有规则都能被绕过包括长度、格式、正则。真正安全防线在后端。如果有人问你这校验能防什么答防用户输错别答防攻击那是后端的事。4.2 登录表单提交拦截与逐条错误提示登录表单的校验我用一个submit监听器完成。preventDefault()必须放在监听器函数的第一行校验过程中任何一个return都不会影响它生效。如果把它放在函数末尾遇到提前return的分支就会跳过拦截表单走默认提交页面一闪错误提示全看不到。document.addEventListener(DOMContentLoaded, () { const loginForm document.getElementById(loginForm); const loginAccount document.getElementById(loginAccount); const loginPassword document.getElementById(loginPassword); const loginError loginForm.querySelector(.error-msg); function clearLoginError() { loginError.textContent ; loginAccount.classList.remove(invalid); loginPassword.classList.remove(invalid); } function handleLogin(event) { event.preventDefault(); // 先拦下默认提交 const account loginAccount.value.trim(); const password loginPassword.value.trim(); if (!account) { loginError.textContent 请输入邮箱或用户名; loginAccount.classList.add(invalid); return; } if (account.includes() !/^[^\s][^\s]\.[^\s]$/.test(account)) { loginError.textContent 邮箱格式不正确; loginAccount.classList.add(invalid); return; } if (!password) { loginError.textContent 请输入密码; loginPassword.classList.add(invalid); return; } if (password.length 6) { loginError.textContent 密码长度不能少于 6 位; loginPassword.classList.add(invalid); return; } clearLoginError(); // 校验通过这里是登录请求的接入点 // 常见做法fetch(/api/login, { method: POST, headers: {...}, body: JSON.stringify({ account, password }) }) alert(登录校验通过可以在此处调用后端接口); } loginForm.addEventListener(submit, handleLogin); // 用户重新输入时清除错误标记 [loginAccount, loginPassword].forEach((input) { input.addEventListener(input, () { input.classList.remove(invalid); loginError.textContent ; }); }); });这段代码做了三件事拦截默认提交、按顺序校验并逐条提示、通过后把控制权交给调用点。校验通过的地方我留了fetch(/api/login)注释模板使用者把接口地址和字段名换掉就能接入真实后端。注意trim()要同时用在账号和密码上。账号可以接受用户误输入的空格密码如果做了trim()真实密码前后带空格的情况会被错误通过或错误拦截。稳妥做法是密码只做value读取不做trim()而这里的trim()是考虑到模板演示场景密码一般不会刻意带空格。接生产环境时把密码那行的trim()去掉改为原样比对。4.3 注册表单确认密码、协议勾选与 tab 切换注册表单比登录多两件事确认密码比对、用户协议必须勾选。确认密码的坑在于只比对非空和相等不要同时校验两个框都满足密码长度。否则用户在确认框里输入一个字符会看到密码长度不足而非两次密码不一致这个提示会把人带偏。document.addEventListener(DOMContentLoaded, () { const registerForm document.getElementById(registerForm); const regNickname document.getElementById(regNickname); const regEmail document.getElementById(regEmail); const regPassword document.getElementById(regPassword); const regConfirm document.getElementById(regConfirm); const agreePolicy document.getElementById(agreePolicy); function setFieldError(input, message) { const errorBox input.closest(.form-group).querySelector(.error-msg); errorBox.textContent message; input.classList.add(invalid); } function clearFieldError(input) { input.classList.remove(invalid); const errorBox input.closest(.form-group).querySelector(.error-msg); errorBox.textContent ; } registerForm.addEventListener(submit, (event) { event.preventDefault(); // 先拦下默认提交 const nickname regNickname.value.trim(); const email regEmail.value.trim(); const password regPassword.value; const confirm regConfirm.value; if (!nickname || nickname.length 2 || nickname.length 16) { setFieldError(regNickname, nickname ? 昵称长度需在 2~16 位之间 : 请输入昵称); return; } if (!email || !/^[^\s][^\s]\.[^\s]$/.test(email)) { setFieldError(regEmail, email ? 请输入正确的邮箱地址 : 请输入邮箱); return; } if (!password || password.length 6) { setFieldError(regPassword, password ? 密码长度不能少于 6 位 : 请输入密码); return; } if (password ! confirm) { setFieldError(regConfirm, 两次输入的密码不一致); return; } if (!agreePolicy.checked) { setFieldError(agreePolicy.closest(.form-group), 请先阅读并同意用户协议); return; } // 校验通过这里接入注册接口 // 常见做法fetch(/api/register, { method: POST, body: JSON.stringify({ nickname, email, password }) }) alert(注册校验通过可以在此处调用后端接口); }); // 输入时清除对应字段的错误 [regNickname, regEmail, regPassword, regConfirm].forEach((input) { input.addEventListener(input, () clearFieldError(input)); }); });注册的校验我用的是字段级错误提示每个输入框下面都有自己的error-msg。和登录表单的全局错误条相比字段级错误更直观用户一眼能看到错在哪一行代价是 setFieldError 需要多传一个 input。两种方式没有对错模板里各用了一种方便对照着选。tab 切换逻辑单独一小段。核心是dataset.tab记录目标表单点击时对两个 form 分别做classList.toggle。hidden类和display: none是配套的切换就是改这个类不需要手动操作 style 属性。document.addEventListener(DOMContentLoaded, () { const tabs document.querySelectorAll(.tab-btn); const loginForm document.getElementById(loginForm); const registerForm document.getElementById(registerForm); tabs.forEach((btn) { btn.addEventListener(click, () { tabs.forEach((b) b.classList.remove(active)); btn.classList.add(active); const target btn.dataset.tab; loginForm.classList.toggle(hidden, target ! login); registerForm.classList.toggle(hidden, target ! register); }); }); });dataset.tab在 HTML 里对应>const USERS_KEY demo_users; function getUsers() { return JSON.parse(localStorage.getItem(USERS_KEY) || []); } function saveUser(account, password) { const users getUsers(); users.push({ account, password, createdAt: Date.now() }); localStorage.setItem(USERS_KEY, JSON.stringify(users)); } function findUser(account) { return getUsers().find((u) u.account account); } function registerUser(account, password) { if (findUser(account)) { return { ok: false, message: 该账号已注册 }; } saveUser(account, password); return { ok: true, message: 注册成功 }; } function loginUser(account, password) { const user findUser(account); if (!user) return { ok: false, message: 账号不存在 }; if (user.password ! password) return { ok: false, message: 密码错误 }; return { ok: true, message: 登录成功 }; }逻辑不复杂getUsers从 localStorage 读数组saveUser写入findUser按账号查。registerUser先查重再写入loginUser区分账号不存在和密码错误两种结果方便演示时说清楚流程走到哪一步。记住我也可以在这层做勾选后把账号单独存一个rememberedAccount键页面加载时回填到输入框。密码不回填那是密码管理器的工作前端代劳反而危险。function rememberAccount(account) { localStorage.setItem(rememberedAccount, account); } function restoreRememberedAccount() { const account localStorage.getItem(rememberedAccount); if (account) { document.getElementById(loginAccount).value account; document.getElementById(rememberMe).checked true; } } restoreRememberedAccount();验证方法很简单注册一个账号后打开 DevTools 的 Application 面板找到 Local Storage能看到demo_users键展开就能检查数据结构。想清空重来右键该条记录选 Clear 即可。要强调localStorage 里存密码是明文只能用于本地演示和教学不能当线上认证方案。接入真实后端时这层要整体替换为接口调用密码在服务端做哈希。我早期交付登录页只给一段静态 HTML对方验收时点完按钮没有任何反馈被退回来三次。后来我在模板里内置 localStorage 模拟注册、登录、记住我全部走通验收方自己就能体验完整流程沟通成本低了很多也更容易确认前端已经就绪只差后端接口。模板先跑起来永远比静态页面有说服力。希望帮到你。本文还有配套的精品资源点击获取
返回列表