HTML 里显示代码并带行号:pre 与 CSS 计数器的分工

编程狮(w3cschool.cn) 2026-10-28 10:57:03 浏览数 (34)
反馈

你有没有遇到过:一段代码贴进 <pre> 后缩进全乱了,行号只能手动一排排敲?这种事每个写技术文章或做文档页的人都踩过。一句话结论:<pre> 负责“原样保留空格和换行”,<code> 负责“这是代码”的语义,两者分工明确;而行号最干净的做法是用 CSS 计数器(counter-reset + counter-increment + ::before 的 content: counter())自动生成,而不是在 HTML 里手写数字。本文把 <pre>、<code> 的边界,以及 CSS 计数器生成行号的完整写法一次讲清,并点出“长行把行号挤歪”“用 JS 插行号导致复制时带数字”这两个常见坑,照着做你就能既好看又好复制。读完你会发现,结构归结构、表现归表现,这条原则在代码块上同样成立,别再用脚本去数行号了。

代码带行号pre 与 CSS 计数器怎么分工

一、pre 与 code 各自管什么

<pre> 是“预格式化”容器,它会保留你写进去的所有空格和换行,不自动折行——这是代码能对齐的根本。但 <pre> 不表示“这是代码”,语义上它只是预格式文本。整体结构建议先看 CSS 教程 把盒模型与文本排版基础补一补,再看本文的具体组合方式会更顺,也能理解为什么换行控制要交给特定的属性而不是全局样式。

<code> 才是“这是代码片段”的语义标签,通常嵌在 <pre> 里使用,形成 <pre><code>...</code></pre> 的经典结构。它们的适用边界很清楚:<pre> 管“怎么显示空白”,<code> 管“这是什么内容”。混用或漏用其一,都会导致样式或无障碍语义出问题,比如屏幕阅读器读不出这是代码块。

举个具体例子:如果你把代码直接丢进 <div> 而不用 <pre>,缩进和换行会被 HTML 当普通空白折叠,代码瞬间变成一坨;如果反过来只用 <pre> 忘了 <code>,视觉效果对了,但辅助技术不知道“这是代码”,对视障用户不友好。两者配合才是标准写法。对比一下:white-space: pre 是 <pre> 的默认行为,而 <code> 本身不改空白,只改语义——很多人误以为 <code> 也会保留缩进,其实是 <pre> 在背后出力。

实际项目里还有一个边界:当代码里有 <、> 这类字符,必须写成实体 <、>,否则浏览器会当成标签解析。这一步常被新手漏掉,导致代码块渲染出错甚至把页面结构搞乱。把“转义特殊字符”当成贴代码前的固定动作,能省掉不少奇怪的渲染问题。

二、为什么缩进会被“吃掉”

把下面这段贴进普通 <div>,缩进一定丢;贴进 <pre> 就原样保留:

<pre><code>def hello():
    print("hi")   # 这一行的前导空格会被保留
    return 1</code></pre>

这里缩进能被原样保留,是因为 pre 的功劳。样式细节可查 CSS 参考手册,里面列出了 white-space 各取值对换行的真实影响,排查“缩进乱了”时直接对照即可,不必凭记忆猜,也不会被网上的过时写法带偏。配置好这个结构后,运行页面看预期效果,检查代码块是否保留缩进,若不符合预期多半是 white-space 被别处覆盖。

顺带说一个排错经验:如果你发现 <pre> 里的缩进还是乱了,第一反应应该是查“有没有别处给 pre 或 code 设了 white-space: normal”。很多 CSS 重置(reset)会全局改空白行为,把 <pre> 的默认 pre 覆盖掉,于是缩进失效。解决办法是给代码块单独写 white-space: pre 或 pre-wrap 强制锁回来,而不是去调 HTML。

pre-wrap 和 pre 的适用边界也不同:pre 不折行,超长行会撑宽容器;pre-wrap 会在必要时折行,更适合长代码。选哪个取决于你愿不愿意让用户横向滚动。演示短片段用 pre 更原汁原味,贴长日志用 pre-wrap 体验更好。

三、行号用 CSS 计数器最干净

