HTML入门

前言

  • 这一篇是 HTML 的入门笔记:文档骨架、语义化标签、字符实体、URL 与超链接、图片音视频、表格、iframe,最后是表单(前后端交互的入口,也是本篇重点)。
  • 笔记里的技术结论不是照抄来的:默认的字号、边距、b 与 strong 的差别、type 属性还剩哪些能用、表单到底提交了什么,全部在本机 Chrome 里实测过;表单的报文是用一个本地回显服务(Python 写的 /echo)把浏览器真实发出去的请求打出来核对的。

先分清三件事

  • HTML(HyperText Markup Language,超文本标记语言):规定内容的结构与语义——这是一段话、一张图、一个按钮,还是表格里的一格。
  • CSS:规定呈现——颜色、字号、位置、响应式布局。
  • JavaScript:规定行为——点击之后发生什么、数据从哪来。
  • 三者各司其职是 modern web 的基本共识。早期 HTML 里那些 bgcolor、align、width、<font>、<center> 之类的「表现型」写法都被归为遗留特性:浏览器至今还认(实测 Chrome 里 <font color="red" size="5"> 仍然渲染成 24px 红色,<center> 仍然居中),但新代码一律用 CSS。

三种角色之间的关系

1
2
浏览器(客户端)  --请求(GET/POST)-->  服务器
浏览器(客户端) <--响应(HTML/CSS/JS/图片)-- 服务器

浏览器拿到 HTML 后:解析成 DOM 树 → 应用 CSS 得到渲染树 → 布局 → 绘制;遇到 <script> 且没有 defer/async 就会暂停解析去执行它。这也是为什么脚本一般放在 </body> 之前或用 defer。

C/S 与 B/S

维度 C/S(Client/Server) B/S(Browser/Server)
客户端 需要安装专门程序 只要一个浏览器
升级维护 两端都要发版 只改服务器,用户刷新即可
能力 能直接用本地硬件、系统 API,交互天花板高 受浏览器沙箱限制(现在有 WASM、各类 Web API,差距在缩小)
跨平台 通常要为每个平台各做一份 天然跨平台,但要兼容不同浏览器
典型场景 大型客户端软件、游戏、离线工具 网站、管理后台、Web 应用

B/S 可以看成 C/S 的一种特殊实现:客户端换成了浏览器。

一个页面的骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>                                <!-- 声明这是 HTML5 文档,必须放在第一行 -->
<html lang="zh-cn"> <!-- 根元素,lang 帮搜索引擎和读屏软件确定语言 -->

<head>
<meta charset="utf-8"> <!-- 字符编码,必须是 head 里靠前的位置 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>页面标题</title> <!-- 唯一会显示在浏览器标签页上的内容 -->
<link rel="stylesheet" href="style.css">
</head>

<body>
<h1>这是一个标题</h1>
<p>这是一个段落。</p>
</body>

