到第六天,我终于不再对着碎片化的小练习页敲代码了。今天开始真正上手做“智慧商城项目-1”,第一件事就是把登录界面完整搭出来。如果你是和我一样自学前端、刚把HTML和CSS基础过完的小白,这个项目应该能帮上大忙——商城类页面在互联网产品里太典型了,列表、详情、购物车、结算,后台模型几乎都是围绕这些场景来的,而登录页恰好是门槛最低但细节最多的一页,适合作为“第一个完整页面”来练手。
这一天的内容不算难,难的是把几十个小细节串起来:头部的品牌区、两个输入框、一个登录按钮、协议勾选、忘记密码入口、第三方登录入口,还要考虑手机号校验、密码可见切换、按钮置灰、点击后的加载状态。我原以为花两三个小时就能做完,结果从早摸到晚,中间反复查资料、调样式、改交互,最后才把页面打磨到能看的程度。这篇文章就把我第六天的完整实践过程、踩过的坑和当时的思考写下来,主要给正在学前端、想找一个真实项目练手的朋友做个参考。
如果你已经有基础,可以直接跳过基础概念,重点看我后面的交互状态处理和踩坑记录;如果你是零基础,跟着这篇文章一步步把页面“画”出来,再把按钮和输入的联动逻辑跑通,前端的“手感”会明显不一样。
1. 项目全貌与设计思路
1.1 为什么从“智慧商城”入手
我选商城项目不是随手的。前五天基本都在做单点练习,今天和 标签打交道,明天写一个flex布局。等到第六天,我发现最大的问题不是“某个语法不会”,而是“把一堆语法放在同一个页面里,不知道怎么组织”。商城系统刚好是一个完整的解法:首页要轮播图、商品列表、分类导航,详情页要图片放大、规格选择,购物车要数量增减和金额计算,结算页要地址联动、表单校验。这些场景会反复用到flex布局、事件监听、数组方法、正则校验,只要这个项目走通一轮,前面学的所有知识基本都能串起来。
而且商城项目的页面边界很清晰,不会一上来就让人面对一个几万行代码的巨型工程。每个页面拆开看都是独立的,先做哪个后做哪个没有严格的依赖。我第一步选登录页,是因为它最独立、依赖最少,同时又是整个项目的入口。登录页做好的时候,我可以先把网络请求的模拟流程定下来,之后首页、个人中心需要用户信息时,直接复用这一套登录逻辑就行。
1.2 登录页的信息层级怎么定
设计登录页之前,我先想了一个问题:一个手机用户打开登陆页,眼睛第一秒应该看到什么?答案是三件事——我在哪里、要我做什么、做完之后去哪里。对应到页面上就是:顶部的Logo和产品名,中间的输入框和按钮,底部用于辅助的注册入口、协议说明、第三方登录。
很多人做登录页容易犯一个毛病,就是什么都要突出。Logo要大、输入框要亮、按钮要炫、促销文案也往上面塞。结果页面一打开,用户根本不知道该看哪里。我做这个页面时给自己定了一条规矩:信息越重要,视觉越突出;辅助信息一律弱化。主输入区是核心,手机号和密码必须一眼可见;协议勾选和第三方登录属于辅助决策,放在底部,文字颜色变浅、字号变小;忘记密码入口可以放在密码框附近,但不要横在中间抢视线。
这样划分之后,页面结构就很清爽了:顶部品牌区(约20%高度)、中部表单区(约50%高度)、底部辅助区(约30%高度)。实际写代码的时候不需要严格按比例来,留白和间距自然会把层次拉开。
1.3 第六天该不该上框架:技术栈取舍
其实我心里很清楚,现在很多公司前端都在用Vue、React这类框架,但第六天我依然决定先用原生三件套——HTML、CSS、JavaScript来写这个登录页。原因很简单:框架本质上是把DOM操作、状态更新、组件复用这些事做了封装,如果我对原生DOM事件、样式计算、表单交互都还没有体感,直接上框架会出现一种很尴尬的情况——报错了不知道是框架的问题还是自己代码的问题,整天在搜索引擎上找答案。
用原生方式写登录页,我能实实在在地感受到:按钮的disabled属性是怎么阻断点击的、input事件什么时候触发、为什么失去焦点和输入实时校验要做两种不同的处理。这些手感在框架里会被“语法糖”掩盖一部分。等这个商城项目做到第二、第三个页面时,如果觉得组件复用越来越麻烦,再考虑引入工程化工具或者轻量框架,水到渠成,反而学得更快。
当然,纯原生不代表写代码可以没有工程意识。我从这个页面开始就养成了几个习惯:用 CSS 变量管理主题色、把每个区域的class命名规范成“页面-模块-元素”的结构、把公用的间距和圆角定义成常量。这样后面加页面的时候,样式不会乱成粥。
2. 登录页结构搭建与布局实现
2.1 页面整体骨架(HTML 结构)
我习惯先把结构写出来,再写样式。这就像盖房子,先搭框架,再刷墙。整个登录页的HTML骨架我拆成了三个大块:顶部品牌区、中间表单区、底部辅助区。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="format-detection" content="telephone=no" /> <title>智慧商城登录</title> </head> <body> <header class="login-brand"> <!-- 品牌区:Logo、产品名称、标语 --> </header> <main class="login-form"> <!-- 表单区:手机号输入、密码输入、登录按钮 --> </main> <footer class="login-footer"> <!-- 辅助区:协议说明、注册入口、第三方登录 --> </footer> </body> </html>这里我特意用了<header>、<main>、<footer>这些语义化标签,而不是全堆<div>。原因有两个:一是语义标签对搜索引擎和屏幕阅读器更友好,二是对后续CSS布局来说,结构本身就有定位信息,不用每个<div>都加注释才知道是干嘛的。
品牌区我放了一个Logo占位块、项目名称“智慧商城”和一句说明文字。表单区是主角,包含手机号输入框、密码输入框、主登录按钮、协议勾选。辅助区放了“还没有账号?立即注册”和“微信登录”的图标占位。实际做响应式的时候,这三个区域在移动端纵向排列,非常自然。
2.2 flex 布局撑起登录表单
第六天我把flex布局练得比较熟了,所以这个页面全部用flex来排。整个主体大方向是纵向的:浏览器根节点设置为flex纵向排列,main区域通过flex: 1自动占据剩余高度,这样底部辅助区永远是贴底的。
表单内部用flex做一个纵向布局的“卡片”,所有元素基于主轴居中。
.login-form { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 16px; padding: 0 32px; }这里gap属性特别好用。以前我控制间距要靠每个元素单独写margin,写多了就会发现头部多一截、尾部少一截。用gap统一管理间距,改一处全局生效,而且不会再出现最后一个元素也带着多余边距的问题。我建议所有间距都从4px的基础倍数往上加:4、8、12、16、24、32。间距看起来会特别有节奏感,不会忽大忽小。
登录按钮的宽度我用了width: 100%,但外面套了一个最大宽度约束,这样在手机上好按,在平板上也不至于变成一长条。按钮高度固定为48px,这是手机端一个常识性的推荐值——苹果官方建议可点击区域至少44px,48px在普通视觉和易用性之间比较平衡,手指点击不会觉得吃力。
2.3 用“页面灰模”先把排版定死
在写真实样式之前,我做了一步对小白来说很值得学的操作:先用纯黑白灰的“灰模”把整体排版跑通。灰模阶段不关心颜色和图片,只看区块、字体大小、间距对不对。
我在灰模阶段做了这样几件事:把品牌区的文案设置为居中标题,字号24px,旁边放一个40px见方的Logo占位块;输入框统一高度48px、圆角8px、左边留16px内边距,两个输入框用16px间距隔开;登录按钮用实心底色,圆角同样8px,和输入框保持一致的视觉圆角节奏;底部辅助文字统一13px、浅灰色,距离主按钮24px。
灰模跑通之后,整个页面的空间感立刻出来了。此时再决定主题色、渐变、阴影这些装饰性的东西,心里会有底很多。很多初学者一上来就调颜色,调了半天布局还是歪的,就是少了这一步。
3. 输入框、按钮的细节打磨与适配
3.1 手机号输入框的细节设计
输入框是登录页的核心控件,我用的是type="tel"而不是type="text"或者type="number"。这三个里面有一个常见的坑:type="number"在部分浏览器会带上上下箭头,还允许用户输入e、+、-等数学符号,拿来当手机号输入框非常不合适。type="tel"在移动端会把键盘切换成纯数字键盘,又不带数字框的那些奇怪行为,所以手机号输入框选它是没错的。
我给手机号输入框加了三个限制:maxlength="11"限制最大长度、inputmode="tel"强化移动端数字键盘的唤起、autocomplete="tel"允许浏览器自动填充。自动填充这个事经常被忽略,但对用户来说真的很重要。很多人手机上都会开密码管理器,如果autocomplete属性写对了,浏览器会自动把号码填进输入框,省去姓名和数字的来回切换。
样式上小细节更多。首先用-webkit-appearance: none把iOS上自带的圆角边框和内阴影去掉,否则精心调好的圆角会被系统默认样式覆盖;其次给焦点加上明显的边框高亮,让用户知道自己正在哪个输入框里;最后要在placeholder的样式上做一点控制,很多浏览器默认的placeholder颜色太浅,看着很费劲。
.phone-input { width: 100%; height: 48px; border: 1px solid #e5e5e5; border-radius: 8px; padding: 0 16px; font-size: 16px; background: #fff; -webkit-appearance: none; transition: border-color 0.2s; } .phone-input:focus { border-color: #ff6700; outline: none; } .phone-input::placeholder { color: #bbb; }这里transition加的不起眼的0.2秒过渡会让焦点效果看起来柔和得多,不突兀,属于小付出大回报的细节。
3.2 密码输入框与可见性切换
密码输入框一开始就是type="password",让内容变成圆点。但只做这一步是不够的,实际产品里密码框右侧都有一个“眼睛”图标,点击可以切换明文和密文。这个小交互逻辑很简单,但它牵扯到JS和DOM的操作,对第六天的我来说正好是一个练习点。
HTML结构上,我把密码输入框和一个切换按钮包在一个div里,让它俩排成一行。按钮不写文案,用一个“眼睛”的图标代替,如果没有图标素材,先放一个文字按钮“显示”也能占位。
<div class="pwd-field"> <input type="password" id="pwdInput" class="pwd-input" placeholder="请输入密码" maxlength="20" /> <button type="button" class="pwd-toggle" id="pwdToggle">显示</button> </div>const pwdInput = document.getElementById('pwdInput'); const pwdToggle = document.getElementById('pwdToggle'); pwdToggle.addEventListener('click', function () { const isHidden = pwdInput.type === 'password'; pwdInput.type = isHidden ? 'text' : 'password'; pwdToggle.textContent = isHidden ? '隐藏' : '显示'; });这里必须用type="button",不加这个属性的话,按钮在表单环境里的默认行为是提交表单,可能导致页面莫名其妙刷新。切换密码可见性只是前端交互层面的功能,和真正传输安全没有直接关系,真正的安全要靠HTTPS加密通道和后端对密码的处理,前端不要自作主张搞“加密”,后端接口收明文密码再走自己的安全策略才是正规玩法。
3.3 移动端适配与刘海屏安全区
写移动端页面,第一步永远是viewport meta。我之前见过直接把别人代码拷贝过来用,结果页面在手机上字小得可怜,就是因为没有这个meta标签。正确的写法是:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />width=device-width告诉浏览器按设备宽度渲染,initial-scale=1.0让初始缩放比例为1,viewport-fit=cover则让页面覆盖到屏幕最边缘的安全区域之外,这样才能使用刘海屏的整个显示空间。
输入框的字号我坚持用16px,不为了“看起来精致”而调成14px之类的小字号。原因是iOS在font-size: 16px以下的输入框聚焦时,会自动放大页面以方便输入,用户一旦发现页面突然放大了,体验直接掉分。保持16px可以避开这个系统行为,这也是为什么所有移动端网页的输入框字号几乎都是16px。
底部按钮和安全区的适配也值得说。iPhone带底边横条或者全面屏的手机,页面底部的内容很容易被系统导航条或横条遮挡。处理方式是给footer增加一个基于环境变量的padding:
.login-footer { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }env(safe-area-inset-bottom)是iOS在刘海屏和全面屏上提供的安全区变量,没有安全区时返回0,不会影响普通手机;有安全区时自动向下撑开,避免底部文字被手势横条遮住。页面整体最大宽度设成480px并且水平居中,桌面浏览器打开这个页面时也不会拉伸得没法看。
4. 表单校验与登录交互实现
4.1 校验规则怎么定才合理
校验逻辑是登录页从“静态页面”变成“可交互页面”的关键一步。我先定了一套足够简单但不松垮的规则:手机号必须是1开头的11位数字,且第二位限定为3到9;密码长度必须在6到20位之间。为什么第二位限定3到9,因为国内手机号目前主流的号段,第二位基本都在3到9这个范围,这个正则基本能覆盖绝大部分真实号码,同时排掉大量乱填的“1”加十位数字。
const phoneRegex = /^1[3-9]\d{9}$/; const pwdRegex = /^[\w!@#$%^&*\-]{6,20}$/;密码正则我用了[\w!@#$%^&*\-],即允许字母、数字、下划线和部分常用特殊字符,长度6到20位。其实真实产品中密码策略常常更严格,比如必须同时包含字母和数字,但对一个练手项目的前端登录页来说,初期做一个长度和空值校验就够了。如果用户输得太长直接挡住,体验反而很糟糕。更复杂的规则要在后台校验层面做,前端不要把所有判断全部揽下来。
校验触发时机我做了两个:一是input事件实时校验,用户每敲一个字符按钮状态都跟着变;二是blur失焦时再检查一次,如果输入不合法就给出一个轻提示。实时校验的目的是让用户在输入过程中就有感知,失焦校验的目的是防止用户填完就走没看到错误。
4.2 按钮状态的实时联动逻辑
登录按钮在页面刚加载时是置灰的,我给它加了disabled属性。有真实的表单项目经验的朋友应该知道,一个默认可点的登录按钮,用户什么都不填也能点,点了之后要么报错要么转圈,体验很差。置灰状态从视觉上就告诉用户:现在还不能提交。
我写了这样一个联动逻辑:无论手机号还是密码输入框触发input事件,都调用一次checkForm()函数,该函数同时验证两个输入框的值,只有都合法才给按钮加上可用的样式类并移除disabled。
const phoneInput = document.getElementById('phoneInput'); const submitBtn = document.getElementById('submitBtn'); function checkForm() { const phoneOk = phoneRegex.test(phoneInput.value); const pwdOk = pwdRegex.test(pwdInput.value); if (phoneOk && pwdOk) { submitBtn.disabled = false; submitBtn.classList.add('active'); } else { submitBtn.disabled = true; submitBtn.classList.remove('active'); } } phoneInput.addEventListener('input', checkForm); pwdInput.addEventListener('input', checkForm);对应的CSS里,默认按钮是浅灰色或者低饱和度的颜色,active类把它变成品牌主题色的渐变实心,再加一个轻微的阴影和按压动画。我还给按钮加了cursor: pointer,但记住一个细节:disabled为true的按钮,鼠标悬停时会显示禁止手势样式而不是默认的手型,这其实是浏览器给用户的提示,不要为了“好看”把它覆盖掉。
4.3 模拟登录请求与加载反馈
前端没有后端接口时,登录按钮点击后怎么办?我选择了“模拟请求”的方式:点击按钮后立即禁用按钮,文案从“登录”变成“登录中...”,用一个setTimeout模拟2秒的网络延迟,时间到了再恢复按钮。这个过程虽然假,但把前端的加载态、防重复点击、成功回调三个概念都跑通了,以后接真实接口只是换个数据源的问题。
submitBtn.addEventListener('click', function () { if (submitBtn.disabled) return; submitBtn.disabled = true; const originText = submitBtn.textContent; submitBtn.textContent = '登录中...'; setTimeout(function () { submitBtn.textContent = originText; submitBtn.disabled = true; // 这里后续可以跳转到首页 // window.location.href = 'home.html'; }, 2000); });我在这个模拟逻辑里特意保留了一行跳转注释。以后只要后端接口就绪,把setTimeout换成fetch或者axios调用,在then里跳转,在catch里提示“登录失败,请稍后重试”,登录流程就完整了。
另外一个容易忽略的细节:点击按钮进入加载态后,输入框应该保持可编辑状态,但按钮的文案变化期间用户如果继续修改输入内容,校验函数会重新把按钮置灰。这两套逻辑其实是并行工作的,互不干扰,但需要确保加载态结束后重新跑一次checkForm(),否则已经合法的账号密码,按钮可能永远停在灰色状态。
5. 常见问题与踩坑记录
5.1 我实际踩过的几个坑
这一天我踩的坑比预想多,每个都花了不少时间排查,这里挑几个有代表性的记录下来。
第一个是100vh在手机上的“弹性问题”。我在页面根容器上用了height: 100vh,在电脑浏览器、安卓部分浏览器里看着没问题,但iPhone上地址栏一收一缩,页面高度跟着跳,底部内容一会儿被顶上去一会儿被切掉。后面我把根容器的写法改成了min-height: 100vh; min-height: 100dvh;,后者是动态视口高度,会跟随浏览器地址栏状态自动更新,问题就没了。当然100dvh的兼容性还没到完美,所以保留100vh作为兜底。
第二个坑是iOS聚焦输入框时页面“顶起”。就是在iOS Safari里点击输入框,输入框所在的容器会被自动滚到屏幕中间,如果页面布局有绝对定位元素,经常会出现错位。我的处理方式是给输入框所在的容器设置scroll-margin-top,同时在页面级把scroll-padding-top设为一个安全值,让聚焦滚动到指定位置时留出间距。这个坑在开发工具里基本模拟不出来,一定要真机测试才能发现。
第三个坑是密码自动填充后背景颜色变蓝。浏览器自带密码管理器填充之后,会把input背景改成浅蓝色,和我页面白色的主题色冲突得非常明显。网上有一种“土办法”是用内阴影去盖掉系统的填充背景色:
input:-webkit-autofill { -webkit-box-shadow: 0 0 0px 1000px #fff inset; -webkit-text-fill-color: #333; }这个办法的原理是用一个巨大的内阴影覆盖掉系统着色的底层,同时把文字颜色强制改回来。它不算优雅,但确实有效,在兼容性要求比较高的场景里很实用。
5.2 踩坑之后的排查思路
这几个坑排查下来,我总结了一条对小白很有用的经验:移动端Web开发的问题,永远不要觉得自己代码写得没问题就跳过。解决顺序一般是“先分清是样式问题还是行为问题,再先真机复现、后查兼容性”。样式问题打开电脑浏览器开发者工具的移动模式基本能定位到八九成,行为问题则大概率出在系统交互和浏览器默认行为上,必须真机测试。
100vh跳动员的都是动态环境,iOS自己会把地址栏算进视口高度,所以开发时我在Chrome设备工具栏里也确认了地址栏影响,但最终仍然真机验证。这个现实是模拟器替代不了的,建议每个学习前端的人尽早备一台可以调试的真机。
5.3 这个登录页下一步能怎么接
当前这个登录页完全是静态的,没有真实数据。下一步我会在页面里埋一个“模拟用户系统”:把用户填写的信息存到localStorage里,首页加载时读取,如果没读到登录信息就强制跳回登录页。这个逻辑很简单,但能让整个商城项目真正“串”起来——从登录到首页到后续的商品页,就是一个有状态的应用雏形了。
页面本身也预留了扩展点:协议勾选那里加一个change事件,未勾选时登录按钮保持禁用;第三方登录图标以后可以换成真实接入的OAuth跳转;忘记密码可以做成一个独立页面,表单结构和登录页非常相似,直接复用样式。整体模块做好隔离之后,每增加一个新页面,搭建成本会越来越低。
写在最后的第六天体会
白天看上去只是个普通的登录页,到了晚上回看,才发现它浓缩了很多东西:HTML语义化、flex布局、移动端适配、表单校验、状态联动、事件处理、模拟异步。第六天之前我学的都是零散的,一天做完这一个页面,所有知识点开始在我脑子里串成一条线。这种“把东西做出来”的感觉,比刷任何教程都让人踏实。
如果你也在自学前端,我建议别一直停留在听课和照抄代码的阶段,找一个像智慧商城这样的系列项目,从登录页开始,一页一页慢慢做。卡住了就停一停,先分清是样式问题、逻辑问题还是兼容问题,再逐个击破。等你做到五六个页面之后,回头看看自己的登录页,一定会特别有成就感。