Old-School Front-End / 2026 ・ No.02
古法前端入门
第2讲 HTML 骨架 ── 用“有含义的标签”搭出申请列表
发布
不靠 AI,一行一行亲手写的“古法”前端入门。从第 2 讲开始,终于要动工了。
这次要做的是报销系统申请列表的骨架。不用 CSS,也不用 JavaScript,只用 HTML 来搭。
关键词是“有含义的标签”。即使外观一样,标签选得不同,页面内容传达给读屏软件、测试工具和 AI 的方式也截然不同。
最后,我们会把写好的 HTML 交给检查工具验一验。
本讲的目标
新井前辈,申请列表的页面我让 AI 做好了!你看,显示得好好的。
古井我看看……div、div、div、div。好一锅“div 汤”。
新井可是看起来没问题啊?
古井看起来是没问题。今天要讲的,是外表下面的“骨头”。
当前位置:第 2 讲
这次要把上一讲还是“工地”的 index.html,改建成申请列表页面。内容如下。
- 页头 ── 图标和站名、菜单(申请列表・审批・仪表盘)、当前登录的用户
- 页面主体 ── 大标题、新建申请的链接、本月汇总、申请一览表
- 页脚 ── 版权信息
因为还不用 CSS,外观会很朴素(第 4 讲再给它穿衣服)。正因如此,可以把注意力集中在选哪个标签上。
标签・元素・属性
先认识各部分的名字。HTML 是用标签把文字夹起来,标明“这是什么”的语言。
标签的解剖图
- 元素 ── 开始标签、内容、结束标签合在一起。叫作“a 元素”“table 元素”等
- 属性 ── 给元素附加的信息。在开始标签里以 名称="值" 的形式书写
- 空元素 ── 没有内容的元素,也不写结束标签(img、br、input、meta 等)
任何元素都能加的属性里,最常用的是这三个。
| 属性 | 含义 | 示例 |
|---|---|---|
| id | 页面里唯一的名字。同一个 id 不能用两次 | <table id="list"> |
| class | 分组的名字,加在几个元素上都行。CSS(第 4 讲)和 JavaScript(第 8 讲)定位元素时会用到 | <td class="amount"> |
| lang | 内容的语言。通常只在 html 元素上写一次 | <html lang="zh-CN"> |
另一个要点是嵌套规则。把元素放进元素里时,要像盒子套盒子一样,后打开的标签先关闭。
嵌套规则
还记得上一讲删掉 </h1> 后整页都变成标题的事吗?即使有错,浏览器也不报错,而是按自己的理解重组这棵树再显示。它的“理解”是否符合你的意图,可没人保证。
页面的区块(地标)
页面通常分成页头、菜单、主体和页脚。HTML 为它们各自准备了专用标签。这些区块叫作地标(landmark)。
div 汤,与有含义的标签
| 标签 | 含义 | 本连载里的用法 |
|---|---|---|
| header | 页面(或区块)的头部 | 图标・站名・用户名 |
| nav | 主要导航(菜单) | 通往申请列表・审批・仪表盘的链接 |
| main | 本页的主角。每页 1 个 | 标题・汇总・一览表 |
| section | 带标题的一组内容 | “本月汇总”“申请一览” |
| article | 本身就能独立成立的内容 | (博客文章、一条新闻等) |
| aside | 正文旁边的补充 | (第 13 讲的仪表盘里也许会用) |
| footer | 页面(或区块)的尾部 | 版权信息・联系方式 |
| div | 没有含义的盒子,只为外观而分组时使用 | 没有合适标签时的最后手段 |
那么,把只用 div 做的页面和用有含义的标签做的页面放在一起比较一下。
单看屏幕,几乎一模一样。可在读屏软件等辅助技术眼里,二者截然不同。我们用测试工具 Playwright 取出了浏览器根据页面生成的“无障碍树”(实际输出,较长部分已省略)。
# ① div 汤:几乎只是“一张图片,加一长串文字”
- img
- text: 报销系统 • 申请列表 • 审批 • 仪表盘 新井 太郎(申请人) 申请列表 + 新建申请 本月汇总 …
# ② 有含义的标签:区块、标题、列表、表格都原样传达
- banner:
- paragraph:
- link "报销系统":
- /url: index.html
- navigation:
- list:
- listitem:
- link "申请列表":
- /url: index.html
- listitem:
- link "审批":
- /url: approve.html
- listitem:
- link "仪表盘":
- /url: dashboard.html
- paragraph: 新井 太郎(申请人)
- main:
- heading "申请列表" [level=1]
- paragraph:
- link "+ 新建申请":
- /url: form.html
- heading "本月汇总" [level=2]
- list:
- listitem: 本月申请:4 笔
- listitem:
- text: 待审批金额:
- strong: 32,400 日元
- listitem: 被退回:1 笔
- heading "申请一览" [level=2]
- table "2026 年 10 月的申请(按时间倒序)":
…
在 ② 里,banner(header)、navigation(nav)、main、标题层级、列表项数,乃至表格的表头都传达出来了。而在 ① 里,连“链接”这个角色都消失了(只是让 span 能点击,所以用键盘的 Tab 键也到不了)。
- 读屏软件的用户 ── 很多人会在标题之间跳转来把握整个页面(WebAIM 的调查)。地标也能同样当作“目录”使用。遇到 div 汤,就只能从头听到尾
- 测试工具 ── 开发指南Playwright 入门与实战介绍的 getByRole('navigation') 这类定位方式,只有在用了有含义的标签时才能用
- AI 工具 ── 操作浏览器的 AI,同样以这些角色信息为线索来读页面。对人友好的页面,对机器也友好
古井div 就是个“无名盒子”。里面装的是什么,从外面谁也看不出来。
新井那让 AI 写的时候,加一句“用有含义的标签”就行了吧。……至于写出来的对不对,还得自己读、自己确认。
古井开窍了嘛。
DOM 树
第 1 讲说过,浏览器会读取 HTML,生成一棵叫作 DOM 的树。把本讲的页面画成树,就是下面这样。
申请列表页面的 DOM(主要部分)
把 HTML 的缩进对齐着写,代码的形状就是树的形状。缩进不是装饰,而是用眼睛确认树形的工具。在浏览器里右键点击元素,选择“检查”,就能在开发者工具的 Elements 标签页里看到这棵真实的树。
以后,CSS 要指定“给树的哪根枝条穿衣服”,JavaScript 要指定“让树的哪根枝条动起来”。先把树搭得干净整齐,是让后半程轻松的最快捷径。
标题・段落・列表
标题从 h1 到 h6 共 6 级。关键在于按文章结构来选,而不是按大小来选。
标题就是目录
HTML 规范里曾经写有一种根据 section 的嵌套自动决定标题层级的机制(大纲算法),但没有任何浏览器实现过,2022 年被从规范中删除。如今的规矩是:标题层级要自己正确地标。
| 标签 | 含义 | 在申请列表里 |
|---|---|---|
| h1〜h6 | 标题(数字越小,范围越大) | h1“申请列表”,h2“本月汇总”“申请一览” |
| p | 段落 | 站名、用户名、新建申请的链接 |
| ul / ol / li | 无序列表/有序列表/其中一项 | 菜单、本月汇总 |
| strong | 重要(加重语气) | 待审批金额 |
| em | 强调(改变语气) | (本讲不用) |
| small | 注释、版权等小字 | 页脚的版权信息 |
| time | 日期或时间。在 datetime 属性里写机器可读的格式 | 申请日期 |
菜单用 ul 来写,是因为它是“有 3 个项目的一组列表”。读屏软件会先告诉你“列表,3 项”。如果用 <br> 换行来排列,这个信息就传达不出去了。
搭建表格
申请一览是有行(一笔申请)有列(申请日期・事由・金额・状态)的数据。这类数据要用 table。
表格的组成部分
<table>
<caption>2026 年 10 月的申请(按时间倒序)</caption>
<thead>
<tr>
<th scope="col">编号</th>
<th scope="col">申请日期</th>
<!-- …事由・金额(日元)・状态… -->
</tr>
</thead>
<tbody>
<tr>
<td>0007</td>
<td><time datetime="2026-10-16">2026-10-16</time></td>
<!-- …每行的格数与表头相同… -->
</tr>
</tbody>
</table>
古井想当年,页面布局全都用 table 来搭。为了留出空隙,还要铺满透明的小图片。
新井这也太“古法”了吧。
古井所以我才要说:表格只给表格用。布局是 CSS 的事(第 5 讲)。
- 状态用文字写 ── 以后就算给“审批中”上黄色、“已退回”上红色,也不要只靠颜色,文字要留着。这样色觉辨识有困难的人、黑白打印时也看得懂
- 金额列 ── 在表头写上“金额(日元)”,每格只写数字就行。右对齐交给 CSS(第 4 讲)
- 行数不要写死 ── 现在是手写 4 行,从第 10 讲起会读取数据,用 JavaScript 生成行
链接与图片
链接用 a 元素的 href,图片用 img 元素的 src 来写目标(文件的位置)。写法有三种。
链接地址的写法
| 写法 | 示例 | 用途 |
|---|---|---|
| 相对路径 | form.html、img/logo.svg、../index.html | 站内链接。从当前文件出发的路线 |
| 绝对 URL | https://getbootstrap.com/ | 其他网站 |
| 页内链接 | #list | 跳到同一页面中 id="list" 的位置 |
用 target="_blank" 在新标签页打开链接时,以前为了安全必须同时写上 rel="noopener",现在主流浏览器都会自动加上。不过在业务系统里,标签页莫名其妙变多往往招人嫌。新标签页还是只在打开外部网站或 PDF 时用吧。
图片千万别忘了 alt(替代文字)。看不到图片的人,或图片加载失败时,传达的就是这段文字。
| 图片的种类 | alt 的写法 | 示例 |
|---|---|---|
| 传达含义的图片 | 写出图片表达的内容 | alt="收据:出租车费 2,400 日元" |
| 装饰性图片 | 留空(写 alt="",而不是省略) | 分隔花纹等 |
| 旁边有同样文字的图片 | 留空(同一内容不必读两遍) | 本讲的图标。旁边就写着“报销系统” |
width 和 height 也要写上。这样图片还没加载时就预留了位置,可以防止加载过程中页面突然跳动。
搭出申请列表
把前面的部件组合起来,就是本讲的成品。把上一讲的 index.html 整个改写成下面这样。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>申请列表 | 报销系统</title>
</head>
<body>
<header>
<p><a href="/"><img src="img/logo.svg" alt="" width="32" height="32"> 报销系统</a></p>
<nav>
<ul>
<li><a href="/">申请列表</a></li>
<li><a href="approve.html">审批</a></li>
<li><a href="dashboard.html">仪表盘</a></li>
</ul>
</nav>
<p>新井 太郎(申请人)</p>
</header>
<main>
<h1>申请列表</h1>
<p><a href="form.html">+ 新建申请</a></p>
<section>
<h2>本月汇总</h2>
<ul>
<li>本月申请:4 笔</li>
<li>待审批金额:<strong>32,400 日元</strong></li>
<li>被退回:1 笔</li>
</ul>
</section>
<section>
<h2>申请一览</h2>
<table>
<caption>2026 年 10 月的申请(按时间倒序)</caption>
<thead>
<tr>
<th scope="col">编号</th>
<th scope="col">申请日期</th>
<th scope="col">事由</th>
<th scope="col">金额(日元)</th>
<th scope="col">状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>0007</td>
<td><time datetime="2026-10-16">2026-10-16</time></td>
<td><a href="detail.html">大阪出差(交通费・住宿费)</a></td>
<td>32,400</td>
<td>审批中</td>
</tr>
<tr>
<td>0006</td>
<td><time datetime="2026-10-15">2026-10-15</time></td>
<td><a href="detail.html">拜访客户的电车费</a></td>
<td>3,830</td>
<td>已批准</td>
</tr>
<tr>
<td>0005</td>
<td><time datetime="2026-10-09">2026-10-09</time></td>
<td><a href="detail.html">购买技术书籍</a></td>
<td>4,180</td>
<td>已退回</td>
</tr>
<tr>
<td>0004</td>
<td><time datetime="2026-10-02">2026-10-02</time></td>
<td><a href="detail.html">技术沙龙报名费</a></td>
<td>3,000</td>
<td>已报销</td>
</tr>
</tbody>
</table>
</section>
</main>
<footer>
<p><small>© 2026 KIJI 报销系统(练习用)</small></p>
</footer>
</body>
</html>
- head 里 ── 在上一讲的 charset 和 title 之外加了 meta name="viewport",让手机按屏幕宽度显示(第 5 讲详解)。title 按“页面名 | 站名”的顺序写,开了很多标签页时也容易分辨
- 同样是“报销系统” ── 在 header 里它是站名,所以用 p;main 里的页面名“申请列表”才是 h1。拿不准哪个该用 h1 时,就问自己“这个页面的题目是哪个”
- 事由的链接目标 ── 现在都指向 detail.html。至于显示哪一笔申请的详情,第 10〜12 讲会在 URL 里带上编号来传递
菜单里的“审批”“仪表盘”,以及链接目标 form.html、detail.html 都还不存在。点进去会显示“找不到文件”,这是正常的。我们会依次把它们建起来。
写完就检查
浏览器会把错误默默吞下,所以HTML 的语法要用专门的检查工具来确认。W3C 公开的 Nu Html Checker(validator.w3.org/nu),交给它文件或贴上代码就能用。我们检查了一段故意写错 3 处的 HTML。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>申请列表</title>
</head>
<body>
<header>
<p><img src="img/logo.svg"> 报销系统</p>
</header>
<main>
<h1 id="title">申请列表</h1>
<p><strong><a href="form.html">+ 新建申请</strong></a></p>
<h2 id="title">申请一览</h2>
</main>
</body>
</html>
结果如下(实际输出,长行已折行)。
Error: An “img” element must have an “alt” attribute, except under
certain conditions. For details, consult guidance on
providing text alternatives for images. (第 9 行)
Error: End tag “strong” violates nesting rules. (第 13 行)
Error: No “a” element in scope but a “a” end tag seen. (第 13 行)
Error: Duplicate ID “title”. (第 14 行)
Warning: The first occurrence of ID “title” was here. (第 12 行)
Document checking completed.
| 指出的问题 | 含义 | 改法 |
|---|---|---|
| img 没有 alt | 缺少图片的替代文字 | 图标旁边有站名,所以写 alt="" |
| strong 的结束标签违反嵌套规则 | 在 a 之前先关了 strong(盒子交叉) | 按 </a></strong> 的顺序关闭 |
| id“title”重复 | 本应唯一的 id 出现在两处 | 改掉其中一个 id,不需要的话就删掉 |
把第八章的成品交给同一个工具,会显示“No errors found(没有错误)”。
AI 写的 HTML,也建议先过一遍检查。AI 很少犯语法错误,但 div 汤、标题跳级、忘写 alt 却很常见。检查工具能发现的只是语法错误,“这个标签在含义上对不对”,只能靠人来读、来确认。
结 — 一页总结与下期预告
- HTML 是用标签把内容夹起来、标明“这是什么”的语言。开始标签、内容、结束标签合起来是一个元素。属性写作 名称="值"。嵌套时,后打开的先关闭。
- 用 header・nav・main・footer 等地标划分页面。div 是“无名盒子”,是最后的手段。
- 即使外观相同,有含义的标签会把页面结构原样传达给读屏软件、测试工具和 AI。
- 标题不跳级、按结构选。表格只用于有行有列的数据,并加上 caption・thead・th scope。图片要写 alt。
- 写完用 Nu Html Checker 检查,AI 写的 HTML 也一样。含义对不对,靠人来读、来确认。
新井骨架搭好了!下一步是点了“新建申请”之后的页面吧。
古井没错,第 3 讲是申请表单。输入框、选项、上传文件,这可是业务系统的重头戏。
参考资料
- WHATWG — HTML Living Standard
- W3C — W3C and WHATWG to work together to advance the open Web platform (2019)
- MDN — HTML elements reference
- MDN — <table>: The Table element
- MDN — <img>: The Image Embed element
- Can I use — a: target="_blank" implies rel="noopener"
- Bruce Lawson — Why the HTML Outlining Algorithm was removed from the spec (2022)
- WebAIM — Screen Reader User Survey
- W3C — Nu Html Checker
- Playwright — Snapshot testing (ARIA snapshots)
页面截图是笔者制作的演示页面,于 2026 年 10 月 17 日用云端的 Chromium(Playwright)拍摄。无障碍树是 Playwright 的 ariaSnapshot() 的实际输出(较长部分已省略),检查结果是在本地运行 Nu Html Checker(vnu-jar 26.10.12)的实际输出。target="_blank" 的行为依据 Can I use 的兼容性表,大纲算法的删除依据 Bruce Lawson 的解说(2022 年)。登场人物均为虚构。
古法前端入门 第2讲 — HTML 骨架:用“有含义的标签”搭出申请列表 / 2026.10