Old-School Front-End / 2026 ・ No.02

古法前端入门

第2讲 HTML 骨架 ── 用“有含义的标签”搭出申请列表

发布

没有加 CSS、只有 HTML 的申请列表页面。从上到下依次是图标和“报销系统”链接、项目符号菜单(申请列表・审批・仪表盘)、用户名、大标题“申请列表”、新建申请的链接、本月汇总的列表、2026 年 10 月的申请表格和版权信息
本讲的目标:只用 HTML 搭出的申请列表。还没有任何装饰,但内容的“骨架”已经齐全(笔者的演示页面,用云端的 Chromium 截图。2026 年 10 月 17 日。浏览器边框是为了说明而后加的)

不靠 AI,一行一行亲手写的“古法”前端入门。从第 2 讲开始,终于要动工了。
这次要做的是报销系统申请列表的骨架。不用 CSS,也不用 JavaScript,只用 HTML 来搭。
关键词是“有含义的标签”。即使外观一样,标签选得不同,页面内容传达给读屏软件、测试工具和 AI 的方式也截然不同。
最后,我们会把写好的 HTML 交给检查工具验一验。

一

本讲的目标

新井前辈,申请列表的页面我让 AI 做好了!你看,显示得好好的。

古井我看看……div、div、div、div。好一锅“div 汤”。

新井可是看起来没问题啊?

古井看起来是没问题。今天要讲的,是外表下面的“骨头”。

当前位置:第 2 讲

1 准备 2 HTML 骨架 3 表单 4 CSS 基础 5 布局 6 Bootstrap 7 JS 基础 8 DOM 与事件 9 输入校验 10 通信 11 登录 12 审批流程 13 统计与打印 14 jQuery 15 收尾 当前位置 准备 HTML CSS JavaScript 实战 收尾

这次要把上一讲还是“工地”的 index.html,改建成申请列表页面。内容如下。

  • 页头 ── 图标和站名、菜单(申请列表・审批・仪表盘)、当前登录的用户
  • 页面主体 ── 大标题、新建申请的链接、本月汇总、申请一览表
  • 页脚 ── 版权信息

因为还不用 CSS,外观会很朴素(第 4 讲再给它穿衣服)。正因如此,可以把注意力集中在选哪个标签上。

二

标签・元素・属性

先认识各部分的名字。HTML 是用标签把文字夹起来,标明“这是什么”的语言。

标签的解剖图

<a href="form.html"> + 新建申请 </a> 属性 名称="值" 开始标签 内容 结束标签 合起来,才是一个“元素” <img src="logo.svg" alt=""> 空元素 没有内容,也没有结束标签 img・br・input・meta 标签名习惯用小写。属性值用 " " 括起来
  • 元素 ── 开始标签、内容、结束标签合在一起。叫作“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">

另一个要点是嵌套规则。把元素放进元素里时,要像盒子套盒子一样,后打开的标签先关闭。

嵌套规则

○ 盒子套盒子 p strong a 链接文字 <p><strong><a>…</a></strong></p> 后打开的标签先关闭 × 盒子交叉 strong a <strong><a>…</strong></a> strong 关了,a 却还开着 套娃原则:按打开的相反顺序关闭。一旦交叉,浏览器会自行重组

还记得上一讲删掉 </h1> 后整页都变成标题的事吗?即使有错,浏览器也不报错,而是按自己的理解重组这棵树再显示。它的“理解”是否符合你的意图,可没人保证。

三

页面的区块(地标)

页面通常分成页头、菜单、主体和页脚。HTML 为它们各自准备了专用标签。这些区块叫作地标(landmark)。

div 汤,与有含义的标签

div 汤(什么是什么?) <div> <div> <div> <div> <div> <div> <div> <div> <div> <div> <div> <div> <div> 有含义的标签(角色清楚) <header> banner <main> main <footer> contentinfo <nav> navigation <h1> <section> <h2> <ul> <section> <h2> <table> 右侧的小号英文是传达给读屏软件等的“角色(role)”。div 没有角色
标签含义本连载里的用法
header页面(或区块)的头部图标・站名・用户名
nav主要导航(菜单)通往申请列表・审批・仪表盘的链接
main本页的主角。每页 1 个标题・汇总・一览表
section带标题的一组内容“本月汇总”“申请一览”
article本身就能独立成立的内容(博客文章、一条新闻等)
aside正文旁边的补充(第 13 讲的仪表盘里也许会用)
footer页面(或区块)的尾部版权信息・联系方式
div没有含义的盒子,只为外观而分组时使用没有合适标签时的最后手段

那么,把只用 div 做的页面和用有含义的标签做的页面放在一起比较一下。

左右并排的两个页面。1 只用 div 做的页面,2 用有含义的标签做的页面。标题、列表和链接的外观几乎一样
① 只靠 div 和外观设置“做得像样”的页面,② 用有含义的标签做的页面。单看屏幕,几乎分不出来(笔者的演示页面,用云端的 Chromium 截图。2026 年 10 月 17 日。浏览器边框是为了说明而后加的)

单看屏幕,几乎一模一样。可在读屏软件等辅助技术眼里,二者截然不同。我们用测试工具 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 head 不显示在屏幕上的信息 meta, title 字符编码、标题 body 显示在屏幕上的内容 header 页头(站名・菜单・用户) p, nav, p 站名 / 菜单 / 用户名 main 本页的主角(每页 1 个) h1 大标题“申请列表” p > a 新建申请的链接 section 本月汇总(h2 和 ul) section 申请一览(h2 和 table) table 申请表格 footer 页脚(版权信息)