</html>
  • <!DOCTYPE html>:它不是标签,是一条「doctype 声明」,用来让浏览器进入标准模式。HTML5 把它简化成这一行,早期的 XHTML/HTML 4.01 那一长串 DTD(<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ...>)全部作废,现在统一只写 <!DOCTYPE html>。
    • 实测:有这一行时 document.compatMode 是 CSS1Compat(标准模式);写错或漏写会退回 BackCompat(怪异模式),盒模型、滚动条位置都可能变。
  • <html>:整篇文档的根。
  • <head>:放「说明性」内容,不显示在页面上。常见子元素(实测本页 head 的直接子元素就是 META、TITLE):
    • <meta charset="utf-8">:声明编码,中文页面的标配。
    • <meta name="viewport" ...>:移动端布局视口。
    • <title>:文档标题,必需且唯一。
    • <meta name="description" content="..."> / <meta name="keywords" content="...">:给搜索引擎的摘要与关键词(关键词的 SEO 价值早已归零,描述仍会被抓取)。
    • <meta http-equiv="refresh" content="5; url=https://example.com/next">:延时刷新/跳转。能用,但体验差,正常做法是服务端 302 或前端路由。
    • <link rel="stylesheet" href="...">:引外部 CSS。
    • <base href="...">:改变页面内所有相对链接的基准,很少用且容易挖坑(一旦设置,#锚点 也会被它影响)。
    • <script>、<style>、<template> 也可以出现在 head 里。
    • 注意:<meta http-equiv="content-type" content="text/html; charset=UTF-8"> 这种老写法在 HTML5 里已不被支持,直接写 <meta charset="utf-8">。
  • <body>:所有可见内容。
  • 注释:<!-- 这是注释 -->。注释不能嵌套,也不能写在标签的尖括号里面(见前言的 iframe 例子)。

语义化的分区标签

div 什么都能装,但「什么都没说」。HTML5 给了一组自带语义的分区标签,屏幕阅读器和搜索引擎会据此理解页面结构:

标签 语义 用法要点
<header> 页眉/区块开头 可多次出现(每个 <article> 里也能有自己的 header)
<nav> 主导航区块 放一组主要链接
<main> 页面主内容 一个页面只应有一个可见的 <main>,不要嵌在 article/aside/footer 里
<section> 有主题的一个分节 规范建议每个 section 有自己的标题(h1~h6)
<article> 能独立分发的一块内容 博文、评论、商品卡片
<aside> 与主内容相关但次要 侧边栏、附注
<footer> 页脚/区块结尾 版权、联系方式
<h1>~<h6> 标题层级 按层级顺序写,不要跳级;一页通常只有一个 <h1>
<figure> + <figcaption> 图片及其说明 图示与文字绑定
<details> + <summary> 可折叠的「更多信息」 点击 summary 展开,纯 HTML 就能实现折叠

说明:<main>、<section>、<article> 这些标签在浏览器里没有默认外观(实测 constructor.name 就是 HTMLElement),它们只提供语义。真正让页面「长什么样」的仍然是 CSS。

文本标签:外观相似,语义不同

先看一份实测表(Chrome,正文默认 16px,取的是计算后的样式):

标签 实测外观 语义 备注
<b> font-weight: 700 只管视觉加粗(关键词、产品名) 与 <strong> 外观完全一样
<strong> font-weight: 700 内容重要,读屏软件会强调 优先用它
<i> font-style: italic 视觉斜体(术语、外来语、船名) 与 <em> 外观一样
<em> font-style: italic 着重强调,语气变化 优先用它
<mark> 背景 rgb(255, 255, 0) 高亮「此处与当前上下文相关」 搜索命中词高亮
<small> 13.3333px(≈0.83em) 细则、免责声明、版权 不是「变小一号」的 CSS
<sub> / <sup> vertical-align: sub / super,13.3333px 下标/上标 H2O、E=mc2
<del> line-through 内容被删除(改稿、原价) 可带 cite/datetime
<s> line-through 内容不再准确/不再相关 有效标签,只是不如 <del> 常用
<ins> underline 新增内容 与 <del> 配对使用
<u> underline 特殊标注(拼写错误、专有名词) 容易和链接混淆,慎用
<cite> italic 作品名(书名、文章名) 不是「引用来源」
<q> 自动加引号(实测 ::before 是 open-quote) 短引用 中文场景一般直接写引号
<blockquote> 左右 margin: 40px 长引用 可带 cite 属性
<abbr title="..."> 无(实测 cursor: auto) 缩写,title 给全称 悬停提示,读屏可朗读
<bdo dir="rtl"> 文字方向反转 双向文本覆盖 DOM 里的字符顺序不变,只是视觉反
1
2
3
4
<p>Python 的 <strong>缩进</strong>不是风格而是语法;<mark>IndentationError</mark> 会直接报错。</p>
<p>原价 <del>¥199</del>,现价 <ins>¥99</ins>。<small>(活动截止 2026-11-30)</small></p>
<p>水的化学式是 H<sub>2</sub>O,质能方程是 E=mc<sup>2</sup>。</p>
<p>第一次读 <cite>《Python 编程:从入门到实践》</cite> 时,我最喜欢的章节是 <abbr title="Object Oriented Programming">OOP</abbr>。</p>

排版类标签:

1
2
3
4
5
6
7
8
<p>段落前后会自动留出 margin(实测 16px,等于 1em)。</p>
第一行<br>第二行(br 不结束段落,只换行)
<hr> <!-- 主题分隔线,实测 display: block -->
<pre>
pre 会原样保留空格和换行(实测 white-space: pre),
并且默认使用等宽字体。
</pre>
<blockquote>引用一整段话时用它,浏览器默认缩进 40px。</blockquote>

遗留标签:浏览器还在兼容,但别再用

原笔记拿 <marquee> 当「单边标签」的例子,这里一并说清楚——它和 <font>、<center> 都属于非标准/遗留元素:

1
2
3
<marquee direction="right" scrollamount="20" loop="1">今天是个好日子</marquee>
<font face="Arial" size="5" color="red">老式文字样式</font>
<center>老式居中</center>
  • 实测(Chrome):HTMLMarqueeElement、HTMLFontElement、HTMLFrameSetElement 这些构造函数依然存在,<font size="5" color="red"> 仍然渲染成 24px 红色,<center> 仍然居中(内部映射为 -webkit-center);HTMLCenterElement、HTMLKeygenElement 已经不存在了。
  • 「能跑」不等于「该用」:它们没有语义、无法响应式、样式改不动,且随时可能被移除。文字样式交给 CSS,滚动/跑马灯交给 CSS 动画:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<style>
.ticker {
overflow: hidden; /* 超出视口的部分裁掉 */
white-space: nowrap;
}

.ticker span {
display: inline-block;
padding-left: 100%; /* 从右侧屏幕外开始 */
animation: ticker 10s linear infinite;
}

@keyframes ticker {
from {
transform: translateX(0);
}

to {
transform: translateX(-100%);
}
}
</style>

<div class="ticker"><span>公告:这是用 CSS 动画实现的横向滚动字幕,可随时暂停。</span></div>
  • 好处是可以精确控制速度、方向和暂停(animation-play-state: paused),还能照顾「减少动态效果」的用户偏好:
1
2
3
4
5
@media (prefers-reduced-motion: reduce) {
.ticker span {
animation: none; /* 晕动症用户会感谢这一行 */
}
}

标题的默认字号(实测)

标签 <h1> <h2> <h3> <h4> <h5> <h6>
计算后字号 32px 24px 18.72px 16px 13.28px 10.72px

都是 font-weight: 700。这组数字来自浏览器默认样式表,换浏览器/换字号设置就会变——所以别依赖默认值,要写 CSS。

字符实体

HTML 里 <、&、" 这些字符有语法含义,想显示它们本身就得写成「字符实体」。两种形式:

  • 命名实体:&lt; &gt; &amp; &quot; &apos; &nbsp; &copy; &reg; &times; &divide; &mdash; &hellip;
  • 数字实体:&#60;(十进制)或 &#x3C;(十六进制),对任意 Unicode 字符都适用,例如 &#20013; = 中,&#x4E2D; = 中。

常用对照:

想显示的字符 命名实体 数字实体
< &lt; &#60; / &#x3C;
> &gt; &#62; / &#x3E;
& &amp; &#38; / &#x26;
" &quot; &#34; / &#x22;
' &apos; &#39; / &#x27;
空格(不断行) &nbsp; &#160; / &#xA0;
© &copy; &#169; / &#xA9;
® &reg; &#174; / &#xAE;

分号不能省(实测)

浏览器对「不带分号的实体」有历史兼容行为,但不是所有名字都行:

1
2
3
4
5
6
7
带分号        不带分号(文本里)    不带分号(属性里)     结论
< X < SP X a SP < SP X lt -> 都行(历史遗留名)
& X & SP X a SP & SP X amp -> 都行
© © SP X a SP © SP X copy -> 都行
(U+00A0 空格) U+A0 SP X a SP U+A0 SP X nbsp -> 都行
♥ & h e a r t s SP X a SP & h e a r t s SP X hearts -> 不解码,原样输出
@ @ SP X a SP @ SP X 数字实体 -> 都能解码
  • 规范只给一小撮「遗留名字」开了省分号的口子,&hearts; 这类不带分号就不会被解码;同一段代码在不同浏览器/不同上下文里表现可能不一致。
  • 结论:永远写分号;&nbsp; 只用来「不希望在此处换行」,批量缩进请用 CSS。
1
2
<p>5 &lt; 10 且 10 &gt; 5,符号写法 &amp;lt; 会原样显示成 &lt;。</p>
<p>A &nbsp;B 之间的空格不会被折行拆开;&copy; 2026 保留所有权利。</p>

属性值里也一样要转义:<a href="/search?q=A&amp;B">(两个参数用 & 连接,必须写成 &amp;),页面上显示 A & B 要写 A &amp; B。

列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 有序列表 -->
<ol>
<li>第一项</li>
<li>第二项</li>
</ol>

<!-- 无序列表 -->
<ul>
<li>列表项1</li>
<li>列表项2</li>
</ul>

<!-- 描述列表:术语 + 说明 -->
<dl>
<dt>HTML</dt>
<dd>超文本标记语言。</dd>
<dt>CSS</dt>
<dd>层叠样式表。</dd>
</dl>

编号样式:type 属性 vs CSS

<ol type="I">、<ul type="square"> 这类属性属于遗留特性(HTML5 里不赞成使用),但实测 Chrome 仍然把它翻译成对应的 list-style-type:

写法 实测计算后的 list-style-type
<ol>(默认) decimal
<ol type="I"> upper-roman
<ol type="a"> lower-alpha
<ol type="1"> decimal
<ul>(默认) disc
<ul type="square"> square

推荐写法是 CSS,能玩的花样多得多(upper-greek、cjk-ideographic、hiragana、none,甚至自定义 @counter-style):

1
2
3
4
5
6
7
8
9
10
11
12
13
<ol style="list-style-type: upper-roman;">
<li>第一项</li>
</ol>

<ul style="list-style-type: cjk-ideographic;">
<li>甲项</li>
<li>乙项</li>
</ul>

<!-- 去掉项目符号 -->
<ul style="list-style: none; padding-left: 0;">
<li>导航项</li>
</ul>
  • 列表可以嵌套,<ul>/<ol> 的直接子元素只能是 <li>(说明性内容放在 <li> 里)。
  • <li> 的结束标签可以省略,浏览器会自动补齐——但别在列表项之间手写一个孤立的 </li>:HTML 容错性强,读屏软件和其它工具未必喜欢。

URL:统一资源定位符(重点)

拿这一串当例子:https://user:pw@example.com:8443/dir/file.html?a=1&b=2#frag

这一段 内容 说明 URL 对象里的属性
协议 https: 冒号是协议名的一部分,写在后面 protocol
用户名 user 冒号分隔用户名与密码,整段后面跟 @ 才有效 username
密码 pw 同上 password
主机 example.com 域名或 IP hostname
端口 8443 前面带冒号;默认端口会被省略(http://example.com:80 的 port 是空串,实测) port;host = 主机 + 端口
路径 /dir/file.html 从第一个 / 开始 pathname
查询串 ?a=1&b=2 ? 是分隔符,参数之间用 & search;逐个取用 searchParams
片段 #frag # 是分隔符,不会发给服务器,浏览器拿来定位页内锚点 hash

用 URL 对象把上面这串拆开(实测):

1
2
3
4
5
6
7
8
9
10
11
const u = new URL('https://user:pw@example.com:8443/dir/file.html?a=1&b=2#frag');
console.log(u.protocol); // https:
console.log(u.hostname); // example.com
console.log(u.port); // 8443
console.log(u.host); // example.com:8443(含端口)
console.log(u.pathname); // /dir/file.html
console.log(u.search); // ?a=1&b=2
console.log(u.hash); // #frag
console.log(u.username); // user
console.log(u.origin); // https://example.com:8443(协议+主机+端口,跨域判断用的就是它)
console.log([...u.searchParams]); // [ [ 'a', '1' ], [ 'b', '2' ] ]

相对路径怎么解析(实测)

基准地址为 http://127.0.0.1:4099/dir/sub/index.html?p=1#f 时:

写法 解析结果
./a/b、a/b http://127.0.0.1:4099/dir/sub/a/b(相对于当前目录)
../a http://127.0.0.1:4099/dir/a(回一层)
../../a http://127.0.0.1:4099/a
/x http://127.0.0.1:4099/x(站根,只看协议+主机+端口)
?k=2 http://127.0.0.1:4099/dir/sub/index.html?k=2(保留路径,换查询串)
#top http://127.0.0.1:4099/dir/sub/index.html?p=1#top(保留一切,只换锚点)
//other.example/dir http://other.example/dir(协议相对,跟随当前页协议)
  • 关键坑:a/b 相对的是当前文件所在目录,不是当前页面「看起来」的目录;路由式地址(如 /user/1)下相对路径会解析到意外位置,这时用 /abs/path 最稳。
  • http:// 与 https://、// 开头的写法要注意混合内容(HTTPS 页面加载 HTTP 资源会被拦)。

编码:三个函数别用错(实测)

1
2
3
4
const s = '/中文/a b?x=中文';
encodeURI(s); // /%E4%B8%AD%E6%96%87/a%20b?x=%E4%B8%AD%E6%96%87 保留 / ? & 等结构字符
encodeURIComponent(s); // %2F%E4%B8%AD%E6%96%87%2Fa%20b%3Fx%3D%E4%B8%AD%E6%96%87 连 / ? 都编码
new URLSearchParams({q: '中文 x&y=1'}).toString(); // q=%E4%B8%AD%E6%96%87+x%26y%3D1 空格变 +
  • 编码整个路径段或单个参数值用 encodeURIComponent;拼一整串 URL 用 encodeURI。
  • 表单提交(urlencoded)时空格编码成 +,而 encodeURIComponent 编成 %20,两者都能被标准解析还原,但手写解析时要记住这点。
  • 中文路径在生产环境尽量少用:不同系统的文件名规范化差异会带来编码问题。

超链接 <a>(重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 跳到另一个站点 -->
<a href="https://example.com" target="_blank" rel="noopener nofollow">外部站点</a>

<!-- 同站内相对路径 -->
<a href="/docs/intro.html">文档首页</a>

<!-- 跳到本页的某个位置:目标是任意元素的 id -->
<a href="#section2">跳到第二节</a>
<h2 id="section2">第二节</h2>

<!-- 下载链接,download 指定保存的文件名(仅对同源/可写 URL 有效) -->
<a href="/files/report.pdf" download="报告.pdf">下载报告</a>

<!-- 邮件与电话 -->
<a href="mailto:someone@example.com?subject=咨询">发邮件</a>
<a href="tel:+8613800000000">拨打电话</a>

常用属性:

属性 作用 要点
href 链接目标 必需;也可以是 mailto:、tel:、#锚点、data:、javascript:(后者在 CSP 下会被禁,且不利于无障碍)
target 在哪里打开 _self(默认)、_blank 新标签、_parent、_top;也可指向 <iframe name="..."> 或 <a>/<form> 声明的浏览上下文名字
rel 与目标的关系 noopener(切断 window.opener,防反向钓鱼)、noreferrer(不发 Referer,含 noopener 效果)、nofollow(不参与 SEO 权重传递)、external、preload 等,空格分隔可写多个
download 提示下载 值可作文件名;跨域链接上会被忽略
title 悬停提示 别把关键信息只放在 title 里(键盘用户看不到)
type 目标 MIME 类型 遗留属性,基本不用
name 老式锚点 HTML5 已废弃,改用 id

target="_blank" 为什么要配 rel="noopener"

  • 早期:新页面能通过 window.opener 反过来把原页面导航到钓鱼站点(tabnabbing),并与原页面同进程影响性能。
  • rel="noopener" 切断 window.opener。现代浏览器对 target="_blank" 已默认隔离,但显式写上仍是兼容性最好的做法;noreferrer 顺带不发 Referer。
  • 需要新页面操作 opener(少见)时才用 rel="opener"。

锚点与页面滚动

  • href="#id" 会滚动到 id 对应的元素;也支持特例 #top 回到页面顶部。
  • 目标元素需要 tabindex="-1" 才能被键盘用户正确聚焦,这是无障碍细节。
  • 平滑滚动属于 CSS:html { scroll-behavior: smooth; }。

图片与多媒体

图片 <img>

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 最简单:src 必填,alt 必填(图挂了/读屏/搜索引擎都靠它) -->
<img src="/static/img/net/tcp.png" alt="TCP 三次握手与四次挥手过程" width="700" height="400">

<!-- 响应式:按屏宽选图,picture 用于「不同图」 -->
<img src="photo-480.jpg" srcset="photo-480.jpg 480w, photo-960.jpg 960w, photo-1920.jpg 1920w"
sizes="(max-width: 600px) 100vw, 50vw" alt="海边日落" loading="lazy">

<picture>
<source srcset="logo.svg" type="image/svg+xml">
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="站点标志">
</picture>
  • alt 不是「可选」:图片加载失败时它是唯一线索,也是无障碍的硬要求。装饰性图片写 alt=""。
  • 写 width/height(或用 CSS 固定比例)能避免图片加载完成时页面抖动。
  • loading="lazy":延迟加载屏外图片。
  • <picture> + <source> 用于按条件换不同的图(格式回退、艺术方向),srcset 用于同一张图的不同尺寸。
  • 老笔记里的「<img> 是单边标签」写法 <img ... /> 也能解析;实测解析器会丢弃这个斜杠(序列化回来是 <img src="..." alt="i">),HTML 里写不写都行,按一个风格写死即可。

音频与视频

1
2
3
4
5
6
7
8
9
10
11
12
<audio controls>
<source src="song.mp3" type="audio/mpeg">
<source src="song.ogg" type="audio/ogg">
您的浏览器不支持 audio 标签。
</audio>

<video controls width="640" height="360" poster="cover.jpg" preload="metadata" playsinline>
<source src="movie.mp4" type="video/mp4">
<source src="movie.webm" type="video/webm">
<track kind="captions" src="zh.vtt" srclang="zh" label="中文字幕" default>
您的浏览器不支持 video 标签。
</video>
属性 作用 备注
controls 显示播放控件 不写就没有任何控件,需要自己用 JS 做
autoplay 自动播放 主流浏览器要求同时 muted 才允许带自动播放,否则被拦
loop 循环 —
muted 默认静音 自动播放的门票
preload none / metadata / auto 实测 Chrome 里不写时 video.preload 读回 metadata(只预取时长/尺寸等元信息),写 auto 才积极预加载
poster 封面图 视频未播放时显示
playsinline iOS 上不强制全屏 移动端常见坑
<track> 字幕/章节 WebVTT 格式

<embed> 与 <object>

1
2
3
4
5
6
<!-- 嵌入 PDF,并提供下载兜底 -->
<object data="/files/report.pdf" type="application/pdf" width="800" height="600">
<a href="/files/report.pdf">下载 PDF</a>
</object>

<embed src="player.swf" type="application/x-shockwave-flash" width="400" height="300">
  • 实测 <object> 的 data 会被解析成绝对地址(http://127.0.0.1:4099/f.pdf),它和 src 的相对路径规则一样。
  • Flash 已在 2020 年底停止支持,<embed> 放 .swf 的写法只出现在老页面里;PDF/地图/视频优先用 <iframe> 或 <object>。

表格 <table>

表格的本职是展示二维数据,不是页面布局(当年用 table 排版的历史已经翻篇,布局请用 CSS Grid/Flex)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<table>
<caption>课程表</caption> <!-- 表格标题,实测 DOM 里就是 <caption> 元素 -->
<thead>
<tr>
<th scope="col">时间</th> <!-- 列表头 -->
<th scope="col">星期一</th>
<th scope="col">星期二</th>
<th scope="col">星期三</th>
<th scope="col">星期四</th>
<th scope="col">星期五</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">09:00-10:00</th> <!-- 行表头,仍用 th -->
<td>数学</td>
<td>语文</td>
<td>英语</td>
<td rowspan="2">物理</td> <!-- 跨两行 -->
<td>化学</td>
</tr>
<tr>
<th scope="row">10:15-11:15</th>
<td>体育</td>
<td colspan="2">美术</td> <!-- 跨两列 -->
<td>生物</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="6">注:周六、周日无课</td>
</tr>
</tfoot>
</table>
标签 作用 关键属性
<table> 表格容器 —
<caption> 表标题 是 <table> 的直接子元素,必须放在最前面
<colgroup> / <col> 按列设置样式/宽度 span
<thead> / <tbody> / <tfoot> 分组行 <tfoot> 可以写在 <tbody> 前面,渲染时仍在底部
<tr> 一行 —
<th> 表头单元格 scope="col|row|colgroup|rowgroup"、abbr
<td> 数据单元格 colspan、rowspan
  • 合并单元格:被覆盖掉的 <td> 必须删掉,否则整行会错位。上面例子里第二行只有 5 个单元格,因为「物理」被上一行的 rowspan="2" 占了。
  • 实测 td.colSpan === 2、td.rowSpan === 2、th.scope === 'col',这些属性在 DOM 里都能读回。
  • border、cellpadding、cellspacing、bgcolor、align、width 都是遗留属性(HTML5 不支持)。样式全交给 CSS:
1
2
3
4
5
6
7
8
table {
border-collapse: collapse; /* 合并相邻边框,等价于老写的 border + cellspacing=0 */
width: 100%;
}
caption { caption-side: top; font-weight: 700; padding: 8px; }
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; }
tbody tr:nth-child(2n) { background: #fafafa; } /* 斑马纹 */
thead { background: #f0f0f0; }
  • 让长表格在手机上不至于撑破布局:外层套 <div style="overflow-x: auto">。

iframe:把另一个文档嵌进来

1
2
3
4
5
6
7
8
9
10
11
<!-- 最常用:嵌入地图/视频/第三方页面 -->
<iframe src="https://example.com"
title="示例站点"
width="600" height="400"
loading="lazy"
sandbox="allow-scripts allow-forms"
referrerpolicy="no-referrer-when-downgrade"
style="border: 0;"></iframe>

<!-- 直接写内容,不发请求(注意引号转义) -->
<iframe srcdoc="<p>Hello, world!</p>" title="内联内容"></iframe>
属性 作用 说明
src 内嵌页面地址 也可由 JS 改 frame.src
srcdoc 直接给一段 HTML 实测读回时引号已被解码(<p>hi</p>);与 src 二选一
name 命名浏览上下文 给 <a target> / <form target> 用——表单提交结果开在指定 iframe 里就是这么实现的
sandbox 沙箱限制 空值=最严(禁脚本、禁表单、禁同源、禁弹窗);按需加 allow-scripts、allow-forms、allow-same-origin、allow-popups、allow-top-navigation
allow 权限策略 如 allow="fullscreen; autoplay; encrypted-media",控制摄像头/全屏/自动播放等特性开关(不是 Flash)
allowfullscreen 允许内部内容全屏 视频站必备
loading lazy / eager 实测 Chrome 支持
referrerpolicy 发多少 Referer 常用 no-referrer-when-downgrade、strict-origin-when-cross-origin
title 无障碍名字 建议每个 iframe 都写
frameborder / scrolling / width / height 遗留 实测 scrolling 属性在 Chrome 还存在,但请改用 CSS:border: 0; overflow: hidden;

用 iframe 做布局的坑

  • 注释不能写在标签里(原笔记的示例就犯了这个错)。实测:

    1
    <iframe src="a" <!-- 注释 --> width="5"></iframe>

    被解析成 <iframe src="a" <!--="" c="" --=""> —— 三个垃圾属性,标签在 --> 的 > 处提前闭合,剩下的 width="5" 变成 iframe 的文本内容。

  • 跨域 iframe 不能直接访问对方 DOM(同源策略);父子通信只能用 postMessage。

  • iframe 会被计入页面请求数、影响首屏;移动端滚动体验也差。除非确有必要(嵌第三方播放器/地图/支付页),优先自己渲染内容。

  • 安全:嵌不可信内容时务必配 sandbox,并注意点击劫持防护(对方可能用 X-Frame-Options / frame-ancestors 拒绝被嵌)。

<frameset>:已废弃,了解即可

原笔记里有一段 frameset 示例,它本身写错了(<frame> 是空元素,不能把 <frameset> 嵌套在 <frame> 里面)。正确形态长这样——注意 <frameset> 是和 <body> 平级、且不能同时出现 <body>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<title>Frameset 示例(已废弃,仅作了解)</title>
</head>
<frameset rows="100,*" frameborder="1" border="5">
<frame src="top.html" name="topFrame" noresize>
<frameset cols="200,*">
<frame src="menu.html" name="menuFrame">
<frame src="main.html" name="mainFrame" scrolling="auto">
</frameset>
</frameset>
<noframes>
<body>本页面使用框架,当前浏览器不支持。请直接访问 <a href="main.html">正文</a>。</body>
</noframes>
</html>
  • <frame> 永远是空元素(没有结束标签);嵌套是 <frameset> 套 <frameset>。
  • rows/cols 用逗号分隔,* 表示剩余空间,可写像素或百分比。
  • frameset 的问题是本质缺陷:每个 frame 是独立文档,地址栏不反映状态、无法整体滚动、SEO 不友好、窗口尺寸无法精确控制。HTML5 已废弃,现代做法是 CSS 布局 + <iframe>(或前端路由 + 组件)。

表单 <form>(非常重要)

表单是浏览器向服务器提交数据的主要入口。学习重点不是标签怎么拼,而是 「点提交之后,到底哪些东西、以什么格式发出去了」 。下面所有结论都是本机 Chrome + 一个本地回显服务实测出来的报文。

基本结构

1
2
3
4
5
6
7
8
9
10
<form action="/user/register" method="post" enctype="application/x-www-form-urlencoded"
name="regForm" autocomplete="on">
<label for="username">用户名</label>
<input type="text" id="username" name="username" required placeholder="字母数字,2~20 位">

<label for="pwd">密码</label>
<input type="password" id="pwd" name="pwd" required minlength="8">

<button type="submit">注册</button>
</form>
属性 作用 要点
action 提交到的地址 实测不写时默认就是当前页面完整 URL(form.action 读回来是绝对地址)
method get(默认)或 post 大小写都行;method="dialog" 只在表单位于 <dialog> 内时有意义(关闭对话框,不发请求),实测 form.method 能读回 dialog
enctype 编码方式(仅 POST 生效) application/x-www-form-urlencoded(默认)、multipart/form-data(传文件必须用它)、text/plain
name 表单名 JS 里 document.forms['regForm'] 用
target 结果在哪里显示 _self/_blank/_parent/_top/iframe 的 name
autocomplete 是否允许自动填充 on/off,字段级也能写
novalidate 提交时跳过浏览器校验 写了就不校验,直接发出去
accept-charset 提交字符集 现代页面统一 UTF-8,几乎不用
method="dialog" 把 <dialog> 的关闭动作交给表单 不向后端发请求

实测:点不同按钮时,服务器收到的数据

下面这张表是同一个表单分别由不同「提交者」提交时,回显服务打出来的字段(顺序即文档顺序,提交者自己排最后)。

提交者 实测结果
<input type="submit" name="subin" value="SUB"> 末尾多出 subin=SUB
<button type="submit" name="subbtn" value="BTN">go</button> 末尾多出 subbtn=BTN(原笔记说不能提交,实测能)
<button type="submit" name="subbtn2">无 value 属性</button> 多出 subbtn2=(空串,字段名仍在)
<input type="button" name="ibtn" value="..."> 完全不提交,没有任何请求
<input type="reset"> 只重置表单,不发请求

一次真实 POST 的原始请求体(urlencoded):

1
2
txt=hello&cjk=%E4%B8%AD%E6%96%87+%E7%A9%BA%E6%A0%BC&ro=READONLY&hid=h1&cb=c1
&rd=r2&sel=v&multi=m1&multi=m3&ta=line1&fl=&reqme=ok&subin=SUB

从中能读出的规则(全部实测):

  1. 必须有 name 才会上报,name 就是参数键。
  2. disabled 的字段不出现(例子里 dis=HIDDEN 被剔除)。
  3. readonly 的字段照常提交(ro=READONLY 在里面)——只读不等于不提交。
  4. 复选框只有勾选的才提交,值为它的 value;不写 value 时实测提交 on。单选组里只提交被选中那一项,所以同组 name 必须一致。
  5. <select multiple> 会产生多个同名键(multi=m1&multi=m3);<option> 不写 value 时,实测用它的显示文本作为值。
  6. <textarea> 的内容作为字符串提交;<input type="file"> 未选文件时会产生 fl=(空串)。
  7. 中文按 UTF-8 百分号编码,空格编码成 +。
  8. GET 时上面这一整串直接变成 URL 的查询串,请求体为空;POST 时放进请求体。
  9. <input type="file"> 在未选文件时会产生 fl=(空串);选了文件但用默认的 urlencoded 编码时,实测只发出文件名 fl=my+file.txt,文件内容根本没进去。换成 multipart/form-data 后才收到真正的分片(实测 fl → [file:up.txt bytes=9])——所以上传文件必须 enctype="multipart/form-data",且 method="post"。
  10. 有必填/格式校验不通过时,请求根本不会发出(实测「必填项为空」那一次收不到任何请求);加上 novalidate 才会照样发出(reqme= 空值)。
  11. <fieldset disabled> 会把组内所有控件一起排除(实测提交条目从 2 条变成 0 条);<input type="image"> 提交时附带点击坐标 name.x、name.y(实测得到 iimg.x=3、iimg.y=4)。

三种 enctype 的报文对比(实测)

1
2
3
4
5
6
7
8
9
10
application/x-www-form-urlencoded   txt=hello&cjk=%E4%B8%AD%E6%96%87+%E7%A9%BA%E6%A0%BC&...
text/plain txt=hello\r\ncjk=中文 空格\r\nro=READONLY\r\n...
multipart/form-data ------WebKitFormBoundaryXXX
Content-Disposition: form-data; name="txt"
hello
------WebKitFormBoundaryXXX
Content-Disposition: form-data; name="fl"; filename="up.txt"
Content-Type: text/plain
file-body
...
  • text/plain 是「键=值 + CRLF」的裸文本,中文不编码,服务器按普通表单解析不了(实测按 urlencoded 解析会得到一整坨),只适合调试或极特殊接口。
  • multipart/form-data 每个字段是一个带分隔符的分片,因此能携带二进制,代价是报文更大。

提交按钮:<button> 的默认 type 是坑

  • 实测 document.createElement('button').type 返回 submit。也就是说:表单里的 <button> 不写 type 就会提交表单。做「只负责前端交互」的按钮时必须写 type="button"。
1
2
3
4
<button type="submit">提交表单</button>            <!-- 提交 -->
<button type="button" onclick="check()">仅执行 JS</button> <!-- 不提交 -->
<button type="reset">重置</button> <!-- 恢复默认值 -->
<input type="submit" name="sub" value="提交"> <!-- 提交,并带上 sub=提交 -->
  • <button> 里可以放 HTML(图标 + 文字),<input type="submit"> 只能显示 value 文本。
  • 想「点击后先跑 JS 再决定是否提交」:监听 submit 事件,必要时 event.preventDefault(),或用 form.requestSubmit() 主动触发。

<label>:一定要用

1
2
3
4
5
6
<!-- 写法一:for 指向控件的 id(最常用,两者中间还能放别的元素) -->
<label for="email">邮箱</label>
<input type="email" id="email" name="email">

<!-- 写法二:把控件包进 label,隐式关联,不需要 id -->
<label>邮箱 <input type="email" name="email"></label>
  • 实测:点击 <label> 会让关联控件获得焦点(document.activeElement.id 变成该控件);点复选框的 label 等于点复选框本身——移动端体验差距就在这。
  • 屏幕阅读器靠它念出「这是哪个字段的输入框」,没有 label 的表单在无障碍检测里属于严重问题。

表单里能放什么

  • 任何标签都能放进 <form>,但只有带 name 的可交互控件(input/select/textarea/button)会进入提交数据。
  • <fieldset> + <legend> 用来给字段分组,实测 fieldset.disabled = true 能一次性禁用整组控件。
  • <datalist> 给文本框提供候选项(输入框仍能手输),<output> 显示计算结果。
1
2
3
4
5
6
7
8
9
10
11
12
<fieldset>
<legend>性别</legend>
<label><input type="radio" name="gender" value="m" checked>男</label>
<label><input type="radio" name="gender" value="f">女</label>
</fieldset>

<label>城市</label>
<input list="city" name="city">
<datalist id="city">
<option value="郑州"></option>
<option value="洛阳"></option>
</datalist>

原笔记的「表单使用说明」,逐条核对后的版本

1
2
3
4
5
6
1. 要提交的控件必须落在 <form> 里面(或用 form="formId" 属性关联),否则不参与提交。   —— 正确
2. 只有带 name 的控件才会上报;单选、多选的同组 name 必须一致。 —— 正确
3. 不能由用户输入的控件(radio/checkbox/option)必须写 value,且值要短。 —— 正确
4. 上传文件:method="post" 且 enctype="multipart/form-data"(原文拼成 form-datab)。 —— 已修正
5. 补充:disabled 的控件不提交;readonly 的照常提交;button 不写 type 默认就是 submit。
6. 补充:校验不通过时请求根本不会发出,novalidate 可跳过。

<input> 的类型与属性

type 决定了控件的长相、可用属性和校验规则。实测:写了浏览器不认识的值会静默退化成 text,所以别指望校验会自动生效。

type 控件 实测结论
text 单行文本 默认值(document.createElement('input').type 返回 text)
password 密码 掩码显示
search 搜索框 支持,外观可能带清除按钮
tel 电话号码 支持,移动端弹数字键盘;不做格式校验
email 邮箱 支持,提交时做基本格式校验
url URL 支持,校验相对宽松
number 数字 支持,配合 min/max/step
range 滑块 支持
date / time / datetime-local / month / week 日期时间类 都支持(桌面 Chrome 有原生选择器)
datetime 带时区的日期时间 已移除,实测退化回 text——别再写
checkbox / radio 复选 / 单选 支持
file 文件 支持,配合 multiple、accept
submit / reset / button 三种按钮 支持
image 图片提交按钮 支持,提交时会附带 name.x / name.y(点击坐标)
hidden 隐藏字段 支持,页面上不可见但会提交
color 取色器 支持,值是 #rrggbb

常用属性

属性 适用类型 说明
name 全部 提交的键,不写就不上报
value 全部 初始值;checkbox/radio 的是「被选中时上报的值」
placeholder 文本类 占位提示,不能当 label 用(输入后就不见了)
required 大多 空值时阻止提交
disabled 全部 禁用、不提交、不可聚焦
readonly 文本类 只读,仍会提交,可用 Tab 聚焦
minlength / maxlength 文本类 长度限制;maxlength 会真的截断输入
min / max / step number 及日期类 见下方 step 的坑
pattern 文本类 正则校验,自动锚定整串(等于 ^(?:…)$)
multiple file / email 允许多个值
accept file 过滤可选文件类型,如 image/*、.pdf
autocomplete 文本类 on/off,或 name、email、new-password 等提示
size 文本类 可见宽度(字符数),不是最大长度
form 全部 把控件关联到页面上某个 id 的表单,不必物理嵌套
formaction / formmethod / formenctype / formnovalidate / formtarget submit 类 单个按钮覆盖表单的同名属性(「保存草稿用 POST、发布用另一套」就靠它)

step 的基准是 min(实测)

1
<input type="number" name="num" min="18" max="99" step="5">
1
2
3
value=18 -> stepMismatch: false    (18 是 step 基准)
value=23 -> stepMismatch: false (18 + 5)
value=20 -> stepMismatch: true (20 不在 18,23,28,... 里)
  • 合法值必须是 min + n × step。没有 min 时基准是 value(若有)或 0。上面这个例子如果 min 换成 0,20 就合法了。
  • 这个校验只影响提交;用 JS 改 el.value 不会实时报错,要读 el.validity.stepMismatch。

校验与提示

1
2
3
4
5
6
<form novalidate>                          <!-- 关掉浏览器默认拦截,自己来 -->
<input type="text" name="code" pattern="A-Z{3}-\d{4}" required
title="形如 ABC-1234">
<output name="msg"></output>
<button>校验</button>
</form>
1
2
3
4
5
const el = form.code;
console.log(el.checkValidity()); // 单个控件是否合格
console.log(el.validity.patternMismatch);
console.log(form.checkValidity()); // 整个表单
console.log(form.reportValidity()); // 让浏览器显示默认提示
  • 内置校验的报错文案由浏览器控制、跟随系统语言,样式也很丑;正式项目通常用 :invalid/:valid 或 JS 接管提示。
  • 浏览器校验只是体验,服务端校验才是安全边界——永远要在后端重做一遍。

<select> 与 <textarea>

<select>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<label for="car">车型</label>
<select name="car" id="car" required>
<option value="">请选择</option>
<option value="volvo" selected>Volvo</option>
<option value="saab">Saab</option>
</select>

<!-- 多选 + 分组 + 指定可见行数 -->
<select name="cars" multiple size="4">
<optgroup label="瑞典车">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
</optgroup>
<optgroup label="德国车">
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</optgroup>
</select>
位置 属性 说明
<select> name、id 同上
<select> size 不滚动时可见行数;配合 multiple 才像列表框
<select> multiple 多选,Ctrl/Cmd 点选;实测会产生多个同名参数
<select> required 想让它真正起作用,就放一个 value="" 的占位选项(实测默认 selected 生效时 select.value === 'volvo')
<select> disabled 整个下拉禁用,不提交
<option> value 提交值;不写就用显示文本
<option> selected 默认选中(多个同时写,取第一个)
<option> disabled 该项不可选
<optgroup> label 分组标题,不可选
  • 想用「请选择」拦截空提交,<option value=""> + required 是最省事的写法(空值会被拦下)。
  • 需要搜索/多选花哨交互时,考虑 <input list> + <datalist> 或第三方组件。

<textarea>

1
2
3
<label for="comment">留言</label>
<textarea id="comment" name="comment" rows="4" cols="50"
maxlength="200" placeholder="说点什么……" required></textarea>
属性 说明
rows / cols 可见行数 / 以字符计的宽度
placeholder 提示文字
maxlength 最大字符数(实测属性读回就是 200,浏览器会真的限制输入)
readonly / disabled 同上;readonly 仍可提交
required 空则阻止提交
wrap soft(默认)/ hard;hard 会在提交前按 cols 折行(实测属性可写可读,日常用不上)
autocomplete / autofocus / spellcheck 同常规
  • 内容写在开始/结束标签之间,不是 value 属性:<textarea name="x">初始内容</textarea>。
  • 里面的 < 也要转义(&lt;),否则会被当标签解析——这是常见 bug 源。
  • 需要「富文本」(加粗、贴图、@人)时不能用 <textarea>,要用可编辑区域或富文本编辑器。
  • 让文本域自动增高、可拖拽缩放是 CSS 的活:resize: vertical; min-height: 6rem;。

块级元素与行内元素

实测 Chrome 默认 display 值:

类别 代表标签 实测 display 特点
块级 <div>、<p>、<h1>~<h6>、<ul>、<ol>、<li>、<table>、<form>、<hr>、<pre>、<blockquote> block 独占一行,宽度默认撑满父元素,能设宽高
行内 <span>、<a>、<b>、<i>、<em>、<img>、<input>、<button>、<label> inline 不换行、跟着文字排,宽高与上下 margin 无效(<img> 可设宽高,因为它是替换元素)
行内块 无默认,靠 CSS inline-block 内部像块、外部像行内
表格相关 <table>、<tr>、<td> table、table-row、table-cell 有自己的布局规则
不显示 <head>、<script>、<template>、<style> none 实测 <template> 里的内容不参与渲染(textContent 读回空,内容存在 el.content 这个文档片段里)
  • 内容的嵌套有规则,而且浏览器的自动纠错会改变结构。实测把 <div> 塞进 <p>:
    1
    <p>甲<div>乙</div>丙</p>
    解析结果是 <p>甲</p><div>乙</div>丙<p></p> —— <p> 被提前闭合,「丙」掉到外面,末尾还多出一个空 <p>。同理,规范上 <ul> 的直接子元素只能是 <li>(校验器会报错,浏览器则原样保留,不会替你修正),<table> 里只能出现表格相关标签。
  • <div> / <span> 是「无意义容器」:结构上需要分组、但要挂 CSS/JS 时用它们,语义上能用 <section>/<article>/<em> 时优先用语义标签。
  • 实测 <center> 仍然生效(Chrome 内部把它映射成 -webkit-center),但它早已废弃,居中请用 CSS。

标签写法与容错

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 双边标签:有开始、有结束 -->
<p class="note">内容写在中间</p>

<!-- 空元素(单边):本身没有内容,写不写斜杠都行 -->
<br>
<input type="text" name="a">
<img src="a.png" alt="a">
<meta charset="utf-8">
<hr>

<!-- 属性写法 -->
<a href="/x" title="提示文字" data-id="7" class="link a-link">链接</a>
  • 标签名一律小写,属性名小写,属性值加引号(不加引号浏览器也能容错解析,但值里有空格就必须加)。
  • 空元素(void element)没有结束标签:area、base、br、col、embed、hr、img、input、link、meta、param、source、track、wbr。写 <br/> 是 XHTML 遗留,HTML5 解析器会丢掉那个斜杠(实测序列化回来是 <br>),两种都能用,选一种坚持。
  • <div/> 不是自闭合! 实测:
    1
    <div id="sc"><div/>后面的内容</div>
    解析成 <div><div>后面的内容</div></div> —— 斜杠被忽略,第二个 <div> 照样是开始标签,它没有显式闭合,于是后面所有兄弟节点全被吞进它里面(实测第一个子元素的 tagName 是 DIV,且 innerHTML 里包含了后续所有区块)。只有空元素才能省结束标签。
  • HTML 容错性很强(少写结束标签、大小写混写、属性不加引号都能渲染),但别利用它:读屏软件、爬虫、XHTML 类工具、HTML 校验器都不容错。
  • 属性名可以自定义,但要加 data- 前缀(如 data-id="7"),JS 里通过 el.dataset.id 读取——这是「把数据塞进 DOM」的规范做法。

常见坑速查

  • 图片不显示:src 相对路径是相对当前页面 URL 的,不是相对 CSS/HTML 文件所在目录;先看控制台是不是 404。
  • 表单字段收不到:
    • 忘了写 name。
    • 或者字段被 disabled。
    • 或者根本不在 <form> 里。
    • 或者 id 写成了 name。
  • 文件上传收不到文件:enctype 没写 multipart/form-data,或用了 GET。
  • 点按钮页面刷新了:<button> 在表单里没写 type,默认就是 submit。
  • 中文提交后是乱码:页面/数据库/连接串编码不是 UTF-8,或 method="GET" 时被中间层做了二次编码。
  • 锚点跳不过去:目标元素的 id 与 #值 不一致,或被 <base href> 改写了基准。
  • & 在 URL 里失效:写在 HTML 属性里要 &amp;(如 ?a=1&amp;b=2)。
  • 表格错位:合并单元格时忘了删掉被覆盖的 <td>。
  • type="datetime" 变成普通文本框:该类型已废弃,改用 datetime-local。
  • 校验没生效:值不在 min + n × step 上(stepMismatch),或 pattern 里写了 ^…$ 造成重复锚定。
  • 样式生效但布局怪:还在用 border、align、bgcolor、<font>、<center> 这些遗留表现属性——换成 CSS。
  • iframe 里注释掉了属性却少了个按钮/边框:注释不能写在标签内部,见 iframe 一节。

动手练习

  • 写一个 3×3 表格,要有 <caption>、表头 <th> 与数据 <td>,并合并第 1、4 格与第 8、9 格(想想 rowspan/colspan 该落在哪个单元格,合并后为什么行会「少一格」)。
  • 用 <form target="某个 iframe 的 name"> 做一个「提交后结果显示在页面右侧」的小页面——本篇的表单实测就是这么做的。
  • 写一个报名表:用户名、密码、性别(单选)、爱好(复选)、省份(下拉)、简介(文本域)、提交按钮;用 <table> 或 CSS 对齐,并确认提交报文里每个字段都有预期的键和值。
  • 把上面几条用「查看 → 网络面板」核对:name 缺失、disabled、readonly、未勾选的复选框分别会怎样体现在报文里。

参考资料