行号用 CSS 计数器最干净。思路是:给 pre 设一个计数器,每出现一行(::before 配合 counter-increment)就加一,并用 content: counter(行号) 显示。进阶美化可参考 Css Secrets 教程 里关于代码块的处理技巧,能做出更精致的边距与配色,让行号不只是功能性的数字而是一块好看的装饰。

pre {
  counter-reset: line;
  white-space: pre;
  overflow-x: auto;
}
pre code > span.line {
  counter-increment: line;
  display: block;
}
pre code > span.line::before {
  content: counter(line);
  margin-right: 12px;
  color: #999;
}

这段的性能开销极低,纯 CSS 渲染,不依赖脚本;对比之下,用 JS 在每行前插 <span> 行号,虽然也能跑,却会让“复制代码”时把数字一起选进去,读者粘贴后还得手动删,体验明显更差。

这里有个细节容易踩:计数器必须放在正确的元素上。counter-reset 要放在“每一行都从它开始计数的容器”(通常是 pre),而 counter-increment 要放在“每一行对应的元素”上(比如每行包一个 .line)。如果两者放反,行号要么全是 1,要么根本不出现。

性能上,纯 CSS 计数器几乎零开销,比 JS 方案省心。但要注意:如果你用 ::before 输出行号,复制代码时这些数字是不会被选中的(伪元素不参与选区),这正好是我们要的效果。若用 JS 插 <span> 行号,复制就会带数字——这就是前面说的坑。所以除非有特殊交互需求,否则 CSS 计数器是首选。

四、分工原则与易踩的坑

关于“什么时候该用 CSS 计数器、什么时候该上 JS”,更系统的取舍可以看 表格宽度失效,它同样是在讲“结构归结构、表现归表现”的分工,思路是相通的,照着这套原则去设计代码块通常不会出大错。

两个最易踩的坑:第一,长行不折行会把行号列整体挤歪,解决是给 pre 加 overflow-x: auto 让长行横向滚动,而不是强行 white-space: normal(那会毁掉缩进);第二,用 JS 插行号后复制带数字,解决是直接用上面的纯 CSS 方案。这两条都属于排错经验,提前知道能省下不少返工时间。

pre 与 code 分工的结构示意 CSS 计数器生成行号的步骤

五、验证清单(前置·操作·验证·失败处理)

前置:确认你的代码放在 <pre><code> 结构里,且外层没有覆写 white-space。配置:按“先给 pre 加 counter-reset,再给每行包 <span class="line"> 并在 ::before 用 counter() 输出”的顺序写。运行:在浏览器打开页面,检查行号是否从 1 开始递增。预期:正常情况每行左侧出现连续数字,且横向滚动时长行不会把行号列挤歪。失败处理:若行号不出现,检查 counter-increment 是不是放错了元素(必须放在被计数的那一层);若复制带数字,说明用了 JS 插行号,改用纯 CSS 方案即可。修复:把 counter-reset 移到 pre、把 counter-increment 移到 .line。验证:改完再复制一段代码粘贴到编辑器,确认只有代码本身、没有行号数字,分工即正确。

总结

<pre> 负责原样保留空格与换行,<code> 负责声明“这是代码”的语义,两者职责分离;行号最干净的实现是 CSS 计数器(counter-reset + counter-increment + ::before 的 content: counter()),不依赖 JS。两个主要坑是长行挤歪行号列、以及 JS 插行号导致复制带数字。记住“结构归结构、表现归表现”,代码块的美观和可复制性就能兼得,不必再手敲行号或用脚本数行。

常见问题

Q:为什么我的行号从不对齐或根本不显示?

多半是 counter-reset 和 counter-increment 不在正确的元素上:counter-reset 应放在 pre,counter-increment 应放在每一行(如 .line)。两者层级错开,计数器就计不上数。

Q:复制代码时把行号也一起选进去了怎么办?

说明你用了 JS 在每行前插入行号节点。改用纯 CSS 的 ::before 方案,行号是伪元素、不参与选区,复制出来的就是干净代码。

Q:长行代码把行号列挤歪了怎么处理?

给 pre 加 overflow-x: auto 让超长行横向滚动,同时保留 white-space: pre,不要为了“不滚动”而改成 normal,那样会破坏缩进对齐。

延伸学习

0 人点赞