这套HTML笔记我断断续续整理了很久。每次带新人或者帮人review页面,我发现大多数问题的根源不是某个标签不会写,而是对HTML的定位理解得不够——总觉得HTML很简单,随便套几个div就行,结果一到调样式、做SEO、适配移动端,就各种翻车。这篇文章我把常用知识点按自己的使用习惯重新过了一遍,不按教科书从一堆标签讲起,而是从实际开发中最容易出错、最值得花时间搞清楚的地方说起。内容覆盖页面骨架、语义化标签、表格表单、盒模型、返回顶部这类高频交互,再到编辑器选型和格式转换等场景延伸,适合刚入门HTML的新手,也适合写过不少页面但想补一补底层细节的朋友。
1. 页面骨架解析:DOCTYPE、lang 和 meta 这样写才不会埋坑
1.1<!DOCTYPE html>为什么决定了整个页面的解析方式
很多人写HTML是直接从网上复制模板,知道第一行要写<!DOCTYPE html>,但不知道不写会出什么问题。其实这一行是浏览器进入标准模式的开关。如果缺失,老版本浏览器会进入怪异模式(Quirks Mode),CSS盒模型计算、行高、字体缩放都可能和标准模式完全不一样。最常见的情况就是同样一个页面,别人机器上margin表现正常,你这边全部偏了几像素,排查半天,最后发现是少了DOCTYPE。
所以每次新建页面,我都会先敲下这一行:
<!DOCTYPE html>注意它不是HTML标签,也不是结束标签,只是告诉浏览器:“按HTML5标准解析这份文档”。HTML5之后这个声明必须全大写、靠前,不能带引号也不能写版本号。以前HTML4时代还要写一堆DTD,现在彻底不需要了,没必要为了显得专业去翻老资料。
1.2lang="zh-CN"不只是一行可有可无的属性
<html lang="zh-CN">这个属性最容易被忽略,但它实际影响的是屏幕阅读器的发音、浏览器的翻译建议、以及CSS里:lang()伪类的匹配。比如一个英文网站里有一段中文引文,你想让中文部分的字体或排版走另一套规则,就可以用:lang(zh)去选中它。如果你把lang写成lang="en",屏幕阅读器会用英文语音去读中文内容,读出来非常奇怪,用户会直接认为网站有故障。
大小写方面,规范里语言标签不区分大小写,zh-CN、zh-cn都能用。但行业惯例一般写zh-CN,因为ISO 3166-1 alpha-2国家代码习惯大写。如果要面向中国大陆的简体中文用户,写zh-CN是安全的;如果是通用中文内容,也可以简写为zh-Hans,表示“简体中文”。我自己的习惯是:按目标区域写,国内站点统一lang="zh-CN"。
1.3 head 里那些 meta 到底各自在干什么
head区域最容易出现的情况是“复制模板一时爽,排查乱码火葬场”。我见过不少项目在head里堆了半屏meta,真正有用的其实就那么几个。
<meta charset="UTF-8">必须放在head最前面,最好在前5行之内。它是字符编码声明,专门解决乱码问题。如果一个页面没有它,浏览器可能用系统默认编码去猜,中文就很容易变成“锟斤拷”一类的乱码。实际工作中遇到中文页面乱码,第一反应就应该是检查这个meta是否缺失、是否被放得太靠后。
<meta name="viewport" content="width=device-width, initial-scale=1.0">是移动端适配的基础。它告诉浏览器按设备宽度来渲染页面,而不是用默认的980像素虚拟宽度。很多人做完手机页面后发现字小得看不清,往往就是少了这一行。这里的initial-scale=1.0表示初始缩放比例为1,如果不写,某些旧浏览器会自动放大页面。
<meta name="description" content="页面简介">是给搜索引擎看的内容摘要。虽然它对排名的影响没有以前那么大了,但在搜索结果里仍然可能作为描述文字展示,也影响点击率。建议每个页面都写一句真实、简洁、能概括页面核心内容的描述,不要堆关键词。
一个干净的现代页面骨架应该是这样:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="HTML常用知识点总结笔记"> <title>页面标题</title> </head> <body> </body> </html><title>也不要漏,它是页面标签页上的名字,也是搜索引擎结果标题。我见过页面写完title还是“新建文档”的,这种细节给用户的第一印象真的很差。
2. 标签不是随便用:div、span 和语义化标签的取舍
2.1 先搞清楚 div 和 span 的定位
div是块级元素,从头到尾占一整行;span是行内元素,只占内容那么宽。这是最基础的区别。但在实际页面里,div和span都是没有语义的,它们只表示“这是一个容器”。把整个页面塞满div会带来两个后果:第一,搜索引擎和屏幕阅读器无法理解内容结构;第二,你自己三个月后再看代码,根本分不清哪块是哪块。
我常用一个类比:纯div布局就像把所有物品都塞进同一种纸箱,然后在纸箱上贴便利贴做标记。短期没问题,但一旦需要搬东西、找东西,效率就很低。语义化标签相当于给每个区域设计不同规格的箱子:header装页头,nav装导航,main装主体,footer装底部。代码的“形状”一看就懂。
2.2 语义化标签怎么选:header、nav、main、article、section、aside、footer
语义化标签不是越多越好,而是要在对的位置用对标签。最常见的组合是一个页面分成这几个区域:
<header> <nav> <ul> <li><a href="/">首页</a></li> <li><a href="/posts">文章</a></li> <li><a href="/about">关于</a></li> </ul> </nav> </header> <main> <article> <h1>HTML常用知识点总结笔记</h1> <p>这里是正文内容。</p> </article> <aside> <h2>相关推荐</h2> <ul> <li><a href="#">CSS盒模型</a></li> </ul> </aside> </main> <footer> <p>备案信息与版权说明</p> </footer>header不一定是页面顶部,它可以作为某个区块的头部;footer同理。main在一个页面里最好只出现一次,表示核心内容区域。article强调的是“可以独立分发或复用”,一篇博文、一个新闻条目、一条评论都可以用article;section是普通内容分组,强调的是“主题相关”,一般会带一个标题。如果你只是想把几个元素包起来做布局,没有任何主题含义,用div完全没问题。
我见过不少人纠结article和section的差别,实际判断标准很简单:把这块内容单独拿出来,若依然完整有意义,就用article;如果只是整篇文章的一个章节,可以用section。拿不准的时候,div加注释也远好过乱用section。
2.3 标题层级 h1-h6 是页面大纲,别乱跳
h1到h6不仅是字体大小不同,它们构成页面的标题层级,就像文章目录。一个页面最好只有一个h1,而且是核心内容的标题,不是logo、不是公司名、不是一句slogan。如果一个页面里有多个h1,屏幕阅读器用户的“按标题跳转”会变得很混乱,分不清哪个才是主内容。
同时要避免跳级,比如h1之后直接写h4。也许视觉上字体大小刚好合适,但从语义上讲,目录会从一级突然跳到四级,中间缺少h2和h3,结构不完整。正确做法是用CSS去调整视觉大小,而不是靠标签层级去“尽量凑”。
我现在写页面时会先在纸上或编辑器的目录面板里看一遍标题结构。如果摘要页面用ul列表代替标题,也会被跳过标题层级,结果就是屏幕阅读器用户根本找不到文章结构,这个问题很隐蔽但很关键。
3. 表格与表单:数据展示和用户交互里的细节决定体验
3.1 表格不只是 table/tr/td,还有 caption、thead、tbody、th
很多新手写表格直接用table > tr > td,忽略了三块重要内容:caption、thead/tbody/tfoot、th的scope。在数据比较多、结构稍微复杂的表格里,没有这些标签,屏幕阅读器用户很难知道当前单元格属于哪一行、哪一列。
推荐的表格结构:
<table> <caption>2024年前端技能学习计划</caption> <thead> <tr> <th scope="col">技能方向</th> <th scope="col">优先级</th> <th scope="col">预计用时</th> </tr> </thead> <tbody> <tr> <td>HTML</td> <td>高</td> <td>2周</td> </tr> <tr> <td>CSS</td> <td>高</td> <td>4周</td> </tr> </tbody> </table>caption是表格的标题,类似图片的figcaption,让表格在没有上下文的时候也能被理解。thead、tbody用来区分表头和表体,如果页面里有多组表头或者需要滚动时固定表头,它们是基础。th scope="col"或th scope="row"用来声明这个表头控制的是整列还是整行,对读屏软件来说,这直接决定了它朗读时会不会丢失行列关系。
至于border="1"这种老式写法,能不用就不用。表格边框完全交给CSS控制,HTML里只保留结构。
3.2 表单提交里最容易漏掉的 name 与 label
表单是页面和用户交互最重要的入口。很多人学表单时只记住了<input>和<button>,却漏掉了两个决定性的东西:name和label。
name决定数据以什么字段名提交给后端。没有name的输入框,即使用户填了内容,提交时数据里也不会有它。比如一个用户名输入框:
<input type="text" id="username" name="username">id是给CSS、JS、label用的,name才是给后端用的。这两个经常被搞混,我的经验是:表单提交相关的字段必须有name,且命名统一,前端用 camelCase、后端用 snake_case 就会造成额外沟通成本。
label的作用是给输入框一个可点击的文字标签。用户点“用户名”三个字时,光标应该自动跳到输入框里,这对鼠标操作和读屏软件都非常重要。正确写法:
<label for="username">用户名</label> <input type="text" id="username" name="username">for的值要对应输入框的id,不是name。如果id不存在,点击文字后焦点不会跳转。还有一种写法是把 input 嵌在 label 里面:
<label> 用户名 <input type="text" name="username"> </label>这种写法不用for/id也能关联,但样式控制上稍微麻烦一点,我一般还是优先用for/id显式关联。
3.3 选择框以外的 input type:好看不等于好用
<select>是原生的下拉选择框,适合选项有限、且需要明确选择的场景。如果要支持输入,可以用<datalist>做联想输入,但它在不同浏览器里的交互差异比较大,生产环境要仔细测试。
还有几个 input type 平时容易被忽略:
type="email"在移动端会调出带@和.的键盘,同时浏览器会做基础格式校验。type="number"右侧会带步进箭头,但用户仍然可以输入字母e这类字符,因为科学计数法里1e3是合法数字。如果需要严格数字,最好用inputmode="decimal"配合pattern,不要只依赖type="number"。type="date"在不同浏览器里样式差异很大,目前没有纯CSS让它在各端显示完全一致。要做好看的日期选择器,通常要引入组件库或者自己造轮子。type="file"的accept属性只是“推荐”文件类型,用户仍然可以切换成“所有文件”选择其他格式,前端过滤不能代替后端校验。我接过一个需求,前端设置了accept="image/*"结果用户传了个exe进去,好在后端拦了,不然就是事故。
表单整体记得加required、pattern等校验属性。但要注意,这些校验只能算用户输入体验的一部分,不能代替服务端校验。required能拦下绝大多数空提交,但攻击者可以绕过前端直接请求接口。后端校验永远是底线。
4. 盒模型与默认样式:HTML结构真正落地时的第一个坎
4.1 总宽度为什么和你写的不一样
HTML负责内容结构,CSS负责视觉呈现。而 CSS 盒模型是所有视觉呈现里绕不开的第一关。新手最常见的困惑是:“我明明把 width 写成 100px,为什么渲染出来是 122px?”
因为默认情况下width只设置内容区宽度,padding和border都要额外加上去,这就是content-box盒模型。假设设置了:
.box { width: 100px; padding: 10px; border: 1px solid #000; }那么元素实际占据的宽度是:
100(内容) + 10*2(左右padding) + 1*2(左右border) = 122px这也解释了为什么两个宽度都是 100px 的元素并排摆放会换行:因为它们实际总宽超过父容器。解决办法是把盒模型切成border-box:
.box { box-sizing: border-box; width: 100px; padding: 10px; border: 1px solid #000; }这时width直接就是元素最终占据的宽度,padding和border会从内容区里挤出来,内容区实际只剩 78px。border-box更符合人脑直觉,所以现在很多项目会做全局设置:
*, *::before, *::after { box-sizing: border-box; }margin不参与元素宽度的计算,但会影响元素在页面里的实际占位。两个相邻元素的 margin 可能会合并(margin collapse),这在上下排列的块级元素之间尤其常见。如果你发现上下两个块之间只生效了较大的 margin,那就是外边距折叠在起作用,不是代码写错了。
4.2 为什么 h1 自带大号字体,ul 自带圆点
浏览器在没有任何CSS文件时,也会给HTML标签套一套默认样式。h1字号大、body有 margin、ul自带 padding 和圆点、a自带蓝色下划线,这些都是用户代理样式表(User Agent Stylesheet)的功劳。它不是bug,只是不同浏览器对这些默认值的定义不完全一样。
所以做项目时,常见的做法是写一段 reset 或 normalize。最极端的 reset 会把所有 margin、padding 清零:
* { margin: 0; padding: 0; }可这样把所有标签都清零也有副作用,比如ul的列表语义还在,但列表符号和缩进没了,屏幕阅读器依然能读出列表结构,可视觉上要重新补回来。所以现在很多项目选择用 Tailwind 的 preflight,或者只针对自己用到的标签做显式定义。我的建议是:新建项目时至少把body的 margin 清掉,把img的max-width: 100%加上,再把box-sizing统一。这三点能避免绝大多数“看起来奇怪”的问题。
4.3 flex 能改视觉顺序,但改不了文档顺序
CSS 的 flex 和 grid 很强大,可以让页面视觉顺序和 HTML 里写的顺序不一样。但这里有个坑:视觉上把元素移到前面了,屏幕阅读器读的顺序仍然是 DOM 顺序,键盘 Tab 走的也是 DOM 顺序。如果 DOM 是“侧边栏 → 正文”,视觉上却是“正文 → 侧边栏”,读屏用户就会先听到侧边栏再听到正文,正文很可能被淹没在一堆导航和推荐链接里。
解决思路有两个:要么直接调整 HTML 顺序,让正文在 DOM 里靠前,再配合 flex 的order或 grid 的grid-area调整布局位置;要么在做完布局后用键盘从页面顶部 Tab 一遍,确认焦点顺序是不是符合阅读预期。我自己的经验是,能用 HTML 顺序解决就尽量用 HTML 顺序,尽量避免用order去调换阅读顺序,因为它对无障碍的破坏非常隐蔽。
5. 一键返回顶部:一个小需求里藏着的滚动与无障碍知识
5.1 返回顶部有三种常见实现
“一键返回顶部”在热搜里出现得很多,看起来很简单,但实现方式不同,体验差别挺大。
第一种是锚点:
<a href="#top">返回顶部</a>然后给body或某个元素加id="top"。点击后页面会跳到该锚点,但同时会在 URL 后面追加#top,刷新时页面会停在顶部。这种方式胜在零依赖,缺点是跳转是瞬间完成的,在长页面里用户会突然失去滚动位置感。
第二种是用 JS 的window.scrollTo:
window.scrollTo({ top: 0, behavior: 'smooth' });这是现在最推荐的方式。behavior: 'smooth'会让滚动变得平滑,用户体验好;同时它接受对象参数,与scrollBy或直接改scrollTop相比,语义更清晰。
第三种是兼容老浏览器的方式:
document.documentElement.scrollTop = 0;现代浏览器基本都不需要这么写了,但如果你维护的是老项目,有些坑还是要了解:Safari 早年间对scrollTo的平滑支持不稳定,所以有些代码会退回到document.body.scrollTop和document.documentElement.scrollTop都设置一遍的写法。现在环境已经改善,新代码统一用scrollTo就好。
5.2 纯 CSS 的 scroll-behavior 和 JS 怎么配合
现在很多项目会在 CSS 里写:
html { scroll-behavior: smooth; }这样页面里所有锚点跳转都会自动平滑,不再需要 JS。但也要注意:scroll-behavior: smooth会“劫持”所有滚动定位操作,包括一些你不想生效的场景。比如前端路由切换后想直接跳回顶部,如果有平滑滚动,页面会从当前位置慢慢滚上去,体验反而拖沓。
所以我的习惯是:全局不轻易加scroll-behavior: smooth,只在特定容器或特定操作里用 JS 控制。返回顶部按钮的滚动,统一用scrollTo({ top: 0, behavior: 'smooth' })。如果用户开启了系统的“减少动效”选项,还要照顾到无障碍:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' });这样对希望减少动画的用户,会直接跳到顶部,不强行播放滚动动画。
5.3 把返回顶部按钮做成组件时要考虑什么
写一个完整的返回顶部按钮,需要处理几个细节:
- 初始隐藏:页面还在顶部时没必要显示按钮。
- 滚动监听:用
window.scrollY > 300判断是否显示,滚动事件触发频率很高,建议用requestAnimationFrame节流。 - 固定定位:按钮一般用
position: fixed放在右下角。 - 可访问性:给按钮加
aria-label="返回顶部",这样读屏软件不会只读一个“↑”箭头。 - 特殊容器:如果页面主体不是
window滚动的,而是某个overflow: auto的 div,那要滚动的是这个容器,不是window。
一个最小实现可以参考:
<button id="backTop" aria-label="返回顶部">↑</button>#backTop { position: fixed; right: 20px; bottom: 20px; display: none; width: 44px; height: 44px; border: none; border-radius: 50%; background: #333; color: #fff; font-size: 20px; cursor: pointer; }const backTop = document.getElementById('backTop'); function updateBackTop() { if (window.scrollY > 300) { backTop.style.display = 'block'; } else { backTop.style.display = 'none'; } } window.addEventListener('scroll', () => { requestAnimationFrame(updateBackTop); }, { passive: true }); backTop.addEventListener('click', () => { const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' }); });为什么按钮宽高要设成 44px?因为 44x44 是移动端比较常见的最小可点击区域标准,能避免用户点不准。这类细节在 PC 上不明显,但换成触摸设备就很重要了。
6. 从编辑器到在线运行:HTML相关的工具场景与复用技巧
6.1 不需要纠结编辑器,但需要会调试
很多新手问:“HTML用什么编辑器好?”答案是:顺手就行。VS Code 有大量插件、Live Server 能实时刷新,是目前最主流的选项。但如果你在 Ubuntu 等 Linux 桌面环境里,没有图形编辑工具时用 Vim 也能写;只要能保存为.html文件并让浏览器打开,剩下就是纯手写能力的问题。
比编辑器更重要的是会调试。打开浏览器 F12,用 Elements 面板查看元素和样式,用 Network 面板看资源加载情况,用 Console 看报错信息。页面样式和预期不一致时,不要凭感觉改,先在 Elements 里找到该元素,看哪条规则覆盖了它,再决定改哪个选择器。我给新人的一条实操建议是:在 Elements 面板里右键元素选 “Copy -> Copy outerHTML”,可以直接拿到网页渲染后的真实结构,排查动态生成的 HTML 时非常有用。
6.2 data:text/html 与在线运行的小技巧
浏览器地址栏其实可以当做一个临时的 HTML 运行环境。输入data:text/html,<h1>hello</h1>回车,浏览器会直接渲染这段 HTML。这个技巧特别适合临时测试单个标签或一小段代码。
但要注意,data URL 里如果有中文、空格、尖括号,最好做 URL 编码,否则部分浏览器会解析失败。比如:
data:text/html,<p style="color:red">test</p>这种简单的能用,但更长的代码建议复制到本地 HTML 文件或在线运行平台。在线运行的好处是方便分享和演示,像 CodePen、JSFiddle 都支持实时编辑,但要注意:公共平台上的代码不要太随意地填入隐私信息,公司项目代码尤其不要直接粘贴到在线工具里,这是基本的安全习惯。
6.3 HTML转Markdown、表格转Excel、PyQt5显示HTML
实际工作中,HTML 不只是给网页用的,还经常要在不同格式之间转换。
把 HTML 转成 Markdown,我自己最常用的是命令行工具 pandoc:
pandoc input.html -t markdown -o output.md它的转换质量很高,能保留标题、列表、链接等结构。如果要在 Node.js 环境里批量转换,可以用 turndown 或开源的html-to-md包。转换后记得检查一下代码块和特殊字符,因为不是所有 HTML 标签都有 Markdown 等价物。
把 HTML 里的表格转成 Excel 或 WPS 表格,有个很土的技巧:直接用 WPS 或 Excel 打开 HTML 文件,选择“从 HTML 导入”,再另存为 xlsx。如果是通过程序处理,用 Python 的 pandas 配合pd.read_html()也能把表格读成 DataFrame,再导出 Excel。比如:
import pandas as pd dfs = pd.read_html('page.html') dfs[0].to_excel('output.xlsx', index=False)这个场景在数据报表、网页信息抓取时很常用。pd.read_html会自动识别table标签,所以前面提到的表格结构如果写规范,转换时会更顺手。
PyQt5 里显示 HTML 是另一个常见需求,比如做富文本预览、报表展示、邮件模板预览。最简单的方案是用QTextBrowser:
import sys from PyQt5.QtWidgets import QApplication, QTextBrowser app = QApplication(sys.argv) browser = QTextBrowser() browser.setHtml("<h2>标题</h2><p>正文内容</p>") browser.show() sys.exit(app.exec_())QTextBrowser支持基础的 HTML 和富文本,但它不是完整浏览器内核,加载不了现代 CSS、JavaScript 和外部网络资源。如果要在应用里展示完整的网页效果,得上QWebEngineView,那相当于内置了一个精简版浏览器。代价是程序体积变大、内存占用变高,而且依赖系统 WebEngine 库。具体选哪个,取决于你要展示的 HTML 多复杂。如果只是几行排版文本,QTextBrowser足够;如果是要运行前端项目里的页面,建议直接上QWebEngineView,不要自己造轮子。
6.4 用 W3C 校验器和预检习惯降低低级错误
写完 HTML 后,我还会做一步很多人嫌麻烦但很有效的操作:把 HTML 粘贴到 W3C Nu Markup Validator 校验一遍。它会报告标签嵌套错误、缺少必要属性、标签大小写不规范等浏览器不会主动报错的问题。浏览器容错能力很强,标签没闭合也能渲染,但代码里藏着的结构错误会成为后续维护和样式调整的隐患。校验器能把这些隐患提前暴露出来。
7. 上线前我会做的页面自查清单
HTML 写完后到底能不能直接上线?我给自己定了一张简单的自查清单,避免低级问题被带到生产环境。现在分享出来:
<!DOCTYPE html>存在,页面在标准模式下渲染。<html lang="zh-CN">和内容语言一致。<meta charset="UTF-8">在 head 前部,中文没有乱码。- 有
<meta name="viewport">,移动端缩放正常。 - 页面只出现一次 h1,标题层级没有跳级。
img都有alt,按钮都有可访问名称,纯图标按钮有aria-label。- 表单控件都有对应的
label,提交字段都设置了name。 - 语义化标签覆盖了主要区域,没有整页 div 到底。
- 把浏览器窗口缩到 375px 宽度,没有横向滚动条。
- 用键盘 Tab 走一遍页面,焦点顺序和视觉顺序基本一致。
- 按一次返回顶部按钮,滚动行为正常且不会造成页面跳动。
- F12 的 Console 里没有报错,Network 里没有 404 资源。
这清单不是什么地方的标准,只是我踩过不少坑之后积累的肌肉记忆。比如横向滚动条,很多时候不是某个元素真的需要 1000px 宽,而是有一个width: 100%再加padding导致的溢出。这时候把box-sizing: border-box加上,问题可能直接消失。
HTML 看起来是前端三件套里最简单的,但它是所有页面内容的载体。结构写得乱,CSS 和 JS 都会跟着难受。多花十分钟检查结构,后面就能省下几个小时的样式调试时间。这份笔记是我日常写页面的工具,整理出来也算给自己做了一次补充,如果你也有自己常踩的坑,欢迎补上。