前言
- 这一篇是 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> <html lang="zh-cn">
<head> <meta charset="utf-8"> <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> <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 里 <、&、" 这些字符有语法含义,想显示它们本身就得写成「字符实体」。两种形式:
- 命名实体:
< > & " ' © ® × ÷ — …
- 数字实体:
<(十进制)或 <(十六进制),对任意 Unicode 字符都适用,例如 中 = 中,中 = 中。
常用对照:
| 想显示的字符 |
命名实体 |
数字实体 |
< |
< |
< / < |
> |
> |
> / > |
& |
& |
& / & |
" |
" |
" / " |
' |
' |
' / ' |
| 空格(不断行) |
|
  /   |
| © |
© |
© / © |
| ® |
® |
® / ® |
分号不能省(实测)
浏览器对「不带分号的实体」有历史兼容行为,但不是所有名字都行:
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 数字实体 -> 都能解码
|
- 规范只给一小撮「遗留名字」开了省分号的口子,
♥ 这类不带分号就不会被解码;同一段代码在不同浏览器/不同上下文里表现可能不一致。
- 结论:永远写分号;
只用来「不希望在此处换行」,批量缩进请用 CSS。
1 2
| <p>5 < 10 且 10 > 5,符号写法 &lt; 会原样显示成 <。</p> <p>A B 之间的空格不会被折行拆开;© 2026 保留所有权利。</p>
|
属性值里也一样要转义:<a href="/search?q=A&B">(两个参数用 & 连接,必须写成 &),页面上显示 A & B 要写 A & 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); console.log(u.hostname); console.log(u.port); console.log(u.host); console.log(u.pathname); console.log(u.search); console.log(u.hash); console.log(u.username); console.log(u.origin); console.log([...u.searchParams]);
|
相对路径怎么解析(实测)
基准地址为 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); encodeURIComponent(s); new URLSearchParams({q: '中文 x&y=1'}).toString();
|
- 编码整个路径段或单个参数值用
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>
<a href="#section2">跳到第二节</a> <h2 id="section2">第二节</h2>
<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
| <img src="/static/img/net/tcp.png" alt="TCP 三次握手与四次挥手过程" width="700" height="400">
<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
| <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> <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> <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; 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>(或前端路由 + 组件)。
表单是浏览器向服务器提交数据的主要入口。学习重点不是标签怎么拼,而是 「点提交之后,到底哪些东西、以什么格式发出去了」 。下面所有结论都是本机 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
|
从中能读出的规则(全部实测):
- 必须有
name 才会上报,name 就是参数键。
disabled 的字段不出现(例子里 dis=HIDDEN 被剔除)。
readonly 的字段照常提交(ro=READONLY 在里面)——只读不等于不提交。
- 复选框只有勾选的才提交,值为它的
value;不写 value 时实测提交 on。单选组里只提交被选中那一项,所以同组 name 必须一致。
<select multiple> 会产生多个同名键(multi=m1&multi=m3);<option> 不写 value 时,实测用它的显示文本作为值。
<textarea> 的内容作为字符串提交;<input type="file"> 未选文件时会产生 fl=(空串)。
- 中文按 UTF-8 百分号编码,空格编码成
+。
GET 时上面这一整串直接变成 URL 的查询串,请求体为空;POST 时放进请求体。
<input type="file"> 在未选文件时会产生 fl=(空串);选了文件但用默认的 urlencoded 编码时,实测只发出文件名 fl=my+file.txt,文件内容根本没进去。换成 multipart/form-data 后才收到真正的分片(实测 fl → [file:up.txt bytes=9])——所以上传文件必须 enctype="multipart/form-data",且 method="post"。
- 有必填/格式校验不通过时,请求根本不会发出(实测「必填项为空」那一次收不到任何请求);加上
novalidate 才会照样发出(reqme= 空值)。
<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 每个字段是一个带分隔符的分片,因此能携带二进制,代价是报文更大。
- 实测
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="提交">
|
<button> 里可以放 HTML(图标 + 文字),<input type="submit"> 只能显示 value 文本。
- 想「点击后先跑 JS 再决定是否提交」:监听
submit 事件,必要时 event.preventDefault(),或用 form.requestSubmit() 主动触发。
<label>:一定要用
1 2 3 4 5 6
| <label for="email">邮箱</label> <input type="email" id="email" name="email">
<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 可跳过。
|
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>。
- 里面的
< 也要转义(<),否则会被当标签解析——这是常见 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>:
解析结果是 <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 属性里要 &(如 ?a=1&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、未勾选的复选框分别会怎样体现在报文里。
参考资料