写HTML这么些年,从第一弹的基础标签(标题、段落、链接、图片、列表)讲完以后,我一直在等一个合适的时机把“剩下那些看着简单、用起来全是坑”的标签补上。这篇就是那篇“第二弹”。内容定位很明确:不聊JavaScript,不聊CSS框架,只聊HTML标签本身——表格、表单、meta、文本标记、语义化结构、图片和链接的疑难杂症。适合刚学完基础标签准备写真实页面的人,也适合写过一阵子但总在某些标签上卡壳的朋友。我会按实际开发中最容易出问题的场景来拆,每个标签都讲清楚“它到底是干嘛的”和“它跟看起来差不多的标签有什么本质区别”。
1. 文本标签不止加粗和倾斜:这些细节很多人没搞对
文本标签大概是HTML里最被人瞧不起的一类——不就是加粗、斜体、下划线嘛。但真到做页面的时候,你会发现很多时候觉得“别人写的页面结构好干净、好懂”,自己写的div却挤成一团,差别往往就在这些不起眼的文本标签上。
1.1 strong和b、em和i,不是换肤关系
先说最经典的误会:<strong>和<b>都能让文字变粗,<em>和<i>都能让文字变斜,但这两组标签的语义完全不同。
<b>(bold)是纯样式标签,它告诉浏览器“这段文字要粗一点”,仅此而已,没有任何“内容上更重要”的意思。<strong>是语义标签,它表示“这段内容很重要”,默认样式是加粗,但你可以通过CSS把它改成红色、加大字号,完全不改变它的语义。屏幕阅读器遇到<strong>会用更重的语气读出来,遇到<b>则没有任何区别。
<em>(emphasis)表示强调,默认斜体;<i>(italic)是纯斜体样式,常用于外来语、技术术语、船只名称、或者简单的图标占位(Bootstrap图标用的就是<i>标签)。所以写的时候有个简单的判断标准:去掉样式以后,这段文字如果语义上依然“需要被强调”,用<strong>或<em>;如果只是视觉上想变粗变斜,用<b>或<i>,甚至完全可以改用CSS的font-weight: bold。
我自己的习惯是,正文里几乎不用<b>和<i>。需要加粗的提醒文字用<strong>,需要语气强调的用<em>,纯装饰性的倾斜一律交给CSS。这样页面做无障碍适配时,读屏器读出来的语气才是对的。
1.2 容易被误用的标记类标签:mark、del、ins、small
这几个标签在真实项目中出镜率不低,但用错的人也不少。
<mark>用于高亮标记,默认样式是黄底黑字。它和<strong>的区别在于:<strong>表示内容本身重要,<mark>表示“用户当前需要关注这一段”,通常用于搜索结果中的关键词高亮,或者引文中特别指出的片段。语义上它更接近“临时标记”,不是“永久重要”。
<del>表示已删除的内容,默认删除线;<ins>表示插入的内容,默认下划线。这俩经常成对出现在编辑记录、文档版本对比、或者是价格模块里“原价用<del>划掉、现价正常显示”。这里有个细节:<del>和<ins>可以带上datetime属性,写明删除/插入的时间,机器可读。
<small>过去被当“小字号标签”用,HTML5之后语义收窄为“附带信息”,比如免责声明、版权信息、法律条款。所以页脚里的“© 2024”,用<small>比用<span>加CSS更合适。
html复制<p>原价 <del datetime="2024-06-01">99元</del>,<ins>今日特价69元</ins></p>
<p><mark>请注意:</mark>该优惠仅限今日有效。</p>
<small>最终解释权归本站所有</small>
1.3 代码、引用、预格式文本的正确用法
写技术类页面或者是博客时,<pre>和<code>几乎一定用得上。<code>表示一段代码,默认是等宽字体;<pre>(preformatted)表示预格式化文本,会保留空格和换行。这俩是绝配:<pre><code>包在一起可以同时拿到“等宽字体”和“保留缩进”两个效果。
有个细节值得注意:<pre>默认会保留文本中的换行和连续空格,这导致它内部不能随便缩进——你为了代码整齐加的那些空格,全都会渲染到页面上。所以实际使用中,要么把代码顶格写,要么让处理模板的工具自动去掉多余缩进。
<blockquote>用于块级引用,默认有左缩进。它里面可以放段落,也可以放<footer>标明出处。行内的短引用用<q>,浏览器会自动加引号,不过考虑到各浏览器对<q>的引号样式支持不一致,很多人干脆用普通引号字符代替,这个看你的项目规范。
<hr>也值得重新认识一下。它不是一条“装饰线”,而是“主题转换的分隔”——表示两个内容块之间发生了话题切换。装饰性分隔线应该用CSS的border-bottom搞定,语义性分隔才用<hr>。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 表格不只是行列:合并单元格与表头的正确姿势
表格是我看到新手页面里“结构最惨烈”的地方。常见画风是:<table>里直接怼一堆<tr>、<td>,连<thead>都没有,更别提<caption>。页面肉眼看着正常,一跑读屏器或者做SEO解析,整张表格就是一团乱麻。其实表格标签的规矩不多,但每一条都影响实实在在的可用性。
2.1 表格的标准骨架:caption、thead、tbody、tfoot
一张完整语义的表格应该包含四个部分:<caption>(表格标题,相当于给整张表起名字)、<thead>(表头区域)、<tbody>(数据区域)、<tfoot>(汇总区域)。<caption>默认显示在表格上方,即使不写CSS样式,读屏器也会优先朗读它,让用户先知道这张表是干嘛的。
<thead>和<tfoot>的顺序有个讲究:HTML规范里<tfoot>必须出现在<tbody>之前(虽然视觉上它在表格底部),这么设计是为了让浏览器在数据还没完全加载时就能先渲染表头和表尾。实际写的时候按“caption → thead → tfoot → tbody”的顺序就行。
列标题和行标题要用<th>而不是<td>,默认显示加粗居中是它的基本样式,更重要的是它自带“表头”语义。多列表头时建议加scope属性:
scope="col"表示该表头统领一列scope="row"表示该表头统领一行
这个属性对屏幕阅读器来说几乎是导航索引,建议别省。
2.2 colspan和rowspan:合并单元格的坐标思维
colspan是横向合并(占多列),rowspan是纵向合并(占多行)。说穿了不值钱,但实际写错的人特别多,原因在于合并之后“格子数量”会变。
举个例子:一个课程表,星期一有3节课,星期二第1、2节合并成1节。如果你在第1行给某个<td>加了rowspan="2",那么这个<td>占掉了第1行和第2行各一个位置。第2行的其他单元格数量就必须相应减1,否则表格会“多挤出一列”,造成错位。
写合并表格时我有个笨办法:先在草稿纸上把网格画出来,数清楚每一行实际有几个格子,再写代码。比如一个4列3行的表:
html复制<table>
<caption>2024年暑期课程安排</caption>
<thead>
<tr>
<th scope="col">时段</th>
<th scope="col">周一</th>
<th scope="col">周二</th>
<th scope="col">周三</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">上午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
</tr>
<tr>
<td colspan="2">体育</td>
<td>美术</td>
</tr>
</tbody>
</table>
这个例子中,第1行“上午”格占了第1、2行的第1列,所以第2行只剩下3个格子。“体育”用colspan="2"占了2列,第3行逻辑上还是4列。每次改完合并,建议用浏览器的开发者工具看一眼表格结构,错位一眼就能发现。
2.3 响应式表格的偷懒思路
表格天生不擅长响应式。移动端面对一张7列8行的数据表,最直接的处理不是缩放,而是“换一种展示形态”。
一个成熟的方案是:小屏幕下把<thead>隐藏,每一行<tr>变成卡片,<td>在显示数值前用伪元素::before把对应表头文字“带出来”。这需要配合data-label属性使用:
html复制<td data-label="报名人数">342</td>
css复制@media (max-width: 600px) {
thead { display: none; }
td {
display: block;
text-align: right;
}
td::before {
content: attr(data-label);
float: left;
font-weight: bold;
}
}
这套方案的优点是兼容性非常好,不用JavaScript,缺点是每张表都要手写data-label。好在表格的列数量一般是固定的,写一次就能一直用。
3. 表单标签的门道:input类型、label绑定与提交机制
表单可能是整个HTML里“标签和属性搭配最复杂”的部分。很多人写表单只关心“长得好不好看”,忽略了表单背后的提交逻辑,最后出现“字段明明填了结果后台收不到数据”这种问题。
3.1 label的两种绑定方式,别只记一种
<label>是给表单控件加文字说明的标签,它最大的作用不是样式,而是“点击文字也能聚焦到控件”。这个交互细节在单选按钮和复选框上尤其重要——没有label绑定,用户想点一个选项必须精确点到那个小圆点上,绑定以后点整段文字都有效,命中面积大了一倍不止。
第一种绑定方式是用for属性关联控件的id:
html复制<label for="username">用户名</label>
<input type="text" id="username">
第二种是把控件直接包在label内部:
html复制<label>
用户名
<input type="text">
</label>
两种写法效果相同,但第一种更灵活——label可以放在页面的任何位置,不一定紧挨着控件。我倾向于推荐第一种,因为在布局复杂的时候,label和input经常会被不同的容器拆开,for + id的方式不受位置影响。
3.2 input的type类型和默认行为
<input type="text">只是input家族的冰山一角。HTML5以后type值已经非常丰富,我按使用频率排个序:
text:普通单行文本password:密码框,内容被遮盖email、tel、url:移动端会弹出对应键盘number:数字输入框,移动端弹数字键盘date、time、month:日期时间选择checkbox、radio:勾选与单选file:文件上传color:颜色选择器range:滑块hidden:隐藏字段,用来在提交时传固定值
一个常见的误区:number类型虽然自带上下箭头,但它不能限制用户的输入格式(用户仍可以输入“e”、“+”等字符,因为它们是科学计数法的一部分)。要限制输入值,需要配合min、max、step属性,甚至JS校验。
另外,type="submit"和<button>的差异也是老生常谈。<input type="submit">只能放纯文本,<button>内部可以放任意HTML内容(比如带图标的按钮)。在不依赖框架的情况下,我更建议按钮一律用<button>,语义清晰且扩展性好。
3.3 name属性与提交机制
表单里最隐蔽的坑是——所有需要提交的控件都必须写name属性。name决定提交到后台时的字段名,id只是给label和JS用的,没有name的输入框在提交时会被直接丢弃。
举个真实案例:有个同事排查了半个小时的“搜索框为什么后台收不到值”,表单长这样:
html复制<form action="/search" method="get">
<label for="q">搜索</label>
<input type="text" id="q">
<button type="submit">搜</button>
</form>
看起来一切正常,提交后URL却是/search?,q字段人间蒸发。原因就是input上只写了id="q",没写name="q"。表单提交依赖的是name而不是id,这是初学阶段最容易踩的死雷。
另外还有三个属性容易被混:required表示必填,为空时浏览器会拦截提交并弹出提示;disabled表示禁用,字段值不会提交;readonly表示只读,字段值会提交但用户不能修改。三者使用场景完全不同,尤其注意disabled的字段默认是“不带你玩”的,服务端拿不到这个值。
select和textarea也有自己的脾气。textarea没有value属性,它的值写在标签内部,首尾不能有多余空格,否则会多出空白内容。<select>的默认行为和表单提交同样依赖name,它的选项写value,不写value时提交的是选项文本。
4. 藏在head里的meta:整页信息的传达窗口
<meta>是我见过的最容易被忽略、又最影响页面外部表现的标签。它不渲染在页面上,浏览器行为、搜索引擎摘要、社交平台分享卡片,全都被它操控着。
4.1 必需的meta:charset和viewport
每个页面的<head>里都该有个<meta charset="utf-8">,它告诉浏览器这个文档用什么编码解析。如果这个标签缺失或者位置不对,中文页面有很大概率出现乱码。这个标签必须放在<head>的最前面,因为浏览器一旦遇见它就会立刻用指定编码重新解析后续内容,放得越靠前越安全。
viewport是移动端页面的命根子:
html复制<meta name="viewport" content="width=device-width, initial-scale=1.0">
不写这行的话,手机上会把页面当成桌面宽度渲染,字体小到需要手动放大。width=device-width让页面宽度等于设备宽度,initial-scale=1.0设置初始缩放比例。这两个值基本是标准答案,不需要改。
4.2 给搜索引擎和社交平台看的meta
搜索引擎摘要主要看description:
html复制<meta name="description" content="这是一个关于HTML标签用法的教程合集,包含表格、表单、meta、语义化等标签的详细解析与实际案例。">
这段文字大概率会直接出现在搜索结果标题下方。写了比不写好,写得好比写了强,控制在70到80个汉字以内,把页面核心内容说清楚就行。
keywords曾经很重要,但现在主流搜索引擎基本不看重它,写了不亏,不写也不影响。
社交媒体分享卡片依赖的是Open Graph协议,Facebook、微信、微博等平台读取的都是这套标签。核心几个:
html复制<meta property="og:title" content="HTML基本标签的用法第二弹">
<meta property="og:description" content="表格、表单、meta、语义化标签的实战详解">
<meta property="og:image" content="https://example.com/cover.png">
<meta property="og:type" content="article">
Twitter还有一套twitter:card标签,原理相同。这套标签建议在任何一个可能会被分享的页面上都加上,不然分享出去就是光秃秃一条链接,观感差很多。
4.3 meta的行为控制:refresh和robots
<meta http-equiv="refresh" content="5;url=https://example.com">可以实现在5秒后自动跳转到指定地址。听起来方便,但实际使用要非常克制——默认的刷新/跳转行为对无障碍用户是灾难,读屏器用户根本来不及读完整页内容就被带走。能用服务端302跳转的地方,就不要用meta refresh。
robots控制搜索引擎爬虫的抓取行为:
html复制<meta name="robots" content="noindex, nofollow">
noindex禁止索引当前页面,nofollow禁止继续追踪页面上的链接。个人网站上如果有一些不想被搜到的隐私页面,这个标签很实用。需要注意它只对“遵守协议”的搜索引擎有效,不能当作真正的权限控制手段。
5. img和a的高频坑:加载失败与不跳转的完整排查链
<img>和<a>是第一弹就讲过的基础标签,但正因为基础,用户踩坑的姿势反而最多。每天都有大量搜索“img标签图片加载失败”“a标签不跳转”的人,我把这两类问题合在一起,按排查链路完整过一遍。
5.1 img加载失败的原因与兜底处理
图片加载失败的常见原因不超过五种:路径写错、文件不存在、服务器跨域限制、防盗链、网速慢超时。这些都要靠onerror事件甚至更前沿的方案来兜底。
最简单的兜底是在src指向的图片加载失败时,用onerror换成占位图:
html复制<img src="real.jpg" alt="商品图片" onerror="this.src='/images/placeholder.png'">
这个方案有个隐患:如果占位图本身也加载失败,onerror会被再次触发,形成死循环。需要加个判断,只允许替换一次:
html复制<img src="real.jpg" alt="商品图片"
onerror="if (!this.dataset.err) { this.dataset.err = '1'; this.src='/images/placeholder.png'; }">
更彻底的办法是直接在CSS层面给img设置背景图:
css复制img {
background: #f0f0f0 url('/images/placeholder.png') no-repeat center;
background-size: contain;
}
图片加载失败时,img元素依然存在,背景图会透过透明的图片区域显示出来,用户看不到裂图图标。这种方案不需要JavaScript,兼容性极好,我自己的项目里首选这个。
还有几个img属性值得用上。alt是替代文本,图片挂了还能告诉用户图片要表达什么,也是搜索引擎理解图片内容的主要依据。loading="lazy"可以让页面下方的图片进入视口前不加载,对多图页面有明显的性能提升。width和height建议在HTML里写明,可以避免图片加载时页面布局抖动——浏览器会先按照这个尺寸占位。
5.2 a标签不跳转的排查顺序
“点击链接没反应”这种问题,最忌讳直接对着代码瞎猜。我一般按照下面这个顺序排查,半小时内基本能定位:
第一,看href有没有写。<a>标签没有href时不具备链接能力,只有href存在,浏览器才把它当链接处理。而href="#"会跳转到页面顶部,如果本意是不跳转,应该用href="javascript:void(0)"或者干脆用<button>更合适。
第二,看有没有被JavaScript拦截。页面可能的document.querySelectorAll('a').forEach(...)级别的全局绑定,或者某个库里调用了event.preventDefault(),点击事件的默认跳转行为被取消。排查方式是DevTools给元素加事件监听断点,或者临时注释掉页面里的第三方JS脚本看看能否恢复跳转。
第三,看target="_blank"有没有生效。这个属性本身不会阻止跳转,它只是决定在哪里打开。但弹窗拦截器有时候会把target="_blank"的新窗口当作广告拦截掉——尤其当链接是动态写入DOM的时候。遇到这种情况,浏览器地址栏附近会显示“弹出窗口被阻止”的提示。
第四,看<base>标签。<base target="_blank">会改变页面上所有没有显式指定target的链接的打开方式,有时候是别人为了某个功能加的base标签,影响了页面全局行为。
第五,看CSS。pointer-events: none能让链接完全“点不中”,一般出现在按钮禁用态或遮罩层上,检查元素时记得看一眼。另外,z-index层级问题会导致链接被其他元素盖住,点的是链接但实际命中的是上层的透明元素。
5.3 锚点跳转和返回顶部的细节
HTML纯标签的“返回顶部”就是锚点链接,一个id为top的元素加上<a href="#top">就能实现。第一弹可能没展开这个,我在这里补一句:锚点跳转的默认行为是瞬间跳到对应位置,如果想要平滑滚动,可以在CSS里加html { scroll-behavior: smooth; },一行代码搞定,不需要JavaScript。
锚点还有个容易犯的错误:目标元素的id不要以数字开头,这在一些浏览器里会定位失败。另外如果页面使用position: sticky做固定导航,锚点跳转后内容会被导航遮住,常见的处理是给目标元素加一个scroll-margin-top,值等于导航高度。
6. 语义化标签:让页面结构自己说话
每次聊到语义化标签,总有开发者觉得“div一把梭”也没什么大不了。当页面规模从一页变成几十个页面的时候,语义化的价值会被成倍放大。这节把HTML5的几个结构标签讲透。
6.1 HTML5结构标签速览:header、nav、main、article、section、aside、footer
这些标签概括一个网页的常规骨架:<header>是页头,<nav>是导航,<main>是页面主体,<article>是独立内容块,<section>是分区,<aside>是侧边信息,<footer>是页脚。其中<main>最特殊——一个页面理论上只能有一个<main>,它是文档的核心内容起点,对无障碍和SEO都极其重要。
它们之间的嵌套关系也有讲究。<section>和<article>的区别经常让人纠结。我的判断标准是:内容脱离页面上下文后是否仍然有独立意义?能独立发布/转载的用<article>(博客文章、新闻条目、评论),不能独立存在的章节性内容用<section>。<section>通常带自己的<h1-h6>标题,表示一个完整的话题区域。
<aside>有两种使用场景:页面级的侧边栏,和article内部的补充说明(比如“相关阅读”“编者按”)。后者放在<article>内部同样是合法写法。
<nav>只用于主要导航区域,不是所有链接集合都叫nav。页脚里那一堆友情链接从语义上说并不构成“主要导航”,用不用<nav>都行,看项目规范。
6.2 一个语义化页面的典型布局
把上面的标签拼起来,一个典型的博客文章页结构长这样:
html复制<body>
<header>
<a href="/" class="logo">我的博客</a>
<nav>
<ul>
<li><a href="/">首页</a></li>
<li><a href="/about">关于</a></li>
<li><a href="/contact">联系</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>HTML基本标签的用法第二弹</h1>
<p>发布者:<time datetime="2024-12-01">2024年12月1日</time></p>
<p>正文内容……</p>
</article>
<aside>
<h2>相关文章</h2>
<ul>
<li><a href="#">第一弹</a></li>
<li><a href="#">第三弹</a></li>
</ul>
</aside>
</main>
<footer>
<small>© 2024 我的博客</small>
</footer>
</body>
这套结构在浏览器里看起来和div布局没什么两样,但代码的可读性、维护性,以及屏幕阅读器对页面结构的感知都会好很多。
6.3 语义化的实际收益:可访问性、SEO与可维护性
语义化标签对文章类页面的SEO收益,比很多人想象的大。搜索引擎爬虫本身读不懂CSS,它依赖HTML结构判断内容权重和层级关系——<article>里的<h1>比<div>里的<h1>更容易被识别为页面主标题,<time>比普通文本更可能被提取为发布时间,<nav>里的链接比正文里的链接有更大的导航权重。
对无障碍适配来说,语义化标签是免费午餐。屏幕阅读器用户可以通过快捷键在<nav>、<main>、<article>之间快速跳转,就像普通人扫视页面一样。如果这些区域全用<div>,读屏器用户就只能从头到尾一个一个读完。
对开发者自己而言,语义化最大的好处是维护成本降低。半年后回来看自己的代码,<article>、<aside>、<footer>一眼就能看出页面模块的边界,不用从class命名里猜。特别是接手别人项目的时候,语义化标签就是最基础的代码注释。
最后分享一个小技巧。我在写HTML时会刻意遵循一条规则:先写语义,再写样式。意思是先不考虑任何CSS,把页面的信息结构用最合适的标签表达出来,然后才开始添加class和样式。这样做的好处是,在样式表加载失败的情况下,页面内容依然顺序清晰、层级分明,读起来不会是一团乱麻。如果看到这儿你正打算写下一个页面,不妨试一下这个流程——大概率第一次用就会觉得“真香”。
