这节内容我从实际开发的角度聊聊HTML里最容易忽略、但也最见功力的三个组件列表、表格、表单。很多人学HTML时觉得这些标签简单——无非就是ul里放li、table里放tr、form里放input——但真到了做项目的时候导航菜单怎么搭才语义清晰课程表这种复杂表格怎么合并单元格表单怎么做前端校验才能把用户引导到位处处都是细节。这篇就把它们一次讲透适合刚学完HTML基础、准备上手写页面的人也适合工作一两年想回头补基础的同学。有些坑是我自己踩过的该说的都会说。1. 列表标签不只是li更要理解语义和使用场景列表是HTML里出现频率最高的组件之一。你打开任何一个网站导航菜单、商品列表、新闻条目、面包屑导航本质上都是列表。但很多人写列表就是机械地套标签从不考虑用ul还是ol还是dl导致做出来的页面结构语义混乱浏览器和辅助工具都没法准确理解内容。这一节把三种列表的元素结构、语义差异和应用场景彻底理清。1.1 三种列表怎么选无序、有序与自定义列表HTML一共提供三种列表无序列表ul、有序列表ol、自定义列表dl。它们的标签结构完全不同语义也不同选错不是语法错误但会直接影响页面的可访问性和SEO理解。无序列表ul是最常用的每个列表项用li包裹适合条目之间没有先后顺序、只是并列关系的场景。比如导航菜单的多个链接、商品列表、标签集合这些条目之间不存在谁前谁后的逻辑关系用ul就是最正确的选择。ul的默认样式是每一项前带一个实心圆点这个圆点属于列表标记可以用CSS的list-style-type控制。有序列表ol同样由li构成但语义上强调顺序。操作步骤、排行榜、比赛名次、执行流程这类内容靠顺序传递信息必须用ol。它默认的标记是数字序号从1开始递增。有个容易被忽略的点如果列表在展示时被插入或删除了某一项ol的序号会自动重新计算而如果你自己手写数字放进文本里改一个就要连带改一串维护成本完全不同。自定义列表dl和其他两个结构上差异较大它不直接使用li而是由dt定义术语和dd描述内容成对组成。dl适合名词解释的配对结构比如商品参数列表、人物介绍、图文配对展示。在HTML5规范中dl的语义被扩展为描述列表可以承载键值对形式的数据所以很多FAQ页面、产品详情页的参数区都用dl来实现结构。实际选型的判断其实很直接列表项内部结构只有一层文本或链接用ul或ol列表项内部承载复杂图文组合且需要清晰的键值语义可以考虑dl但dl在浏览器默认样式的表现力上不如ul直观调试时需要额外重置所以实际项目中用得相对少更多时候是程序员用ul间接实现类似布局。典型场景对比可以用一张表快速过一下列表类型组成标签语义特征典型应用ul无序列表ul li并列、无顺序导航菜单、商品列表、标签组ol有序列表ol li顺序、有先后操作步骤、排行榜、执行流程dl描述列表dl dt dd键值对、术语解释商品参数、名词解释、图文配对1.2 嵌套列表与真实场景导航、面包屑、图文卡片真实页面里列表很少只有一层更多是嵌套结构。最典型的是多级导航菜单一级菜单是一个ul某个菜单项需要展开子菜单就在这个li内部再放一个ul。这种嵌套在HTML结构上是完全合法的而且语义依旧成立因为子菜单和父菜单之间本身就是父项包含子项的关系。嵌套列表有一个开发中非常容易踩的坑一旦ul里套ul浏览器默认样式就会出现双重列表标记而且内外缩进叠加看起来非常杂乱。所以写成CSS时我通常会先把全局的ul、ol统一重置去掉默认的padding-left和list-style再根据具体业务场景手动添加需要的缩进和标记符号。很多人上来直接用通配符*将margin和padding清零目的是一样的——先把浏览器默认样式的不确定性干掉再在可控的基础上重建。面包屑导航很多人会用div加箭头符号实现但从语义角度讲面包屑本身就是当前位置相对于首页的顺序路径天然就是有序列表。用ol实现面包屑每个层级用li包住链接配合CSS去掉数字标记并加分隔符既保持了语义又方便屏幕阅读器识别当前处于第几级页面。图文卡片列表则是用li承载复杂内容的典型每个li内部放一个图片标签和一个文本块可能还有标题、摘要、价格。过去这种列表常用浮动布局实现现在更多用Flex或Grid但列表的语义结构始终是ul li。要注意的是如果整个卡片本身可以点击跳转应该把链接a标签放在li内部并且让a的尺寸撑满整个卡片区域而不是只包住标题文字这样用户的点击热区才足够大。移动端上的点击体验很大程度就取决于这种细节。1.3 列表样式控制与移动端适配的常见坑列表的样式控制看似简单实际开发中问题不少。第一个高频坑是list-style属性在部分场景失效。list-style是复合属性包含list-style-type、list-style-position和list-style-image。大部分时候我们设置list-style: none就能去掉默认标记但如果你给某个元素设置了display: flex或display: grid列表标记会直接消失而改成display: inline-flex后标记又可能回来。原因在于列表标记只适用于块级盒子flex、grid容器本身不再生成常规的列表项盒子。第二个坑是li之间的间隙。当li被设置为display: inline-block时HTML源码中li之间的换行和空格会被渲染为字间距导致相邻项之间出现无法理解的空隙。解决办法有很多最稳的是父元素设置font-size: 0再在li里恢复字体大小或者直接改用Flex布局就不会有这类字符间隙问题。现在的移动端导航几乎全面转向Flex这种技巧知道个原理即可但老项目里仍然能看到。第三个坑是默认缩进不一致。ul默认有40px的padding-left这个值在不同浏览器里有差异主要是旧版浏览器reset掉是常见的做法。但如果内容本身是需要缩进的阅读型列表比如博客正文的有序步骤完全清除缩进反而会让列表层级感丢失需要根据场景决定保留多少缩进而不是一棒子打死。移动端适配时列表的点击区域需要额外照顾。手机上的手指点击区域比鼠标大得多一般建议最小44x44像素。如果列表项只有一行小字用户点起来非常吃力常见的做法是给a标签设置足够的padding或者直接把li的高度撑起来。除此之外列表项如果内容过长需要考虑换行策略导航项通常用white-space: nowrap加横向滚动来处理商品列表则要避免文字截断导致关键信息缺失必要时用两行截断加省略号。2. 表格语义结构、单元格合并与移动端适配表格在前端圈子里名声不太好因为早年大量网站用table做整体页面布局导致结构和表现完全耦合维护起来非常痛苦。但table本身不是洪水猛兽它是展示结构化数据最合适的方式。真正的问题从来不是table标签而是拿它做不该做的事。这一节聚焦表格的正确用法、语义化结构、单元格合并以及移动端表格适配的实操方案。2.1 完整的表格语义结构caption、thead、tbody、tfoot很多初学者写表格就是table tr td三板斧结构上没错但信息不完整。一个完整的表格应该具备以下结构table是整个表格容器可以设置border属性但现在强烈建议所有边框样式交给CSS控制html属性基本不用了。caption是表格的标题或说明放在table内部第一个位置浏览器默认显示在表格上方居中。caption对屏幕阅读器很重要它让用户先知道这个表格是关于什么的。thead、tbody、tfoot分别表示表头、表体、表尾。thead里放表头行通常由th组成tbody放数据行tfoot放汇总行比如表格底部的合计。这三个结构标签的价值不仅是语义清晰还直接影响浏览器的渲染方式。表格在大数据量下浏览器是等所有内容下载完一次性渲染还是边下载边渲染受表格结构影响。加入thead和tbody后部分浏览器能实现表头固定、报表数据独立滚动的效果。CSS方面单独给thead、tbody设置背景色、字体样式也更灵活。th标签专门表示表头单元格它和td的关键区别在于th默认加粗且居中更重要的是th带scope属性可以声明它是行头还是列头。比如scopecol表示这一列的列标题scoperow表示这一行的行标题。屏幕阅读器会通过scope正确地将表头和数据单元格关联起来。对于内容复杂的数据表格scope是提升可访问性最直接的手段。表格的边框处理是另一个基础问题。table、th、td分别设置1px边框会出现双线看起来像是每条边被画了两遍。标准解法是设置table的border-collapse: collapse把相邻单元格的边框合并为一条线整体干净利落。如果你希望保留双线的立体感可以用border-spacing配合默认的separate模式但常规数据表格用collapse就够了。2.2 单元格合并colspan和rowspan的计算逻辑单元格合并是表格里最容易出错的地方。合并分为两种colspan横向合并跨列rowspan纵向合并跨行。难点在于合并后表格的行列数量会发生变化你需要站在整个表格的行列网格角度去理解否则很容易把多出来的单元格漏掉导致表格结构错乱、排版崩坏。横向合并相对好理解。比如一个表格有4列第一行的标题单元格要横跨整个4列就写th colspan4。此时这一行只需要这一个th因为它已经把4列占满了。如果你在colspan4之后还写了td那这一行实际占5列和下面的4列对不上。纵向合并rowspan的情况更复杂。假设一个表格展示多门课程每门课程由同一个老师任教老师的名字单元格需要跨越多行。此时老师单元格写rowspan2它占用了第一行和第二行的同一列位置。关键在于第二行里原本应该写老师名称的td位置必须留空不写因为那个位置已经被第一行的rowspan单元格占用了。如果第二行还继续写td结构就会被多挤出一列表格排版立刻乱掉。为了减少合并错误我在编码时有一个习惯先在纸上画出表格的网格草图标出每个合并区域的起止行列再写代码。比如做一个课程表上午、下午分别就是一个rowspan2的单元格星期一、星期二、星期三作为列头。每一步合并都确认这个格子占了几行几列其他行的对应位置是否已经删除或多写写完之后在浏览器里逐行检查网格对齐了才算过。合并单元格很容易让行列数量出现偏差而这种偏差浏览器不会报任何错只会呈现一个歪歪扭扭的表格肉眼检查是唯一的兜底方案。2.3 表格美化细节边框、斑马纹、列宽控制表格样式设计的常见需求包括边框、斑马纹、悬停高亮、列宽控制。边框前面提过border-collapse是基础。斑马纹通常用pseudo-class选择器最常见的写法是nth-child(even)或nth-child(odd)分别给偶数行、奇数行加背景色。需要注意的是如果表格有多行表头或者用rowspan合并过单元格斑马纹可能会出现跳变——因为合并单元格的列数不同导致某些行看起来对不齐。这种情况下可以考虑改用相邻兄弟选择器或把斑马纹放在td、th上而不是tr上配合背景的body透明化处理。列宽控制是表格体验的关键。table默认按内容自动计算列宽文本多的列宽、文本少的列窄这在数据量少时还行数据一多整个表格就乱。两类常用方案一是设置table-layout: fixed让表格宽度完全由第一行单元格的宽度决定后续行的td不用再写宽度浏览器严格按照第一行的比例分配列宽二是用colgroup标签显式定义每一列的宽度把表格结构和列宽设置解耦后期调整只改colgroup即可。文本对齐在表格里也有讲究数字类数据通常右对齐方便纵向对比大小文本类数据通常左对齐居中的情况一般只出现在表头或者状态标签这类短内容。表格内的长文本处理如果希望保持表格紧凑可以设置white-space: nowrap配合ellipsis省略号如果内容本身是完整句子则应该允许换行不要强行截断。2.4 移动端表格的三套适配方案桌面端的表格到了手机上最常见的症状就是超出屏幕。原因是表格默认最小宽度由内容撑开几列数据加上汉字宽度很容易超过375px。三套方案根据数据量灵活选。第一套是外层包裹容器设置overflow-x: auto表格宽度设置min-width: 768px或用white-space: nowrap保证内容不换行。这是最省事的方式适合结构简单、数据列数不多但内容较宽的场景。用户可以在手机上横向滑动查看完整表格。缺点也明显用户需要横向滚动才能看全数据很容易漏掉右侧的信息。第二套是配合CSS媒体查询在小屏幕上让表格变成卡片式布局利用data-label自定义属性和伪元素在每个td前面插入对应的列标题实现字段名值的纵向堆叠效果用户无需横向滚动。具体做法是给每个td设置data-label姓名然后在媒体查询中让td以block方式显示并通过td::before { content: attr(data-label) }把字段名显示出来。这套方案的前提是HTML里能给每个td写上对应的data-label纯静态页面对应的列名固定还好如果表格是动态生成的后台系统每个像素的label都需要在JS里同步输出维护成本会稍高。第三套是降级简化保留表格的完整结构但在手机上只展示关键列其他列通过媒体查询或类名隐藏。用户想看完整信息再进入详情页。这种方案适合表格只做概览详情另开页的管理端场景。实际项目中后台管理系统用第一套最多前台展示型页面用第二套更好业务复杂的用第三套配合详情页。3. 表单控件类型、label关联和基础校验表单是前端开发中和用户交互最频繁的部分。一个注册页面、一个订单提交、一个搜索框本质都是表单。HTML表单的核心组件是form标签内部由各种输入控件组成。很多人只学会了input的text类型对type的其他成员、select和textarea的适用场景、label的作用、表单校验的边界都缺乏系统性认识。这一节把表单从结构到校验理一遍。3.1 input的type家族不同type决定输入方式与体验input标签通过type属性区分输入类型每一个type都对应不同的输入模式和键盘形态。常用的type对照如下type值输入内容移动端键盘表现使用场景text单行文本默认全键盘用户名、搜索词、单行地址password密码掩码显示默认全键盘登录密码、确认密码email邮箱地址带键的键盘邮箱注册、邮件订阅tel电话号码数字拨号键盘手机号、座机号number数值数字键盘数量、单价、年龄url网址带/.的键盘个人主页、博客链接search搜索词默认键盘加搜索键站内搜索框date日期日期选择器生日、预约时间type的意义不只是键盘更友好它还会触发表单的自动校验。比如typeemail的输入框如果用户填的内容不符合邮箱格式表单提交时浏览器会自动拦截并提示。好处是省去自己写正则坏处是不同浏览器的提示文案不一致而且样式无法统一如果你需要完全自定义的校验体验可以用novalidate属性关闭浏览器原生校验改由JavaScript接管。number和tel的区别要特别注意。tel类型在移动端弹出的是纯数字拨号键盘用户输手机号非常方便。number类型虽然也弹数字键盘但它在桌面端会附带上下箭头微调而且部分浏览器对number输入框允许输入e、E科学计数法符号、正负号等字符如果你要严格限制只能是数字number并不完全可靠。所以手机号输入框的标准做法是用typetel加maxlength11加pattern校验而不是typenumber。readonly和disabled是两个容易被弄混的属性。readonly表示字段只能读不能改但内容会随表单一起提交disabled表示字段完全禁用内容不会提交。两者对样式的影响也不同disabled通常会有灰色背景而readonly的样式默认不变。业务上详情展示但允许复制用readonly条件不满足不允许操作用disabled。3.2 多选框、单选框、下拉选用户选择的三种模式选择类控件是表单里业务逻辑最丰富的地方因为选择本身就意味着有约束的输入。用户不可能随便打字选一个状态只能从你提供的选项里选这天然降低了输入错误的概率。三种选择控件各有适用场景。单选框radio用于多选一的场景结构上要有name属性相同的多个input typeradio因为name相同才属于同一组浏览器才能保证同组内只能选中一个。一个很常见的错误是每个radio的name都不一样导致它们之间没有互斥关系用户可以同时选中多个这是新人最常踩的坑之一。每个radio还需要一个value值这个value就是表单提交时传给后端的实际数据。用户看到的选项文字通过label展示value可以是用户看不懂的代码比如1代表在职、2代表离职。多选框checkbox用于多选多的场景同样通过name关联但同组checkbox的name通常改为数组格式比如namehobby[]这样提交到后端时会以数组形式接收所选的全部爱好。checkbox的另一个常见用法是同意协议这种单选项此时只有一个checkboxchecked属性表示默认选中。下拉选select和option适合选项很多、页面空间有限的场景。select默认一次只能选一个可以加multiple属性支持多选但multiple下拉选在移动端体验并不好通常不推荐。option可以直接设置selected属性指定默认选中项。下拉选的另一个变体是optgroup标签它可以把一组option分组显示比如选择城市时先按省份分组用户体验更清晰。对于全国省市两级联动这种场景原生select处理起来比较复杂通常需要引入专门的选择器组件但理解optgroup对结构分组很有帮助。3.3 label与控件关联可访问性的第一步label标签在表单里看起来不起眼实际作用很大。它的核心功能是把一段说明文字和对应的表单元件绑定起来产生两种直接效果一是用户点击label文字时会自动把焦点转移到对应的输入框或选中对应的选项这大大提升了点击面积尤其移动端特别明显二是屏幕阅读器在朗读表单时会把label的内容作为控件的名字朗读出来没有label的表单对依赖辅助工具的视障用户基本不可用。label与控件的关联有两种写法。第一种是隐式关联把label作为包裹元素内部同时放文本和控件比如label下直接写用户名和input typetext这种写法不需要任何属性即可完成关联但局限性是结构上控件必须嵌在label内部。第二种是显式关联通过label的for属性值和控件的id属性值一一对应比如label forusername配input idusername。显式关联更灵活控件位置可以不在label里面目前推荐的也是这种写法。要注意for和id必须完全一致大小写敏感系统生成的ID也要注意避免重复。placeholder和label的关系也需要分清。placeholder是输入框内的灰色提示文字内容在用户输入时自动消失它不能代替label。原因在于placeholder在失焦后无法提供常驻的字段说明而且许多屏幕阅读器根本不会朗读placeholder内容。我在做登录表单时曾经只放placeholder而不放label结果发现移动端截图测试和可访问性测试都不过关。正确做法是label常驻可见placeholder只作为补充的输入示例或格式提示。3.4 表单校验required、pattern与前端校验的边界表单校验分为前端校验和后端校验。前端校验的目的是在用户提交之前尽早拦截明显错误提升填写体验和降低无效请求量后端校验是不可或缺的最终防线因为前端校验可以被绕开恶意用户可以手动构造请求直接打到后端。这不是安全问题上的道德洁癖而是所有生产级后端的强制性要求。HTML5提供的原生校验属性里最常用的是required必填、pattern正则匹配、minlength/maxlength长度限制、min/max数值范围。比如密码框要求至少6位可以设置minlength6手机号可以用pattern1[3-9]\d{9}匹配国内号码格式。这些属性生效后表单提交时会自动检查不通过就阻止提交并显示浏览器内置的错误提示。CSS方面也有对应的伪类来反馈状态:valid匹配通过校验的控件:invalid匹配未通过校验的控件。你可以用这两个伪类给边界加上红色边框、给通过项加上绿色对勾交互反馈会直观很多。但要注意一点:invalid和:valid在页面初次加载时就会应用用户还没填写就被标红体验比较差。常见做法是配合CSS的:focus、:not(:placeholder-shown)或者通过JS在提交后给form添加class来控制是否触发校验样式让提示过早的问题得以缓解。原生校验的优势是不需要额外引入依赖、开箱即用缺点是样式和文案无法完全统一尤其在浏览器之间的UI差异明显。所以业务要求高的项目通常会用JavaScript做自定义校验或用成熟的校验插件。但无论用哪种方案原则不变前端校验负责体验后端校验负责安全两者缺一不可。纯前端校验而不做后端校验是把安全边界完全暴露出去纯后端校验而放弃前端校验则会让用户在输入错误时多等一次网络来回。表单提交的行为也需要说明form内的submit按钮默认会触发表单提交提交目标由form的action属性指定方法由method属性指定GET或POST。如果没有设置action页面会刷新并把表单参数拼到URL里。现代前端项目通常通过JS拦截submit事件阻止默认刷新然后通过Ajax发送数据。即使是这种场景原生required、pattern的校验依然会先于submit事件生效可以先校验再拦截也可以关闭原生校验后完全由JS接管根据项目需求选择。4. 综合实战会员信息登记页面把列表、表格、表单串起来前几节把列表、表格、表单分开讲了但它们在实际页面里从来都是组合出现的。这节用一个会员信息登记页面把三者放进同一个上下文里让语法知识真正落到页面上。这个示例包含导航菜单无序列表、信息登记表单各种输入控件、以及已经登记会员的表格展示表格标签。学完这个综合案例基本就能独立写出一个中等复杂度的业务页面。4.1 页面规划与结构设计整个页面分三个区块顶部导航、左侧或上方表单、下方数据表格。导航用ul实现四个入口首页、会员登记、会员列表、关于我们鼠标悬停有高亮效果。表单部分包括登记新会员姓名、手机号、邮箱、会员等级下拉选择、兴趣爱好多选、备注多行文本带上基础校验必填项没填就提交会被浏览器拦截。表格部分展示已登记的会员数据包括姓名、手机号、会员等级、兴趣标签、登记时间表格设置了斑马纹和表头背景色并且在外层加了横向滚动容器保证手机端也能浏览完整列。这种页面结构在业务后台非常典型把录入数据和查看数据放在同一个页面操作路径短效率高。导航是整个页面的骨架表格是数据的最终归宿表单是数据进入系统的入口三者各司其职谁也不会抢谁的戏。4.2 HTML结构代码与区块解析导航部分的HTML结构nav classmain-nav ul lia href#home首页/a/li lia href#register classactive会员登记/a/li lia href#list会员列表/a/li lia href#about关于我们/a/li /ul /nav这层结构用ul li a是最标准的导航写法。导航项的点击区域是整个li区域还是仅a区域内取决于CSS中a的display属性。我给a设置了display: block配合padding把整个li撑满这样鼠标和手指点击整个导航条范围都能触发跳转而不是只在文字上有效。表单部分的结构form idregisterForm novalidate div classform-group label forname姓名/label input typetext idname namename placeholder请输入姓名 required /div div classform-group label forphone手机号/label input typetel idphone namephone placeholder11位手机号 maxlength11 pattern1[3-9]\d{9} required /div div classform-group label foremail邮箱/label input typeemail idemail nameemail placeholderexamplemail.com /div div classform-group label forlevel会员等级/label select idlevel namelevel option valuenormal普通会员/option option valuesilver银卡会员/option option valuegold金卡会员/option /select /div fieldset legend兴趣爱好/legend labelinput typecheckbox namehobby valuereading阅读/label labelinput typecheckbox namehobby valuetravel旅行/label labelinput typecheckbox namehobby valuecoding编程/label /fieldset div classform-group label forremark备注/label textarea idremark nameremark rows3 placeholder其他需要说明的信息/textarea /div button typesubmit提交登记/button /form几个细节说明表单设置novalidate是为了让前端JS接管校验否则浏览器原生提示会和JS提示叠加体验混乱。手机号用typetel而非typenumber前面已经解释过。fieldset和legend是一组相关表单项的分组容器把多选框包进去后在语义上更清晰。textarea的rows控制行数它是双标签结构默认值写在标签内部而不是value属性。表格部分的结构div classtable-wrapper table caption已登记会员列表/caption thead tr th scopecol姓名/th th scopecol手机号/th th scopecol会员等级/th th scopecol兴趣爱好/th th scopecol登记时间/th /tr /thead tbody tr td张三/td td13800138000/td td银卡会员/td td阅读, 编程/td td2024-01-15/td /tr tr td李四/td td13900139000/td td金卡会员/td td旅行/td td2024-02-20/td /tr /tbody /table /divthead和tbody分开后表头和数据行的样式设置就互不干扰了。th的scope属性明确声明了它是列标题屏幕阅读器读到某个td时能自动关联到对应列的th可访问性更完整。表格外包一层div是为了给横向滚动设置一个容器。4.3 配套的CSS适配与交互反馈CSS部分重点处理三类问题导航的布局与高亮、表单控件的统一间距和校验反馈、表格的响应式与美化。导航用Flex布局ul设置display: flexli里的a设置display: block和padding。当前页高亮用.active类背景色和文字颜色互相衬托。导航在小屏幕下如果项数太多可以对ul设置overflow-x: auto让导航可以横向滑动。表单部分.form-group统一设置margin-bottom和label的显示宽度控件宽度设为100%加上box-sizing: border-box确保padding不会把输入框撑破。校验反馈用:valid和:invalid配合正则当输入内容符合pattern时边框变绿不符合时边框变红。但为了避免页面加载就全红的问题我给form加了一个submitted类只有提交过一次后校验样式才真正生效代码层面就是监听submit事件并在校验失败时给form加上这个类。表格的响应式重点是通过.table-wrapper设置overflow-x: auto实现横向滚动表格内部用white-space: nowrap保持数据行不换行这样在窄屏下用户可以通过横向滑动查看完整信息。表格本身设置border-collapse: collapse表头行背景色加深文字加粗数据行通过nth-child(even)做斑马纹最后一行是合计行时用tfoot并加底色区分。4.4 这套组合里最容易翻车的三个细节第一个是form的novalidate属性与JS校验的配合问题。如果你忘了给form设置novalidate又自己写了JS校验那么在Chrome里会看到两套验证提示叠在一起一套是浏览器原生的气泡一套是自定义的提示层体验很乱。反过来如果你不希望引入JS但我又用了:valid/:invalid来做校验样式这两种情况都要基于对原生校验行为的准确判断。我目前更推荐一种折中方案表单先用原生required和pattern保底同时加novalidate关闭原生气泡由JS负责展示统一的提示文案和样式这样既有兜底又有体验。第二个是checkbox的name命名。如果不使用数组形式多个checkbox的name相同提交时只会把最后一个选中的值传给后端之前的都被覆盖。所以多选业务的name必须写成namehobby[]这种数组形式或者由前端JS手动收集选中的值。新手容易在这上面排查半天最后发现是name的问题。第三个是表格横向滚动和外层布局的宽度冲突。如果你给了外层的div一个固定宽度或者设置了flex布局但没有allow shrink横向滚动会失效表格直接撑破页面比滚动更难看。处理办法确保表格外层div自身可以收缩通常不设置固定宽度或者明确设置max-width: 100%; overflow-x: auto;让滚动容器明白自己最多能占多宽多出来的部分自己滚。5. 实际项目中的经验沉淀从这三个标签学会读HTML的语义列表、表格、表单这三个东西学起来不难但放到真实项目里我发现它们恰好是观察一个前端开发者是否懂结构的试金石。很多人写页面追求看起来一样却忽略了结构上对不对结果复制出来的代码看着没问题一改需求就浑身难受。举一个实际例子我在做一个后台数据管理页面时表格的数据行上需要显示操作按钮。一开始用简单的td里放a标签处理后来需求变成操作列需要支持菜单展开、权限控制、状态禁用逻辑复杂度一上来就发现td直接套复杂组件在HTML语义上不够干净。最后还是用th td的传统结构把操作按钮集中在一个td内部再用CSS和JS控制交互。这说明一个道理结构越简单清晰扩展时越游刃有余越是为后续留一条好路。再比如表单校验我经历过一版表格项目号码输入框用了number类型结果在iOS上出现数字键盘无法输入小数点的问题后来专门改用tel类型才解决。这种问题是纯语法层面无法预见的只有在真机测试中才能发现。所以我养成了一个习惯凡是带输入控件的页面必须在至少一台Android和一台iOS真机上过一遍键盘弹起、输入法遮挡、数字键盘切换这几个场景。现在回头去看html系列教程最大的收获不在于背会了几个标签而在于建立了用标签的语义去思考问题的思维方式。html标签不是样式类名每一个都存在特定的语义位置。用ul管理导航、用dl管理参数、用table承载数据、用fieldset组合表单项这些都不仅仅是为了符合规范而是为了让浏览器、辅助工具、搜索引擎以及未来的维护者都能快速准确地理解页面内容。这套思维方式一旦建立写出来的HTML即使去掉CSS依然结构清楚、逐层递进就是一份合格的页面骨架文档。最后分享一个我常用的自查方法写完一个页面后把CSS全部屏蔽只留HTML看页面的信息结构是否还清晰可读。如果能做到不看样式也能明白哪个是标题、哪个是导航、哪个是数据表格、哪个是表单字段这份HTML就是合格的。列表、表格、表单这三样恰恰是最能体现这种结构性思维的起点。