把 HTML 的缩进对齐着写,代码的形状就是树的形状。缩进不是装饰,而是用眼睛确认树形的工具。在浏览器里右键点击元素,选择“检查”,就能在开发者工具的 Elements 标签页里看到这棵真实的树。

POINT

以后,CSS 要指定“给树的哪根枝条穿衣服”,JavaScript 要指定“让树的哪根枝条动起来”。先把树搭得干净整齐,是让后半程轻松的最快捷径。

五

标题・段落・列表

标题从 h1 到 h6 共 6 级。关键在于按文章结构来选,而不是按大小来选。

标题就是目录

○ 不跳级 h1 申请列表 h2 本月汇总 h2 申请一览 × 按外观挑选 h1 申请列表 h4 本月汇总 h4 申请一览 ・标题层级(h1〜h6)按文章结构来选。大小可以用 CSS 改(第 4 讲) ・读屏软件的用户常常在标题之间跳转,以把握页面内容 ・h1 只放一个、作为页面的题目,最容易理解

HTML 规范里曾经写有一种根据 section 的嵌套自动决定标题层级的机制(大纲算法),但没有任何浏览器实现过,2022 年被从规范中删除。如今的规矩是:标题层级要自己正确地标。

标签含义在申请列表里
h1〜h6标题(数字越小,范围越大)h1“申请列表”,h2“本月汇总”“申请一览”
p段落站名、用户名、新建申请的链接
ul / ol / li无序列表/有序列表/其中一项菜单、本月汇总
strong重要(加重语气)待审批金额
em强调(改变语气)(本讲不用)
small注释、版权等小字页脚的版权信息
time日期或时间。在 datetime 属性里写机器可读的格式申请日期

菜单用 ul 来写,是因为它是“有 3 个项目的一组列表”。读屏软件会先告诉你“列表,3 项”。如果用 <br> 换行来排列,这个信息就传达不出去了。

六

搭建表格

申请一览是有行(一笔申请)有列(申请日期・事由・金额・状态)的数据。这类数据要用 table。

表格的组成部分

2026 年 10 月的申请(caption) 编号 日期 事由 金额 0007 10-16 大阪出差 32,400 0006 10-15 电车费 3,830 0005 10-09 技术书籍 4,180 caption thead tbody th tr td ・table = 整个表格,tr = 一行,th = 表头单元格,td = 数据单元格 ・用 thead 和 tbody 区分“表头行”和“内容行”。caption 是表格标题 ・th 的 scope="col" 表示“这一列的标题”。读屏时能读出每格对应的标题 ・表格是给有行有列的数据用的。拿来做页面布局是大忌
<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 来写目标(文件的位置)。写法有三种。

链接地址的写法

keihi/ index.html form.html img/ logo.svg help/ faq.html ①② 从 index.html 写,③ 从 faq.html 写 1 href="form.html" 同一文件夹,只写文件名 2 src="img/logo.svg" 进入下级文件夹写“文件夹名/” 3 href="../index.html" “../”表示上一级文件夹 ・链接到其他网站,从 https:// 开始写全(绝对 URL) ・跳到同一页面内的位置用 #id 例:href="#list" 跳到 id="list" 的元素 以 index.html 为基准写“相对路径”,整个文件夹搬家也不会坏
写法示例用途
相对路径form.html、img/logo.svg、../index.html站内链接。从当前文件出发的路线
绝对 URLhttps://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>
给申请列表页面加上红框和编号。1 header,2 其中的 nav,3 h1 标题,4 新建申请的链接,5 本月汇总的 section,6 申请 table,7 footer
完成的骨架与用到的标签:① header(图标・站名・用户),② nav(菜单),③ h1,④ 新建申请的链接,⑤ 汇总的 section,⑥ table,⑦ footer(笔者的演示页面,用云端的 Chromium 截图。2026 年 10 月 17 日。浏览器边框是为了说明而后加的)
  • 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(没有错误)”。

NOTE

AI 写的 HTML,也建议先过一遍检查。AI 很少犯语法错误,但 div 汤、标题跳级、忘写 alt 却很常见。检查工具能发现的只是语法错误,“这个标签在含义上对不对”,只能靠人来读、来确认。

结 — 一页总结与下期预告

  1. HTML 是用标签把内容夹起来、标明“这是什么”的语言。开始标签、内容、结束标签合起来是一个元素。属性写作 名称="值"。嵌套时,后打开的先关闭。
  2. 用 header・nav・main・footer 等地标划分页面。div 是“无名盒子”,是最后的手段。
  3. 即使外观相同,有含义的标签会把页面结构原样传达给读屏软件、测试工具和 AI。
  4. 标题不跳级、按结构选。表格只用于有行有列的数据,并加上 caption・thead・th scope。图片要写 alt。
  5. 写完用 Nu Html Checker 检查,AI 写的 HTML 也一样。含义对不对,靠人来读、来确认。

新井骨架搭好了!下一步是点了“新建申请”之后的页面吧。

古井没错,第 3 讲是申请表单。输入框、选项、上传文件,这可是业务系统的重头戏。

参考资料

页面截图是笔者制作的演示页面,于 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

← 返回文章列表