前言
HTML 是所有 Web 页面和 Web 应用的基础。
无论使用 Vue、React、Angular,还是直接开发传统网站,浏览器最终处理的页面结构仍然是 HTML。掌握 HTML 不只是记住标签,更重要的是理解:
- 浏览器如何解析 HTML
- 如何使用语义化标签描述内容
- 如何构建可访问、可维护的页面
- 如何处理表单、图片、音视频等资源
- 如何提高页面性能和搜索引擎友好度
- 如何在真实项目中组织、验证和部署 HTML
本文将从最基础的 HTML 文档开始,逐步讲到表单、多媒体、响应式图片、无障碍、安全、性能优化和工程化实践,并通过一个完整项目串联主要知识点。
1. HTML5 是什么
1.1 HTML 的定义
HTML 的全称是 HyperText Markup Language,即超文本标记语言。
它使用一系列标签描述网页内容的结构和含义,例如:
html
<h1>文章标题</h1>
<p>这是一段正文。</p>
<a href="/about.html">关于我们</a>
HTML 不是编程语言,因为它本身通常不负责条件判断、循环和复杂计算。它是一种标记语言,核心任务是描述内容:
- 这是标题
- 这是段落
- 这是导航
- 这是表格
- 这是表单
- 这是图片
- 这是文章主体
1.2 "HTML5"在今天意味着什么
历史上,HTML5 是 HTML 的一次重要演进,引入或规范了大量现代 Web 能力,例如:
header、main、article、section等语义化元素audio和video- Canvas
- 更丰富的表单控件
- Web Storage
- History API
- Web Workers
- 自定义数据属性
现在 HTML 主要以持续更新的 Living Standard 形式演进,因此开发中所说的"HTML5",通常泛指现代 HTML 标准和相关 Web 平台能力。
需要注意:地理定位、WebSocket、Web Workers 等能力经常被统称为"HTML5 API",但它们并不全是 HTML 标签,而是 Web 平台提供的独立 API。
HTML 标准可参考:
1.3 HTML5 的主要特点
HTML5 相比早期页面开发方式,主要带来了以下变化:
- 文档声明更简单
- 页面结构更语义化
- 浏览器原生支持音视频
- 表单控件更加丰富
- 更适合移动端页面
- 提供更多浏览器端 API
- 更重视可访问性与跨设备能力
- 减少对 Flash 等插件的依赖
2. HTML、CSS 与 JavaScript 的关系
一个现代网页通常由 HTML、CSS 和 JavaScript 共同组成。
| 技术 | 主要职责 | 类比 |
|---|---|---|
| HTML | 描述页面结构与内容 | 建筑骨架 |
| CSS | 控制颜色、尺寸、布局和动画 | 装修与外观 |
| JavaScript | 处理交互、数据和业务逻辑 | 控制系统 |
| HTTP | 在浏览器和服务器之间传输资源 | 运输网络 |
例如:
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>计数器</title>
<style>
button {
padding: 8px 16px;
color: white;
background: #2563eb;
border: 0;
border-radius: 6px;
}
</style>
</head>
<body>
<h1>当前数量:<span id="count">0</span></h1>
<button id="increase" type="button">增加</button>
<script>
const countElement = document.querySelector("#count");
const button = document.querySelector("#increase");
let count = 0;
button.addEventListener("click", () => {
count += 1;
countElement.textContent = String(count);
});
</script>
</body>
</html>
在这个例子中:
- HTML 定义标题、数字和按钮
- CSS 设置按钮样式
- JavaScript 响应点击事件并修改数字
工程中应该尽量做到职责分离:
text
project/
├─ index.html
├─ styles/
│ └─ main.css
└─ scripts/
└─ main.js
3. 第一个 HTML5 页面
创建一个名为 index.html 的文件:
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<meta
name="description"
content="这是我的第一个 HTML5 页面"
>
<title>我的第一个 HTML5 页面</title>
</head>
<body>
<h1>Hello HTML5</h1>
<p>这是我的第一个 HTML5 页面。</p>
</body>
</html>
保存后,直接使用浏览器打开即可。
3.1 <!doctype html>
html
<!doctype html>
这行代码告诉浏览器:当前文档按照现代 HTML 标准解析。
它不是普通 HTML 标签,也不需要闭合。
如果缺少正确的文档声明,浏览器可能进入兼容模式,导致布局和标准行为出现差异。
3.2 <html>
html
<html lang="zh-CN">
html 是整个文档的根元素。
lang="zh-CN" 表示页面主要语言是简体中文。正确设置语言有利于:
- 屏幕阅读器选择正确的发音规则
- 浏览器提供翻译功能
- 搜索引擎理解页面语言
- 拼写检查工具正确工作
3.3 <head>
head 保存页面元数据和资源声明,例如:
- 字符编码
- 页面标题
- SEO 描述
- 样式表
- 图标
-预加载资源 - JavaScript 文件
head 中的内容大多不会直接显示在页面正文中。
3.4 <body>
body 中放置用户可见的页面内容,例如:
- 标题
- 段落
- 图片
- 表格
- 表单
- 导航
- 按钮
4. HTML 的基本语法
4.1 元素、标签与内容
一个普通 HTML 元素通常由开始标签、内容和结束标签组成:
html
<p>这是一段内容</p>
其中:
<p>是开始标签这是一段内容是文本内容</p>是结束标签- 整体称为一个元素
4.2 属性
属性用于提供元素的附加信息:
html
<a href="https://example.com" target="_blank">
访问网站
</a>
这里:
href指定链接地址target="_blank"表示在新标签页打开
属性通常写在开始标签中:
html
<元素名 属性名="属性值">内容</元素名>
推荐始终使用引号包裹属性值:
html
<input type="text" name="username">
4.3 布尔属性
有些属性只要存在就表示开启:
html
<input type="checkbox" checked>
<input type="text" required>
<button type="button" disabled>不可点击</button>
下面的写法没有必要:
html
<input required="true">
对于布尔属性,重要的是属性是否存在,而不是字符串值是不是 "true"。
4.4 空元素
部分元素不能包含子内容,因此没有结束标签:
html
<img src="avatar.png" alt="用户头像">
<input type="text">
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
<br>
<hr>
HTML 中通常不需要写成 XML 风格:
html
<img src="avatar.png" alt="用户头像" />
两种形式在 HTML 中通常都能被浏览器处理,但团队应保持统一风格。
4.5 标签嵌套
HTML 元素可以嵌套:
html
<article>
<h2>文章标题</h2>
<p>文章内容</p>
</article>
必须保持正确的嵌套顺序。
错误示例:
html
<p>
<strong>重要内容
</p>
</strong>
正确示例:
html
<p>
<strong>重要内容</strong>
</p>
4.6 注释
html
<!-- 这是 HTML 注释 -->
注释适合解释必要的结构或兼容性处理,但不要在注释中放入:
- 密码
- Token
- 内网地址
- 私密业务数据
- 不希望用户看到的信息
HTML 注释仍可能被发送到浏览器,用户可以通过"查看网页源代码"看到它。
4.7 字符实体
当页面需要显示 HTML 保留字符时,可以使用字符实体:
| 字符 | 实体 |
|---|---|
< |
< |
> |
> |
& |
& |
| 不换行空格 | |
" |
" |
' |
' |
示例:
html
<p>表达式:5 < 10</p>
<p>技术栈:HTML & CSS</p>
不要使用大量 控制布局,页面间距应该交给 CSS。
5. 文档结构与页面元数据
一个适合实际项目使用的文档结构如下:
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<meta
name="description"
content="HTML5 从入门到工程实践"
>
<meta name="theme-color" content="#2563eb">
<title>HTML5 从入门到工程实践</title>
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="/styles/main.css">
<script type="module" src="/scripts/main.js"></script>
</head>
<body>
<header>网站头部</header>
<main>页面主体</main>
<footer>网站底部</footer>
</body>
</html>
5.1 字符编码
html
<meta charset="UTF-8">
UTF-8 可以表示绝大多数语言字符,现代 Web 项目应统一使用 UTF-8。
为了尽早确定编码,通常将字符编码声明放在 head 前部。
5.2 移动端视口
html
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
它告诉移动端浏览器:
- 页面布局宽度跟随设备宽度
- 初始缩放比例为 1
缺少这项配置时,移动端可能按照桌面页面宽度进行缩放。
5.3 页面标题
html
<title>HTML5 从入门到工程实践</title>
标题通常会显示在:
- 浏览器标签页
- 收藏夹
- 搜索结果
- 分享预览的候选信息中
每个页面应该具有清晰且相对独立的标题。
5.4 页面描述
html
<meta
name="description"
content="系统介绍 HTML5 语法、语义化、表单、多媒体和工程实践"
>
页面描述可能被搜索引擎用作搜索结果摘要,但搜索引擎也可能根据搜索内容重新选择摘要。
5.5 引入 CSS
html
<link rel="stylesheet" href="/styles/main.css">
5.6 引入 JavaScript
现代项目推荐使用 ES Module:
html
<script type="module" src="/scripts/main.js"></script>
模块脚本具有自己的作用域,并支持:
javascript
import { formatDate } from "./date.js";
6. 文本与内容标签
6.1 标题
HTML 提供六级标题:
html
<h1>页面主标题</h1>
<h2>一级章节</h2>
<h3>二级章节</h3>
<h4>三级章节</h4>
<h5>四级章节</h5>
<h6>五级章节</h6>
推荐按照内容层级使用标题,而不是根据默认字体大小选择标签。
错误做法:
html
<h1>文章标题</h1>
<h4>文章第一章</h4>
<h2>第一章的小节</h2>
正确做法:
html
<h1>文章标题</h1>
<h2>文章第一章</h2>
<h3>第一章的小节</h3>
标题大小应由 CSS 控制。
6.2 段落
html
<p>HTML 用于描述页面内容结构。</p>
不要为了制造间距连续写空段落:
html
<p> </p>
<p> </p>
应该使用 CSS 的 margin 或 padding。
6.3 强调与重要性
html
<p>
提交前请<strong>确认订单金额</strong>。
</p>
<p>
这里<em>特别强调</em>操作顺序。
</p>
strong表示较高的重要性em表示语气或强调b通常表示视觉上需要注意但不增加重要性i可以表示术语、外语、特殊语气等
不要单纯为了加粗就滥用 strong。
6.4 代码相关标签
html
<p>使用 <code>npm run build</code> 构建项目。</p>
<pre><code>const message = "Hello HTML5";
console.log(message);</code></pre>
<p>按下 <kbd>Ctrl</kbd> + <kbd>S</kbd> 保存。</p>
<p>程序输出:<samp>Build completed</samp></p>
code:代码片段pre:保留空格和换行kbd:用户键盘输入samp:程序输出
6.5 引用
块级引用:
html
<blockquote cite="https://example.com/article">
<p>语义化 HTML 能够让内容结构更加清晰。</p>
</blockquote>
行内引用:
html
<p>他认为:<q>结构应该优先于样式。</q></p>
6.6 时间
html
<time datetime="2026-08-23">2026 年 8 月 23 日</time>
机器可读的 datetime 有利于工具理解时间含义。
6.7 删除与新增
html
<p>
原价 <del>199 元</del>,
现价 <ins>99 元</ins>。
</p>
6.8 其他常用元素
html
<p>搜索关键词:<mark>HTML5</mark></p>
<p>水的化学式是 H<sub>2</sub>O。</p>
<p>2<sup>10</sup> 等于 1024。</p>
<p><small>活动最终解释权归平台所有。</small></p>
<hr>
hr 表示主题层面的分隔,不应仅作为装饰线使用。
7. HTML5 语义化布局
7.1 为什么要语义化
早期页面经常使用大量无含义的 div:
html
<div class="header"></div>
<div class="nav"></div>
<div class="content"></div>
<div class="footer"></div>
现代 HTML 可以直接描述结构:
html
<header></header>
<nav></nav>
<main></main>
<footer></footer>
语义化的好处包括:
- 代码更容易阅读
- 屏幕阅读器更容易导航
- 搜索引擎更容易理解内容
- 浏览器阅读模式更容易提取正文
- 团队协作和后期维护更简单
7.2 常用语义化元素
| 元素 | 含义 |
|---|---|
header |
页面或区域头部 |
nav |
主要导航区域 |
main |
页面主要内容 |
article |
可独立分发的完整内容 |
section |
按主题划分的内容区域 |
aside |
与主体间接相关的补充内容 |
footer |
页面或区域底部 |
figure |
图片、图表、代码等独立内容 |
figcaption |
figure 的说明文字 |
address |
与当前内容相关的联系信息 |
7.3 完整布局示例
html
<body>
<a href="#main-content" class="skip-link">
跳到主要内容
</a>
<header>
<h1>前端技术博客</h1>
<nav aria-label="主导航">
<ul>
<li><a href="/">首页</a></li>
<li><a href="/articles/">文章</a></li>
<li><a href="/about/">关于</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<article>
<header>
<h2>HTML5 从入门到实践</h2>
<p>
发布于
<time datetime="2026-08-23">2026 年 8 月 23 日</time>
</p>
</header>
<section>
<h3>HTML 基础</h3>
<p>这里介绍 HTML 基础语法。</p>
</section>
<section>
<h3>HTML 工程实践</h3>
<p>这里介绍工程规范。</p>
</section>
<footer>
<p>作者:示例作者</p>
</footer>
</article>
<aside aria-label="相关文章">
<h2>相关文章</h2>
<ul>
<li><a href="/css-guide/">CSS 入门</a></li>
<li><a href="/javascript-guide/">JavaScript 入门</a></li>
</ul>
</aside>
</main>
<footer>
<p>© 2026 前端技术博客</p>
</footer>
</body>
7.4 article、section 与 div 的区别
使用 article
当内容可以相对独立地发布、复用或分发时使用:
html
<article>
<h2>新闻标题</h2>
<p>新闻内容</p>
</article>
典型场景:
- 博客文章
- 新闻
- 商品卡片
- 论坛帖子
- 用户评论
使用 section
当内容属于一个明确主题区域时使用:
html
<section>
<h2>产品功能</h2>
<p>功能介绍</p>
</section>
section 通常应该拥有标题。
使用 div
当只是为了布局、分组或挂载脚本,而没有更合适的语义元素时使用:
html
<div class="card-grid">
<!-- 卡片列表 -->
</div>
原则是:优先使用含义准确的元素,确实没有合适语义时再使用 div。
8. 超链接与 URL 路径
8.1 基本链接
html
<a href="https://example.com">访问示例网站</a>
8.2 相对路径与绝对路径
html
<!-- 当前站点根目录 -->
<a href="/about.html">关于我们</a>
<!-- 当前目录 -->
<a href="./detail.html">详情</a>
<!-- 上一级目录 -->
<a href="../index.html">返回首页</a>
<!-- 完整绝对地址 -->
<a href="https://example.com/docs/">外部文档</a>
工程中应明确项目的部署根路径。部署到子目录时,以 / 开头的路径可能指向域名根目录,而不是当前项目目录。
8.3 页面内锚点
html
<a href="#chapter-security">跳到安全章节</a>
<section id="chapter-security">
<h2>安全实践</h2>
</section>
同一页面中的 id 应保持唯一。
8.4 邮件与电话
html
<a href="mailto:contact@example.com">发送邮件</a>
<a href="tel:+8613800000000">拨打电话</a>
8.5 新标签页与安全
html
<a
href="https://example.com"
target="_blank"
rel="noopener noreferrer"
>
在新标签页打开
</a>
noopener阻止新页面通过window.opener控制原页面noreferrer还会限制 Referer 信息发送
是否使用 noreferrer 需要结合业务统计和隐私要求决定。
8.6 不要使用链接模拟按钮
错误示例:
html
<a href="javascript:void(0)" onclick="saveData()">
保存
</a>
正确示例:
html
<button type="button" id="save-button">
保存
</button>
基本原则:
- 跳转到其他地址使用
a - 执行页面操作使用
button
9. 图片与响应式图片
9.1 基本图片
html
<img
src="/images/html5-cover.webp"
alt="HTML5 页面结构示意图"
width="1200"
height="675"
>
重要属性:
src:图片地址alt:替代文本width:图片固有宽度height:图片固有高度
同时写明宽高,有利于浏览器提前计算布局空间,减少页面加载时的布局跳动。
9.2 如何编写 alt
信息型图片:
html
<img
src="/images/sales-chart.png"
alt="2025 年销售额从第一季度的 100 万元增长到第四季度的 260 万元"
>
装饰性图片:
html
<img src="/images/decorative-line.svg" alt="">
作为链接的图片,需要描述链接的用途:
html
<a href="/">
<img src="/images/logo.svg" alt="示例商城首页">
</a>
不要写成没有意义的描述:
html
<img src="chart.png" alt="图片">
9.3 延迟加载
html
<img
src="/images/article-cover.webp"
alt="文章封面"
width="1200"
height="675"
loading="lazy"
decoding="async"
>
loading="lazy" 适合首屏之外的图片。
首屏最关键的大图通常不应该延迟加载,否则可能影响最大内容绘制时间。
9.4 figure 与 figcaption
html
<figure>
<img
src="/images/browser-rendering.png"
alt="浏览器从 HTML 构建 DOM 的流程"
width="960"
height="540"
>
<figcaption>
图 1:浏览器解析 HTML 并构建 DOM
</figcaption>
</figure>
9.5 srcset 与 sizes
html
<img
src="/images/product-800.webp"
srcset="
/images/product-480.webp 480w,
/images/product-800.webp 800w,
/images/product-1200.webp 1200w
"
sizes="
(max-width: 600px) 100vw,
(max-width: 1000px) 50vw,
800px
"
alt="蓝色机械键盘"
width="1200"
height="800"
>
其中:
srcset提供多个候选资源480w表示图片固有宽度为 480 像素sizes描述图片在不同视口下预计占据的显示宽度- 浏览器根据视口、设备像素比和网络情况选择资源
9.6 picture 与图片格式切换
html
<picture>
<source
type="image/avif"
srcset="/images/cover.avif"
>
<source
type="image/webp"
srcset="/images/cover.webp"
>
<img
src="/images/cover.jpg"
alt="HTML5 技术文章封面"
width="1200"
height="675"
>
</picture>
浏览器从上到下选择支持的资源,最后的 img 是必要的回退内容。
还可以根据不同屏幕使用不同裁剪:
html
<picture>
<source
media="(max-width: 600px)"
srcset="/images/banner-mobile.webp"
>
<img
src="/images/banner-desktop.webp"
alt="夏季技术大会"
width="1440"
height="600"
>
</picture>
10. 列表与表格
10.1 无序列表
当项目没有严格顺序时使用:
html
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
10.2 有序列表
当项目存在顺序时使用:
html
<ol>
<li>创建 HTML 文件</li>
<li>编写页面结构</li>
<li>添加样式</li>
<li>添加交互逻辑</li>
</ol>
10.3 描述列表
html
<dl>
<dt>HTML</dt>
<dd>用于描述页面结构和内容。</dd>
<dt>CSS</dt>
<dd>用于控制页面样式和布局。</dd>
<dt>JavaScript</dt>
<dd>用于实现页面交互和业务逻辑。</dd>
</dl>
10.4 表格
表格用于展示二维表格数据,不应拿来做页面整体布局。
html
<table>
<caption>前端课程安排</caption>
<thead>
<tr>
<th scope="col">课程</th>
<th scope="col">课时</th>
<th scope="col">难度</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">HTML5</th>
<td>12</td>
<td>入门</td>
</tr>
<tr>
<th scope="row">CSS</th>
<td>20</td>
<td>中等</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row">合计</th>
<td>32</td>
<td>-</td>
</tr>
</tfoot>
</table>
表格元素:
| 元素 | 作用 |
|---|---|
table |
表格容器 |
caption |
表格标题 |
thead |
表头区域 |
tbody |
表格主体 |
tfoot |
表尾区域 |
tr |
行 |
th |
表头单元格 |
td |
普通单元格 |
scope="col" 和 scope="row" 可以帮助辅助技术理解表头与数据之间的关系。
11. HTML5 表单
表单是 HTML 工程中最重要的部分之一,常用于:
- 登录
- 注册
- 搜索
- 下单
- 文件上传
- 用户反馈
- 后台管理
11.1 基本表单
html
<form action="/api/register" method="post">
<div>
<label for="username">用户名</label>
<input
id="username"
name="username"
type="text"
required
>
</div>
<div>
<label for="password">密码</label>
<input
id="password"
name="password"
type="password"
minlength="8"
required
>
</div>
<button type="submit">注册</button>
</form>
11.2 action 与 method
html
<form action="/search" method="get">
GET 通常适合:
- 搜索
- 筛选
- 分页
- 读取数据
- 可以通过 URL 分享的查询
html
<form action="/orders" method="post">
POST 通常适合:
- 创建数据
- 提交敏感或较大内容
- 会改变服务端状态的操作
具体接口语义仍应由后端 API 设计确定。
11.3 name 属性的重要性
html
<input id="email" name="email" type="email">
传统表单提交时,服务端接收的数据字段名来自 name,不是 id。
如果没有 name,该控件通常不会进入表单提交数据。
11.4 label 与表单控件
推荐显式关联:
html
<label for="email">邮箱</label>
<input id="email" name="email" type="email">
for 的值必须与控件的 id 完全一致。
这样可以:
- 让屏幕阅读器读出控件名称
- 点击文字时聚焦或选中控件
- 扩大复选框和单选框的可点击区域
不要仅使用 placeholder 代替标签:
html
<!-- 不推荐 -->
<input type="email" placeholder="请输入邮箱">
用户开始输入后,placeholder 会消失,而且它不一定能可靠承担标签的无障碍职责。
11.5 常用输入类型
html
<input type="text">
<input type="password">
<input type="email">
<input type="url">
<input type="tel">
<input type="number">
<input type="date">
<input type="time">
<input type="datetime-local">
<input type="month">
<input type="week">
<input type="color">
<input type="range">
<input type="checkbox">
<input type="radio">
<input type="file">
<input type="search">
<input type="hidden">
移动端浏览器可能根据 type 显示不同的虚拟键盘。
也可以使用 inputmode 进一步提示输入模式:
html
<label for="verification-code">验证码</label>
<input
id="verification-code"
name="verificationCode"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
pattern="[0-9]{6}"
maxlength="6"
>
inputmode 只是输入提示,不等于数据校验。
11.6 原生校验属性
html
<input
type="text"
name="nickname"
minlength="2"
maxlength="20"
required
>
<input
type="number"
name="age"
min="18"
max="120"
required
>
<input
type="text"
name="code"
pattern="[A-Z]{2}-[0-9]{4}"
title="请输入两个大写字母、短横线和四位数字"
>
常用校验属性:
| 属性 | 作用 |
|---|---|
required |
必填 |
minlength |
最小文本长度 |
maxlength |
最大文本长度 |
min |
最小数值或日期 |
max |
最大数值或日期 |
step |
数值步长 |
pattern |
正则模式 |
浏览器校验只能改善用户体验,不能代替服务端校验。用户可以绕过前端直接调用接口。
11.7 单选框与复选框
单选框使用相同的 name 形成一组:
html
<fieldset>
<legend>请选择配送方式</legend>
<label>
<input
type="radio"
name="delivery"
value="standard"
checked
>
普通配送
</label>
<label>
<input
type="radio"
name="delivery"
value="express"
>
加急配送
</label>
</fieldset>
复选框:
html
<fieldset>
<legend>请选择感兴趣的技术</legend>
<label>
<input type="checkbox" name="skills" value="html">
HTML
</label>
<label>
<input type="checkbox" name="skills" value="css">
CSS
</label>
<label>
<input type="checkbox" name="skills" value="javascript">
JavaScript
</label>
</fieldset>
11.8 下拉选择
html
<label for="city">城市</label>
<select id="city" name="city" required>
<option value="">请选择城市</option>
<optgroup label="华北">
<option value="beijing">北京</option>
<option value="tianjin">天津</option>
</optgroup>
<optgroup label="华东">
<option value="shanghai">上海</option>
<option value="hangzhou">杭州</option>
</optgroup>
</select>
11.9 多行文本
html
<label for="introduction">个人介绍</label>
<textarea
id="introduction"
name="introduction"
rows="6"
maxlength="500"
></textarea>
textarea 的默认值写在开始标签与结束标签之间:
html
<textarea name="content">默认内容</textarea>
11.10 文件上传
html
<form
action="/api/upload"
method="post"
enctype="multipart/form-data"
>
<label for="avatar">上传头像</label>
<input
id="avatar"
name="avatar"
type="file"
accept="image/png,image/jpeg,image/webp"
>
<button type="submit">上传</button>
</form>
accept 只是帮助用户筛选文件,服务端仍必须检查:
- 文件真实类型
- 文件大小
- 文件名
- 文件内容
- 存储位置
- 是否包含恶意代码
11.11 按钮类型
html
<button type="submit">提交</button>
<button type="reset">重置</button>
<button type="button">普通按钮</button>
表单内的 button 应明确写出 type,避免普通按钮意外触发表单提交。
11.12 使用 FormData
html
<form id="profile-form">
<label for="nickname">昵称</label>
<input id="nickname" name="nickname" required>
<button type="submit">保存</button>
</form>
<script>
const form = document.querySelector("#profile-form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const response = await fetch("/api/profile", {
method: "POST",
body: formData
});
if (!response.ok) {
throw new Error(`保存失败:${response.status}`);
}
});
</script>
发送 FormData 时,通常不要手动设置 Content-Type,浏览器会自动生成包含 boundary 的请求头。
12. 音频与视频
12.1 音频
html
<audio controls preload="metadata">
<source src="/media/podcast.ogg" type="audio/ogg">
<source src="/media/podcast.mp3" type="audio/mpeg">
您的浏览器不支持音频播放。
<a href="/media/podcast.mp3">下载音频</a>
</audio>
12.2 视频
html
<video
controls
preload="metadata"
poster="/images/video-poster.webp"
width="1280"
height="720"
>
<source src="/media/course.webm" type="video/webm">
<source src="/media/course.mp4" type="video/mp4">
<track
kind="subtitles"
src="/media/course-zh.vtt"
srclang="zh-CN"
label="简体中文"
default
>
您的浏览器不支持视频播放。
<a href="/media/course.mp4">下载视频</a>
</video>
12.3 常用属性
| 属性 | 作用 |
|---|---|
controls |
显示浏览器播放控件 |
autoplay |
自动播放 |
muted |
静音 |
loop |
循环播放 |
poster |
视频封面 |
preload="none" |
不主动预加载 |
preload="metadata" |
只预加载元数据 |
preload="auto" |
允许浏览器预加载媒体 |
浏览器通常会限制带声音的自动播放,因此不要把关键业务流程建立在自动播放必然成功的假设上。
12.4 字幕与无障碍
视频最好提供:
- 同步字幕
- 音频描述
- 文本稿
- 键盘可操作的播放控件
字幕文件通常使用 WebVTT 格式:
text
WEBVTT
00:00:00.000 --> 00:00:03.000
欢迎学习 HTML5。
00:00:03.500 --> 00:00:07.000
本节介绍语义化标签。
13. 常用交互元素
13.1 details 与 summary
html
<details>
<summary>HTML 和 HTML5 有什么区别?</summary>
<p>
HTML5 是现代 HTML 演进中的重要版本,
今天通常泛指现代 HTML 与相关 Web 能力。
</p>
</details>
默认展开:
html
<details open>
<summary>课程目录</summary>
<p>第一章:HTML 基础</p>
</details>
13.2 对话框 dialog
html
<button id="open-dialog" type="button">
删除账户
</button>
<dialog id="confirm-dialog">
<h2>确认删除账户?</h2>
<p>删除后数据将无法恢复。</p>
<form method="dialog">
<button value="cancel">取消</button>
<button value="confirm">确认删除</button>
</form>
</dialog>
<script>
const openButton = document.querySelector("#open-dialog");
const dialog = document.querySelector("#confirm-dialog");
openButton.addEventListener("click", () => {
dialog.showModal();
});
dialog.addEventListener("close", () => {
if (dialog.returnValue === "confirm") {
console.log("执行删除");
}
});
</script>
showModal() 会以模态形式打开对话框,并帮助浏览器处理焦点和背景交互限制。
复杂对话框仍需测试:
- 初始焦点
- 键盘操作
- Escape 关闭行为
- 关闭后的焦点恢复
- 屏幕阅读器表现
13.3 进度条
html
<label for="upload-progress">上传进度</label>
<progress
id="upload-progress"
value="60"
max="100"
>
60%
</progress>
进度未知时可以省略 value:
html
<progress aria-label="正在加载"></progress>
13.4 度量值
html
<label for="disk-usage">磁盘使用率</label>
<meter
id="disk-usage"
min="0"
max="100"
low="60"
high="85"
optimum="30"
value="72"
>
72%
</meter>
progress表示任务完成进度meter表示已知范围内的测量值
14. Canvas、SVG 与 Template
14.1 Canvas
Canvas 是基于像素的绘图区域,适合:
- 数据可视化
- 游戏
- 图片处理
- 动态图形
- 粒子效果
html
<canvas
id="chart"
width="600"
height="300"
aria-label="季度销售额柱状图"
>
您的浏览器不支持 Canvas。
</canvas>
<script>
const canvas = document.querySelector("#chart");
const context = canvas.getContext("2d");
const values = [120, 180, 150, 240];
const labels = ["Q1", "Q2", "Q3", "Q4"];
context.fillStyle = "#2563eb";
context.font = "16px sans-serif";
values.forEach((value, index) => {
const x = 50 + index * 130;
const height = value;
context.fillRect(x, 260 - height, 80, height);
context.fillText(labels[index], x + 25, 285);
});
</script>
Canvas 内容本身不像普通 HTML 那样具有丰富的可访问语义。重要图表应提供:
- 文本说明
- 数据表格
- 合理的替代内容
- 必要的键盘交互
14.2 SVG
SVG 是矢量图形,适合:
- 图标
- Logo
- 流程图
- 几何图形
- 可缩放插图
html
<svg
viewBox="0 0 200 120"
role="img"
aria-labelledby="svg-title svg-desc"
>
<title id="svg-title">增长趋势图</title>
<desc id="svg-desc">
折线从左下方向右上方增长
</desc>
<polyline
points="10,100 50,80 90,85 130,45 190,20"
fill="none"
stroke="#2563eb"
stroke-width="6"
>
</polyline>
</svg>
Canvas 与 SVG 的区别:
| 对比项 | Canvas | SVG |
|---|---|---|
| 图形类型 | 位图 | 矢量图 |
| DOM 节点 | 图形不是独立 DOM 元素 | 每个图形可以是 DOM 元素 |
| 大量对象 | 通常更适合高频绘制 | 节点过多可能影响性能 |
| 缩放 | 可能失真 | 通常保持清晰 |
| 交互 | 需要自行计算坐标 | 可直接绑定事件 |
| 可访问性 | 需要额外替代内容 | 更容易补充语义 |
14.3 template
template 中的内容不会立即渲染,可以在需要时克隆:
html
<template id="user-card-template">
<article class="user-card">
<h2 class="user-card__name"></h2>
<p class="user-card__email"></p>
</article>
</template>
<div id="user-list"></div>
<script>
const template = document.querySelector("#user-card-template");
const userList = document.querySelector("#user-list");
const users = [
{
name: "张三",
email: "zhangsan@example.com"
},
{
name: "李四",
email: "lisi@example.com"
}
];
const fragment = document.createDocumentFragment();
for (const user of users) {
const card = template.content.cloneNode(true);
card.querySelector(".user-card__name").textContent = user.name;
card.querySelector(".user-card__email").textContent = user.email;
fragment.append(card);
}
userList.append(fragment);
</script>
14.4 自定义元素
自定义元素名称必须包含短横线:
html
<user-card name="张三"></user-card>
定义组件:
javascript
class UserCard extends HTMLElement {
connectedCallback() {
const name = this.getAttribute("name") ?? "未命名用户";
const article = document.createElement("article");
const heading = document.createElement("h2");
heading.textContent = name;
article.append(heading);
this.replaceChildren(article);
}
}
customElements.define("user-card", UserCard);
这属于 Web Components 的一部分。复杂组件还会涉及:
- Shadow DOM
- 插槽
slot - 自定义事件
- 属性变化监听
- 样式隔离
- 服务端渲染兼容性
15. HTML DOM 基础
浏览器读取 HTML 后,会将文档解析为 DOM,即 Document Object Model。
例如:
html
<main>
<h1>HTML5 教程</h1>
<p>欢迎学习 HTML。</p>
</main>
可以理解为:
text
document
└─ html
├─ head
└─ body
└─ main
├─ h1
│ └─ "HTML5 教程"
└─ p
└─ "欢迎学习 HTML。"
JavaScript 可以读取和修改 DOM:
javascript
const heading = document.querySelector("h1");
heading.textContent = "HTML5 工程实践";
heading.classList.add("page-title");
heading.setAttribute("data-status", "updated");
15.1 常用 DOM 查询
javascript
document.querySelector("#app");
document.querySelector(".card");
document.querySelectorAll("[data-action]");
document.getElementById("app");
15.2 创建节点
javascript
const listItem = document.createElement("li");
listItem.textContent = "HTML5";
document.querySelector("ul").append(listItem);
15.3 事件监听
javascript
const button = document.querySelector("#save-button");
button.addEventListener("click", () => {
console.log("保存按钮被点击");
});
15.4 事件委托
当列表项很多或会动态变化时,可以把事件监听放在父元素上:
html
<ul id="task-list">
<li>
学习 HTML
<button type="button" data-action="delete">删除</button>
</li>
</ul>
javascript
const taskList = document.querySelector("#task-list");
taskList.addEventListener("click", (event) => {
const button = event.target.closest("[data-action='delete']");
if (!button) {
return;
}
button.closest("li")?.remove();
});
16. SEO 与页面元数据
16.1 页面标题与描述
html
<title>HTML5 从入门到工程实践 - 前端技术博客</title>
<meta
name="description"
content="系统学习 HTML5 语法、语义化、表单、多媒体、性能、安全和工程化实践。"
>
16.2 规范地址
html
<link
rel="canonical"
href="https://example.com/articles/html5-guide/"
>
当多个 URL 可以访问相同或高度相似的内容时,规范地址可以向搜索引擎表达首选地址。
16.3 Robots
html
<meta name="robots" content="index,follow">
不希望测试页面进入搜索结果时可以使用:
html
<meta name="robots" content="noindex,nofollow">
但敏感内容不能依赖 noindex 保护,必须通过服务端权限控制。
16.4 Open Graph
Open Graph 不是 HTML 核心语义的一部分,但经常用于社交平台分享预览:
html
<meta property="og:type" content="article">
<meta property="og:title" content="HTML5 从入门到工程实践">
<meta
property="og:description"
content="系统学习现代 HTML 页面开发。"
>
<meta
property="og:image"
content="https://example.com/images/html5-cover.webp"
>
<meta
property="og:url"
content="https://example.com/articles/html5-guide/"
>
16.5 结构化数据
可以通过 JSON-LD 提供机器可读的结构化信息:
html
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "HTML5 从入门到工程实践",
"datePublished": "2026-08-23",
"dateModified": "2026-08-23",
"author": {
"@type": "Person",
"name": "示例作者"
}
}
</script>
结构化数据必须与页面真实可见内容一致,不能填写虚假评分、作者或商品信息。
16.6 SEO 的核心仍然是内容
HTML 层面应重点保证:
- 页面有明确标题
- 标题层级合理
- 主要内容直接存在于页面中
- 链接可以正常访问
- 图片具有正确替代文本
- 页面加载速度合理
- 移动端可正常使用
- URL 稳定且语义清晰
- 不制造重复、隐藏或虚假内容
17. Web 无障碍
Web 无障碍通常称为 Accessibility,也常缩写为 A11y。
它的目标是让不同能力、不同设备和不同交互方式的用户都能使用网站。
W3C 指南可参考:
Developing for Web Accessibility
17.1 优先使用原生元素
不推荐:
html
<div class="button" onclick="submitForm()">
提交
</div>
推荐:
html
<button type="submit">提交</button>
原生按钮天然具有:
- 键盘支持
- 焦点能力
- 按钮语义
- 禁用状态
- 表单行为
- 辅助技术支持
17.2 页面语言
html
<html lang="zh-CN">
局部外语内容可以单独标记:
html
<p>
HTML 的全称是
<span lang="en">HyperText Markup Language</span>。
</p>
17.3 跳过重复导航
html
<a class="skip-link" href="#main-content">
跳到主要内容
</a>
<nav aria-label="主导航">
<!-- 导航 -->
</nav>
<main id="main-content">
<!-- 主要内容 -->
</main>
css
.skip-link {
position: absolute;
top: 0;
left: 0;
padding: 8px 12px;
color: #fff;
background: #111827;
transform: translateY(-120%);
}
.skip-link:focus {
transform: translateY(0);
}
17.4 保留可见焦点
不要直接删除焦点轮廓:
css
/* 不推荐 */
button:focus {
outline: none;
}
可以提供更清晰的焦点样式:
css
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
17.5 动态消息
html
<p id="save-status" role="status" aria-live="polite"></p>
javascript
const status = document.querySelector("#save-status");
status.textContent = "保存成功";
对于普通状态更新,role="status" 和 aria-live="polite" 可以让辅助技术在适当时机朗读信息。
不要滥用 aria-live,频繁更新会不断打断用户。
17.6 ARIA 的使用原则
ARIA 用于补充语义,不应该替代已有的原生 HTML。
不推荐:
html
<div role="button" tabindex="0">保存</div>
推荐:
html
<button type="button">保存</button>
一个实用原则是:
如果原生 HTML 元素已经能够表达语义和行为,就优先使用原生元素。
17.7 无障碍检查清单
- 页面是否设置正确语言
- 标题层级是否合理
- 是否只有视觉样式而没有语义
- 表单是否拥有标签
- 错误信息是否能被读取
- 图片是否具有合适的
alt - 纯装饰图片是否使用空
alt - 所有功能是否能通过键盘操作
- 焦点是否清晰可见
- 对话框是否正确管理焦点
- 视频是否提供字幕
- 颜色是否是传递信息的唯一方式
- 动态内容是否提供状态提示
- 放大页面后是否仍然可以使用
18. HTML 安全实践
HTML 本身看起来简单,但错误地插入外部数据会产生严重安全问题。
18.1 防止 XSS
危险示例:
javascript
const comment = getCommentFromServer();
document.querySelector("#comment").innerHTML = comment;
如果 comment 包含:
html
<img src="invalid" onerror="alert('XSS')">
它可能被浏览器作为 HTML 执行。
只需要显示文本时,应使用:
javascript
document.querySelector("#comment").textContent = comment;
如果业务必须渲染富文本,应使用经过安全评估的 HTML 清理方案,并明确允许哪些标签和属性。
18.2 不要拼接不可信 HTML
危险示例:
javascript
container.innerHTML = `
<a href="${userUrl}">${userName}</a>
`;
更安全的做法:
javascript
const link = document.createElement("a");
link.textContent = userName;
const url = new URL(userUrl, window.location.origin);
if (!["http:", "https:"].includes(url.protocol)) {
throw new Error("不支持的链接协议");
}
link.href = url.href;
container.replaceChildren(link);
需要根据数据所处的上下文分别处理:
- HTML 文本上下文
- HTML 属性上下文
- URL 上下文
- CSS 上下文
- JavaScript 上下文
一种转义方式不能安全覆盖所有上下文。
18.3 内容安全策略
内容安全策略通常应通过 HTTP 响应头配置:
http
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
CSP 可以降低部分内容注入攻击的影响,但它不是输入校验和输出编码的替代品。
18.4 iframe 沙箱
html
<iframe
src="https://example.com/embed/"
title="第三方内容"
sandbox="allow-scripts"
loading="lazy"
></iframe>
sandbox 可以限制嵌入页面的能力。不要为了让第三方页面正常运行,就不加分析地开放所有权限。
尤其应谨慎组合:
text
allow-scripts
allow-same-origin
具体权限必须根据嵌入内容的可信度和功能需求设计。
18.5 表单安全
前端表单不能代替服务端安全控制。
服务端必须处理:
- 身份认证
- 权限校验
- CSRF 防护
- 输入校验
- 输出编码
- 文件类型和大小校验
- 接口频率限制
- 重复提交和幂等性
- 审计日志
18.6 不在 HTML 中存放秘密
下面的信息不应该直接写入前端 HTML 或 JavaScript:
- 数据库密码
- 私钥
- 管理员 Token
- 云服务密钥
- 可绕过权限的内部接口凭据
发送到浏览器的内容都应视为用户可以查看和修改。
19. HTML 页面性能优化
19.1 脚本加载方式
普通脚本
html
<script src="/scripts/app.js"></script>
普通经典脚本可能阻塞 HTML 解析,直到脚本下载并执行完成。
defer
html
<script defer src="/scripts/app.js"></script>
特点:
- 与 HTML 解析并行下载
- 在文档解析完成后执行
- 多个
defer脚本通常保持文档顺序
async
html
<script async src="/scripts/analytics.js"></script>
特点:
- 与 HTML 解析并行下载
- 下载完成后尽快执行
- 多个脚本之间不保证执行顺序
适合相对独立的脚本,例如部分统计脚本。
模块脚本
html
<script type="module" src="/scripts/app.js"></script>
模块脚本默认具有类似延迟执行的行为,并支持 import 和 export。
19.2 图片性能
建议:
- 使用合适的图片尺寸
- 优先评估 WebP 或 AVIF
- 为图片写明
width和height - 首屏外图片使用
loading="lazy" - 通过
srcset提供多尺寸资源 - 不要把大图只靠 CSS 缩小
- 首屏关键图片不要随意延迟加载
- 压缩图片但保留可接受质量
19.3 资源提示
预加载关键资源:
html
<link
rel="preload"
href="/fonts/site-font.woff2"
as="font"
type="font/woff2"
crossorigin
>
预连接第三方源:
html
<link rel="preconnect" href="https://cdn.example.com">
资源提示不是越多越好。错误预加载会抢占带宽,反而拖慢真正重要的资源。
19.4 减少 DOM 复杂度
避免无意义的深层嵌套:
html
<!-- 不推荐 -->
<div>
<div>
<div>
<div>
<p>内容</p>
</div>
</div>
</div>
</div>
可以简化为:
html
<section>
<p>内容</p>
</section>
复杂 DOM 会增加:
- HTML 解析成本
- CSS 匹配成本
- 布局与绘制成本
- JavaScript 查询和更新成本
- 维护难度
19.5 减少布局偏移
图片和嵌入内容应预留空间:
html
<img
src="/images/banner.webp"
alt="活动横幅"
width="1440"
height="600"
>
不要等 JavaScript 加载后才突然插入没有预留区域的大块内容。
19.6 性能需要测量
常见工具:
- Chrome DevTools Network
- Chrome DevTools Performance
- Lighthouse
- PageSpeed Insights
- WebPageTest
- 浏览器 Performance API
重点指标通常包括:
- LCP:最大内容绘制
- INP:交互响应
- CLS:累计布局偏移
优化前应先测量,找到真正的瓶颈,再进行针对性修改。
20. 渐进增强与兼容性
渐进增强的核心思想是:
- 先提供可以正常工作的基础内容
- 再添加样式和高级交互
- 高级能力不可用时,核心功能仍可完成
20.1 保留基础表单能力
html
<form action="/search" method="get">
<label for="keyword">关键词</label>
<input id="keyword" name="keyword" type="search">
<button type="submit">搜索</button>
</form>
即使 JavaScript 加载失败,用户仍可以提交搜索。
JavaScript 可以在此基础上增加:
- 即时搜索
- 加载状态
- 自动完成
- 无刷新更新
20.2 功能检测
不要仅根据浏览器名称判断功能:
javascript
if ("showModal" in HTMLDialogElement.prototype) {
console.log("支持原生 dialog");
}
对于 CSS,可以使用:
css
@supports (display: grid) {
.layout {
display: grid;
grid-template-columns: 2fr 1fr;
}
}
20.3 回退内容
html
<video controls>
<source src="/video/course.mp4" type="video/mp4">
您的浏览器无法播放视频,
<a href="/video/course.mp4">请下载视频</a>。
</video>
21. 完整工程项目实战
下面实现一个简单的"技术文章收藏夹",包含:
- 语义化页面结构
- 表单与原生校验
templatedialog- 本地存储
- 动态状态提示
- 安全 DOM 操作
- 响应式布局
- 基本无障碍支持
21.1 项目目录
text
html5-reading-list/
├─ index.html
├─ styles.css
└─ app.js
21.2 index.html
html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<meta
name="description"
content="使用 HTML5 构建的技术文章收藏夹"
>
<title>技术文章收藏夹</title>
<link rel="stylesheet" href="./styles.css">
<script type="module" src="./app.js"></script>
</head>
<body>
<a class="skip-link" href="#main-content">
跳到主要内容
</a>
<header class="site-header">
<div class="container">
<h1>技术文章收藏夹</h1>
<p>保存值得阅读的技术文章</p>
</div>
</header>
<main id="main-content" class="container">
<section aria-labelledby="add-heading">
<h2 id="add-heading">添加文章</h2>
<form id="article-form">
<div class="form-field">
<label for="article-title">文章标题</label>
<input
id="article-title"
name="title"
type="text"
minlength="2"
maxlength="100"
autocomplete="off"
required
>
</div>
<div class="form-field">
<label for="article-url">文章地址</label>
<input
id="article-url"
name="url"
type="url"
placeholder="https://example.com/article"
required
>
</div>
<div class="form-field">
<label for="article-category">分类</label>
<select
id="article-category"
name="category"
required
>
<option value="">请选择分类</option>
<option value="HTML">HTML</option>
<option value="CSS">CSS</option>
<option value="JavaScript">JavaScript</option>
<option value="工程化">工程化</option>
<option value="其他">其他</option>
</select>
</div>
<button type="submit">添加文章</button>
</form>
<p
id="status-message"
class="status"
role="status"
aria-live="polite"
></p>
</section>
<section aria-labelledby="list-heading">
<div class="section-heading">
<h2 id="list-heading">收藏列表</h2>
<button
id="open-clear-dialog"
class="danger-button"
type="button"
>
清空全部
</button>
</div>
<p id="empty-message">
暂无收藏文章,请先添加一篇文章。
</p>
<ul id="article-list" class="article-list"></ul>
</section>
</main>
<template id="article-template">
<li class="article-item">
<article>
<header>
<h3 class="article-title"></h3>
<span class="article-category"></span>
</header>
<p>
<a
class="article-link"
target="_blank"
rel="noopener noreferrer"
></a>
</p>
<footer>
<time class="article-time"></time>
<button
class="delete-button"
type="button"
data-action="delete"
>
删除
</button>
</footer>
</article>
</li>
</template>
<dialog id="clear-dialog">
<h2>确认清空全部收藏?</h2>
<p>该操作无法撤销。</p>
<form method="dialog">
<button value="cancel">取消</button>
<button class="danger-button" value="confirm">
确认清空
</button>
</form>
</dialog>
<footer class="site-footer">
<div class="container">
<p>
HTML5 工程实践示例 ·
<time datetime="2026">2026</time>
</p>
</div>
</footer>
</body>
</html>
21.3 styles.css
css
:root {
color-scheme: light;
font-family:
Inter,
"PingFang SC",
"Microsoft YaHei",
sans-serif;
line-height: 1.6;
color: #1f2937;
background: #f3f4f6;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
margin: 0;
}
button,
input,
select {
font: inherit;
}
button {
padding: 10px 18px;
color: #fff;
cursor: pointer;
background: #2563eb;
border: 0;
border-radius: 8px;
}
button:hover {
background: #1d4ed8;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
.container {
width: min(960px, calc(100% - 32px));
margin-inline: auto;
}
.skip-link {
position: fixed;
z-index: 100;
top: 8px;
left: 8px;
padding: 8px 12px;
color: #fff;
background: #111827;
border-radius: 6px;
transform: translateY(-150%);
}
.skip-link:focus {
transform: translateY(0);
}
.site-header {
padding: 48px 0;
color: #fff;
background: #1d4ed8;
}
.site-header h1,
.site-header p {
margin: 0;
}
main {
display: grid;
gap: 24px;
padding-block: 32px;
}
section {
padding: 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 6px 20px rgb(15 23 42 / 8%);
}
.form-field {
display: grid;
gap: 6px;
margin-bottom: 16px;
}
.form-field label {
font-weight: 600;
}
.form-field input,
.form-field select {
width: 100%;
padding: 10px 12px;
border: 1px solid #9ca3af;
border-radius: 8px;
}
.status {
min-height: 1.6em;
margin-bottom: 0;
color: #166534;
}
.section-heading {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
}
.article-list {
display: grid;
gap: 16px;
padding: 0;
list-style: none;
}
.article-item {
padding: 16px;
border: 1px solid #d1d5db;
border-radius: 10px;
}
.article-item header,
.article-item footer {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.article-title {
margin: 0;
}
.article-category {
padding: 2px 8px;
color: #1e40af;
background: #dbeafe;
border-radius: 999px;
}
.article-link {
overflow-wrap: anywhere;
}
.article-time {
color: #6b7280;
}
.danger-button,
.delete-button {
background: #b91c1c;
}
.danger-button:hover,
.delete-button:hover {
background: #991b1b;
}
dialog {
width: min(420px, calc(100% - 32px));
padding: 24px;
border: 0;
border-radius: 12px;
}
dialog::backdrop {
background: rgb(15 23 42 / 60%);
}
dialog form {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.site-footer {
padding: 24px 0;
color: #d1d5db;
text-align: center;
background: #111827;
}
@media (max-width: 600px) {
.section-heading,
.article-item header,
.article-item footer {
align-items: stretch;
flex-direction: column;
}
}
21.4 app.js
javascript
const STORAGE_KEY = "html5-reading-list";
const form = document.querySelector("#article-form");
const titleInput = document.querySelector("#article-title");
const list = document.querySelector("#article-list");
const template = document.querySelector("#article-template");
const emptyMessage = document.querySelector("#empty-message");
const statusMessage = document.querySelector("#status-message");
const clearDialog = document.querySelector("#clear-dialog");
const openClearDialogButton = document.querySelector(
"#open-clear-dialog"
);
let articles = loadArticles();
renderArticles();
form.addEventListener("submit", (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const title = String(formData.get("title") ?? "").trim();
const rawUrl = String(formData.get("url") ?? "").trim();
const category = String(formData.get("category") ?? "").trim();
let url;
try {
url = new URL(rawUrl);
} catch {
showStatus("请输入有效的文章地址。");
return;
}
if (!["http:", "https:"].includes(url.protocol)) {
showStatus("文章地址只支持 HTTP 或 HTTPS。");
return;
}
articles.unshift({
id: createId(),
title,
url: url.href,
category,
createdAt: new Date().toISOString()
});
saveArticles();
renderArticles();
form.reset();
titleInput.focus();
showStatus(`已添加文章:${title}`);
});
list.addEventListener("click", (event) => {
const deleteButton = event.target.closest(
"[data-action='delete']"
);
if (!deleteButton) {
return;
}
const item = deleteButton.closest("[data-id]");
if (!item) {
return;
}
const articleId = item.dataset.id;
const article = articles.find(({ id }) => id === articleId);
articles = articles.filter(({ id }) => id !== articleId);
saveArticles();
renderArticles();
showStatus(
article
? `已删除文章:${article.title}`
: "文章已删除"
);
});
openClearDialogButton.addEventListener("click", () => {
if (articles.length === 0) {
showStatus("当前没有需要清空的文章。");
return;
}
clearDialog.showModal();
});
clearDialog.addEventListener("close", () => {
if (clearDialog.returnValue !== "confirm") {
return;
}
articles = [];
saveArticles();
renderArticles();
showStatus("已清空全部收藏文章。");
});
function renderArticles() {
const fragment = document.createDocumentFragment();
for (const article of articles) {
const item = template.content.firstElementChild.cloneNode(true);
item.dataset.id = article.id;
const title = item.querySelector(".article-title");
const category = item.querySelector(".article-category");
const link = item.querySelector(".article-link");
const time = item.querySelector(".article-time");
const deleteButton = item.querySelector(".delete-button");
title.textContent = article.title;
category.textContent = article.category;
link.href = article.url;
link.textContent = article.url;
time.dateTime = article.createdAt;
time.textContent = formatDate(article.createdAt);
deleteButton.setAttribute(
"aria-label",
`删除文章:${article.title}`
);
fragment.append(item);
}
list.replaceChildren(fragment);
const isEmpty = articles.length === 0;
emptyMessage.hidden = !isEmpty;
openClearDialogButton.disabled = isEmpty;
}
function loadArticles() {
try {
const value = localStorage.getItem(STORAGE_KEY);
if (!value) {
return [];
}
const parsedValue = JSON.parse(value);
return Array.isArray(parsedValue)
? parsedValue.filter(isValidArticle)
: [];
} catch (error) {
console.error("读取本地数据失败", error);
return [];
}
}
function saveArticles() {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(articles)
);
} catch (error) {
console.error("保存本地数据失败", error);
showStatus("数据保存失败,请检查浏览器存储设置。");
}
}
function isValidArticle(value) {
return (
value &&
typeof value.id === "string" &&
typeof value.title === "string" &&
typeof value.url === "string" &&
typeof value.category === "string" &&
typeof value.createdAt === "string"
);
}
function createId() {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function formatDate(value) {
const date = new Date(value);
return new Intl.DateTimeFormat("zh-CN", {
dateStyle: "medium",
timeStyle: "short"
}).format(date);
}
function showStatus(message) {
statusMessage.textContent = message;
}
21.5 运行项目
最简单的方式是直接打开 index.html。
为了更接近真实开发环境,可以在项目目录启动本地服务器:
bash
npx serve .
也可以使用 Vite:
bash
npm create vite@latest
对于已有静态目录,可以安装 Vite:
bash
npm init -y
npm install --save-dev vite
npx vite
不要直接双击 HTML 来测试所有模块化功能,因为 file:// 环境与真实 HTTP 环境存在差异。
22. HTML 工程化工具
22.1 代码格式化
安装 Prettier:
bash
npm install --save-dev prettier
格式化文件:
bash
npx prettier --write "**/*.{html,css,js,json,md}"
检查格式但不修改:
bash
npx prettier --check "**/*.{html,css,js,json,md}"
.prettierignore 示例:
text
dist
coverage
node_modules
22.2 HTML 静态检查
安装 HTML Validate:
bash
npm install --save-dev html-validate
执行检查:
bash
npx html-validate "src/**/*.html" "*.html"
.htmlvalidate.json 示例:
json
{
"extends": ["html-validate:recommended"]
}
22.3 package.json 脚本
json
{
"name": "html5-reading-list",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint:html": "html-validate \"*.html\""
},
"devDependencies": {
"html-validate": "latest",
"prettier": "latest",
"vite": "latest"
}
}
实际团队项目应提交锁文件,通过锁文件固定安装结果,而不是每次安装时任意漂移依赖版本。
22.4 编辑器配置
.editorconfig:
ini
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
22.5 推荐的 CI 检查
持续集成中可以执行:
bash
npm ci
npm run format:check
npm run lint:html
npm run build
还可以继续增加:
- JavaScript 静态检查
- CSS 静态检查
- 单元测试
- 端到端测试
- 无障碍自动检查
- 链接有效性检查
- 构建产物体积检查
23. HTML 与后端接口协作
23.1 前后端共同关注的数据契约
前端表单:
html
<form id="register-form">
<label for="register-email">邮箱</label>
<input
id="register-email"
name="email"
type="email"
required
>
<label for="register-password">密码</label>
<input
id="register-password"
name="password"
type="password"
minlength="8"
required
>
<button type="submit">注册</button>
</form>
发送 JSON:
javascript
const form = document.querySelector("#register-form");
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const payload = {
email: String(formData.get("email") ?? ""),
password: String(formData.get("password") ?? "")
};
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const result = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(
result?.message ?? `注册失败:${response.status}`
);
}
console.log("注册成功", result);
});
前后端需要明确:
- 字段名称
- 字段类型
- 是否必填
- 长度和范围
- 时间格式
- 空值含义
- 错误码
- 错误信息格式
- 身份认证方式
- 幂等规则
- 文件上传限制
23.2 正确展示后端错误
html
<p
id="form-error"
role="alert"
hidden
></p>
javascript
function showFormError(message) {
const errorElement = document.querySelector("#form-error");
errorElement.textContent = message;
errorElement.hidden = false;
}
不要将后端返回内容直接写入 innerHTML。
23.3 防止重复提交
javascript
const submitButton = form.querySelector(
"button[type='submit']"
);
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (submitButton.disabled) {
return;
}
submitButton.disabled = true;
submitButton.textContent = "提交中......";
try {
await submitData();
} finally {
submitButton.disabled = false;
submitButton.textContent = "提交";
}
});
前端禁用按钮只能改善体验。真正的重复请求和资金类操作仍需由服务端通过幂等机制处理。
24. 测试、验证与质量检查
24.1 HTML 语法验证
可以使用 W3C Markup Validation Service:
它可以发现:
- 标签嵌套错误
- 无效属性
- 重复
id - 元素使用位置错误
- 缺少必要属性
24.2 浏览器开发者工具
重点检查:
Elements
- DOM 是否符合预期
- 标签是否被浏览器自动修正
- ARIA 属性是否正确
- 元素尺寸和样式是否正确
Console
- JavaScript 是否报错
- 资源是否存在安全警告
- 是否存在废弃 API 提示
Network
- 是否有 404
- 文件是否过大
- 缓存是否生效
- Content-Type 是否正确
- 请求是否重复
- 图片是否加载了不必要的大尺寸版本
Lighthouse
检查:
- Performance
- Accessibility
- Best Practices
- SEO
自动化评分不是最终质量结论,但适合作为问题发现工具。
24.3 键盘测试
只使用键盘完成以下操作:
- 使用 Tab 正向移动
- 使用 Shift + Tab 反向移动
- 使用 Enter 激活链接
- 使用 Space 或 Enter 激活按钮
- 操作单选框、复选框和下拉框
- 打开和关闭对话框
- 确认焦点不会消失
- 确认没有键盘陷阱
24.4 响应式测试
至少检查:
- 320px 左右的小屏
- 常见手机宽度
- 平板宽度
- 笔记本宽度
- 大屏显示器
- 横屏模式
- 页面放大到 200%
- 较长中文和英文内容
- 系统大字体设置
24.5 自动化端到端测试
安装 Playwright:
bash
npm install --save-dev @playwright/test
npx playwright install
示例:
javascript
import { test, expect } from "@playwright/test";
test("可以添加一篇文章", async ({ page }) => {
await page.goto("http://localhost:5173");
await page.getByLabel("文章标题").fill("HTML5 教程");
await page
.getByLabel("文章地址")
.fill("https://example.com/html5");
await page.getByLabel("分类").selectOption("HTML");
await page.getByRole("button", {
name: "添加文章"
}).click();
await expect(
page.getByRole("heading", {
name: "HTML5 教程"
})
).toBeVisible();
await expect(
page.getByRole("status")
).toContainText("已添加文章");
});
24.6 上线前检查清单
- HTML 文档声明正确
- 页面语言正确
- 字符编码为 UTF-8
- viewport 配置正确
- 每个页面拥有合适标题
- 页面描述准确
- 标题层级合理
-
id没有重复 - 图片拥有合适的
alt - 图片宽高明确
- 表单控件拥有标签
- 按钮类型明确
- 键盘可以完成主要操作
- 焦点样式清晰
- 动态消息可以被感知
- 外部输入没有直接写入
innerHTML - 没有把密钥写入前端
- 页面不存在 404 资源
- 移动端布局正常
- 构建和自动化测试通过
- HTTP 安全头已配置
- 生产环境使用 HTTPS
25. 构建与部署
25.1 构建
使用 Vite:
bash
npm run build
默认会生成类似目录:
text
dist/
├─ index.html
└─ assets/
├─ index-a1b2c3.js
└─ index-d4e5f6.css
部署时应发布构建后的目录,而不是将开发依赖一起上传。
25.2 部署路径
如果网站部署在:
text
https://example.com/
资源根路径可以是:
html
<script src="/assets/app.js"></script>
如果部署在子目录:
text
https://example.com/docs/
则必须确认构建工具的基础路径配置,否则 /assets/app.js 会请求:
text
https://example.com/assets/app.js
而不是:
text
https://example.com/docs/assets/app.js
25.3 MIME 类型
服务端应返回正确的 Content-Type,例如:
text
.html text/html
.css text/css
.js text/javascript
.json application/json
.svg image/svg+xml
.webp image/webp
.woff2 font/woff2
错误的 MIME 类型可能导致模块脚本、样式表或字体无法加载。
25.4 缓存策略
带内容哈希的静态资源:
text
app.a1b2c3.js
styles.d4e5f6.css
可以设置较长缓存:
http
Cache-Control: public, max-age=31536000, immutable
HTML 文件通常需要更谨慎的缓存策略,因为它负责引用最新资源:
http
Cache-Control: no-cache
这里的 no-cache 不等于完全不存储,而是要求使用前向服务器验证资源是否仍然有效。
25.5 压缩
服务端应针对文本资源启用 Brotli 或 Gzip:
- HTML
- CSS
- JavaScript
- JSON
- SVG
已经压缩的图片和视频通常不需要再次进行通用压缩。
25.6 常用安全响应头
根据项目需求评估:
http
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
安全头应该在测试环境验证,尤其是 CSP。配置过严可能阻止合法资源加载,配置过松则失去保护价值。
26. 常见错误与反例
26.1 使用 div 实现所有元素
不推荐:
html
<div class="title">文章标题</div>
<div class="button">提交</div>
推荐:
html
<h1>文章标题</h1>
<button type="submit">提交</button>
26.2 根据默认样式选择标签
不推荐:
html
<h4>我只是想要小一点的标题</h4>
应该根据内容层级选择标题,再用 CSS 控制大小。
26.3 使用 br 制造间距
不推荐:
html
<p>第一段</p>
<br>
<br>
<br>
<p>第二段</p>
推荐:
css
p {
margin-block: 1em;
}
26.4 图片缺少替代文本
不推荐:
html
<img src="payment-result.png">
推荐:
html
<img
src="payment-result.png"
alt="支付成功,订单号为 202608230001"
>
26.5 使用 placeholder 代替标签
不推荐:
html
<input type="email" placeholder="请输入邮箱">
推荐:
html
<label for="email">邮箱</label>
<input
id="email"
name="email"
type="email"
placeholder="name@example.com"
>
26.6 表单按钮没有声明类型
不推荐:
html
<form>
<button id="preview">预览</button>
</form>
浏览器可能把它当成提交按钮。
推荐:
html
<button id="preview" type="button">预览</button>
26.7 信任前端校验
错误认识:
HTML 已经设置
required,后端就不需要验证。
前端校验可以被绕过,服务端必须重新验证全部外部输入。
26.8 直接使用 innerHTML
不推荐:
javascript
result.innerHTML = userInput;
仅显示文本时使用:
javascript
result.textContent = userInput;
26.9 使用表格完成页面布局
不推荐:
html
<table>
<tr>
<td>左侧导航</td>
<td>主要内容</td>
</tr>
</table>
页面布局应使用语义化结构和 CSS:
html
<div class="layout">
<aside>左侧导航</aside>
<main>主要内容</main>
</div>
26.10 认为浏览器能显示就代表代码正确
浏览器具有强大的错误恢复能力,会自动修正部分无效 HTML。
例如:
html
<p>
段落内容
<div>错误嵌套的块级区域</div>
</p>
浏览器可能自动关闭 p,导致最终 DOM 与源代码看起来不一致。
因此需要结合:
- HTML 验证工具
- 浏览器 Elements 面板
- 自动化检查
27. 常见面试题
27.1 HTML5 的文档声明是什么?
html
<!doctype html>
它用于让浏览器按照现代标准模式解析页面。
27.2 什么是语义化 HTML?
语义化 HTML 是使用含义准确的元素描述内容结构,例如:
- 使用
nav表示导航 - 使用
main表示主要内容 - 使用
article表示可独立分发的内容 - 使用
button表示按钮 - 使用
table表示二维表格数据
语义化有利于可维护性、无障碍、搜索引擎理解和浏览器功能。
27.3 section、article 和 div 有什么区别?
article:相对独立、可单独分发的内容section:按主题划分的内容区域div:没有额外语义的通用分组容器
27.4 id 和 class 有什么区别?
id应在当前文档中保持唯一class可以被多个元素复用id常用于锚点、标签关联和精确查询class常用于样式和组件状态
27.5 async 与 defer 有什么区别?
defer:
- 并行下载
- HTML 解析完成后执行
- 通常保持脚本顺序
async:
- 并行下载
- 下载完成后尽快执行
- 不保证脚本顺序
模块脚本默认具有类似延迟执行的行为。
27.6 为什么图片要写 width 和 height?
它可以让浏览器在图片加载前计算宽高比例并预留空间,从而减少页面布局跳动。
27.7 alt="" 和不写 alt 有什么区别?
html
<img src="line.svg" alt="">
空 alt 明确表示该图片是装饰性的,辅助技术通常可以忽略它。
不写 alt 可能让辅助技术尝试读取文件名或以其他不理想方式处理图片。
27.8 为什么前端校验不能代替后端校验?
用户可以:
- 禁用 JavaScript
- 修改 DOM
- 修改网络请求
- 直接调用后端接口
- 编写脚本绕过页面
因此服务端必须将所有客户端数据视为不可信输入。
27.9 为什么不推荐使用 innerHTML 插入用户数据?
因为用户数据可能被解析为 HTML,进而注入脚本、事件属性或危险链接,形成 XSS。
只显示文本时应使用 textContent。
27.10 localStorage 有什么特点?
- 以字符串形式存储数据
- 数据通常跨页面刷新保留
- 同源页面共享
- API 是同步的
- 容量有限
- 用户可以修改或清除
- 不适合保存密码、Token 明文或可信业务状态
服务端权限判断绝不能依赖 localStorage 中的数据。
28. 学习路线
第一阶段:掌握基础语法
学习:
- HTML 文档结构
- 元素与属性
- 标题和段落
- 链接和图片
- 列表和表格
- 基础表单
目标:能够独立编写一个结构正确的静态页面。
第二阶段:理解语义化
学习:
headernavmainarticlesectionasidefooter- 标题层级
- 内容模型
目标:页面不再是由大量无含义的 div 堆叠而成。
第三阶段:学习表单与多媒体
学习:
- 各种
input类型 - 原生校验
labelfieldset- 文件上传
audiovideotrack
目标:能够实现可用、可访问的业务表单和媒体页面。
第四阶段:掌握 DOM 与 JavaScript 协作
学习:
- DOM 查询
- DOM 创建和更新
- 事件监听
- 事件委托
templateFormData- Fetch API
- 本地存储
目标:完成一个不依赖框架的小型交互项目。
第五阶段:学习工程质量
学习:
- Web 无障碍
- SEO
- HTML 安全
- 性能优化
- 响应式设计
- HTML 验证
- 自动格式化
- 静态检查
- 自动化测试
目标:页面不仅"能显示",还要可维护、可测试、可访问和可上线。
第六阶段:进入框架和完整工程
在掌握 HTML 基础后,再学习:
- CSS 布局与响应式设计
- JavaScript 和 TypeScript
- Vue、React 或其他框架
- SSR 与 SSG
- Web Components
- 构建工具
- 前后端接口设计
- 自动化测试
- CI/CD
框架会变化,但语义、表单、可访问性、安全和浏览器解析规则仍然是底层基础。
29. 总结
HTML5 的学习不能停留在"记标签"。
真正的工程能力包括:
- 使用正确的文档结构
- 根据内容含义选择元素
- 编写结构清晰的语义化页面
- 正确处理链接、图片、表格和表单
- 使用原生能力实现基础交互
- 让页面支持键盘和辅助技术
- 安全地处理用户和服务端数据
- 优化资源加载与页面性能
- 使用工具验证 HTML 质量
- 通过构建、测试和部署形成完整工程流程
学习 HTML 的一个重要原则是:
先用 HTML 表达正确的结构和语义,再使用 CSS 控制表现,最后使用 JavaScript 增强交互。
当 JavaScript 加载失败、CSS 没有生效,或者用户只能使用键盘和屏幕阅读器时,页面仍然应该尽可能保留清晰的内容和基础功能。
这才是现代 HTML 工程实践的真正价值。