假设您已决定使用 Next.js 构建博客。像任何开发人员博主一样,您希望在文章中包含格式良好的带语法高亮的代码片段。也许您还想在代码片段中显示行号,甚至可能拥有突出显示特定代码行的功能。
本文将向您展示如何设置这些功能,以及一些关于如何实现这些其他功能的技巧和窍门。其中一些比您预期的要复杂。
先决条件
我们将使用 Next.js 博客入门 作为我们项目的基线,但相同的原则应该适用于其他框架。该仓库提供了清晰(且简单)的入门说明。搭建博客,我们开始吧!
我们在这里使用的另一个工具是 Prism.js,这是一个流行的语法高亮库,甚至在 CSS-Tricks 上也有使用。Next.js 博客入门使用 Remark 将 Markdown 转换为标记,因此我们将使用 remark-Prism.js 插件 来格式化我们的代码片段。
Prism.js 的基本集成
我们先将 Prism.js 集成到 Next.js 入门项目中。由于我们已经知道将使用 remark-prism 插件,因此首先需要使用您最喜欢的包管理器安装它
npm i remark-prism
现在进入 /lib
文件夹中的 markdownToHtml
文件,并启用 remark-prism
import remarkPrism from "remark-prism";
// later ...
.use(remarkPrism, { plugins: ["line-numbers"] })
根据您使用的 remark-html 版本,您可能还需要将其用法更改为 .use(html, { sanitize: false })
。
现在整个模块应该如下所示
import { remark } from "remark";
import html from "remark-html";
import remarkPrism from "remark-prism";
export default async function markdownToHtml(markdown) {
const result = await remark()
.use(html, { sanitize: false })
.use(remarkPrism, { plugins: ["line-numbers"] })
.process(markdown);
return result.toString();
}
添加 Prism.js 样式和主题
现在,让我们导入 Prism.js 需要用来为代码片段设置样式的 CSS。在 pages/_app.js
文件中,导入主要的 Prism.js 样式表,以及您要使用的主题的样式表。我使用的是 Prism.js 的“Tomorrow Night”主题,因此我的导入如下所示
import "prismjs/themes/prism-tomorrow.css";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
import "../styles/prism-overrides.css";
请注意,我还开始了一个 prism-overrides.css
样式表,我们可以在其中调整一些默认值。这在以后会很有用。现在,它可以保持为空。
有了这些,我们现在就有一些基本样式了。以下 Markdown 中的代码
```js
class Shape {
draw() {
console.log("Uhhh maybe override me");
}
}
class Circle {
draw() {
console.log("I'm a circle! :D");
}
}
```
…应该可以很好地格式化

添加行号
您可能已经注意到,我们生成的代码片段即使导入支持它的插件时也未显示行号,我们导入的插件就是 remark-prism。解决方法在 remark-prism README 中一目了然
请不要忘记在您的样式表中包含相应的 css。
换句话说,我们需要在生成的 <pre>
标签上强制添加 .line-numbers
CSS 类,我们可以这样做

有了这些,我们现在就可以显示行号了!

请注意,根据我拥有的 Prism.js 版本和选择的“Tomorrow Night”主题,我需要在上面开始的 prism-overrides.css
文件中添加以下内容
.line-numbers span.line-numbers-rows {
margin-top: -1px;
}
您可能不需要这样做,但这就是方法。我们现在有了行号!
突出显示行
我们的下一个功能需要更多工作。我们要实现的功能是突出显示或标记代码片段中的特定代码行。
有一个 Prism.js 行高亮插件;不幸的是,它没有集成到 remark-prism 中。该插件的工作原理是分析格式化代码在 DOM 中的位置,并根据这些信息手动突出显示行。这在 remark-prism 插件中是不可能的,因为插件运行时没有 DOM。毕竟,这是静态网站生成。Next.js 在构建步骤中运行我们的 Markdown 并生成 HTML 来呈现博客。所有这些 Prism.js 代码都在静态网站生成期间运行,此时没有 DOM。
但不要担心!有一个有趣的解决方法非常适合 CSS-Tricks:我们可以使用纯 CSS(以及一些 JavaScript)来突出显示代码行。
我明确地说,这需要不少工作。如果您不需要行高亮,那么可以跳过下一部分。但即使如此,它也可以作为展示可能性的有趣示例。
我们的基本 CSS
我们先将以下 CSS 添加到 prism-overrides.css
样式表中
:root {
--highlight-background: rgb(0 0 0 / 0);
--highlight-width: 0;
}
.line-numbers span.line-numbers-rows > span {
position: relative;
}
.line-numbers span.line-numbers-rows > span::after {
content: " ";
background: var(--highlight-background);
width: var(--highlight-width);
position: absolute;
top: 0;
}
我们提前定义了一些 CSS 自定义属性:背景颜色和高亮宽度。我们现在将它们设置为空值。但是,以后我们将使用 JavaScript 为要高亮的代码行设置有意义的值。
然后,我们将行号 <span>
设置为 position: relative
,以便我们可以添加具有绝对定位的 ::after
伪元素。我们将使用此伪元素来突出显示我们的代码行。
声明高亮的代码行
现在,让我们手动将 数据属性 添加到生成的 <pre>
标签中,然后在代码中读取该属性,并使用 JavaScript 调整上面的样式以突出显示特定的代码行。我们可以像以前添加行号那样完成此操作

这将导致我们的 <pre>
元素以 data-line="3,8-10"
属性呈现,其中代码片段中第 3 行和第 8-10 行将被高亮显示。我们可以用逗号分隔行号,或者提供范围。
让我们看看如何在 JavaScript 中解析它,并使高亮显示生效。
读取高亮的代码行
转到 components/post-body.tsx
。如果此文件是 JavaScript,您可以随意将其转换为 TypeScript (.tsx
),或者忽略所有类型。
首先,我们需要一些导入
import { useEffect, useRef } from "react";
我们需要向此组件添加一个 ref
const rootRef = useRef<HTMLDivElement>(null);
然后,将其应用于 root
元素
<div ref={rootRef} className="max-w-2xl mx-auto">
接下来的代码有点长,但它并没有做任何疯狂的事情。我将展示它,然后逐行分析。
useEffect(() => {
const allPres = rootRef.current.querySelectorAll("pre");
const cleanup: (() => void)[] = [];
for (const pre of allPres) {
const code = pre.firstElementChild;
if (!code || !/code/i.test(code.tagName)) {
continue;
}
const highlightRanges = pre.dataset.line;
const lineNumbersContainer = pre.querySelector(".line-numbers-rows");
if (!highlightRanges || !lineNumbersContainer) {
continue;
}
const runHighlight = () =>
highlightCode(pre, highlightRanges, lineNumbersContainer);
runHighlight();
const ro = new ResizeObserver(runHighlight);
ro.observe(pre);
cleanup.push(() => ro.disconnect());
}
return () => cleanup.forEach(f => f());
}, []);
我们运行一次效果,在内容完全渲染到屏幕上时。我们使用 querySelectorAll
来获取此 root
元素下的所有 <pre>
元素;换句话说,就是用户正在查看的博客文章。
对于每个元素,我们确保它下面有一个 <code>
元素,并且我们检查行号容器和 data-line
属性。这就是 dataset.line
所检查的。有关更多信息,请参阅 文档。
如果我们通过了第二个 continue
,那么 highlightRanges
就是我们之前声明的高亮显示集,在本例中是 "3,8-10"
,其中 lineNumbersContainer
是具有 .line-numbers-rows
CSS 类的容器。
最后,我们声明了一个 runHighlight
函数,它调用一个 highlightCode
函数,我将稍后展示。然后,我们设置了一个 ResizeObserver
,以便在博客文章大小发生变化时(例如,用户调整浏览器窗口大小)运行相同的函数。
highlightCode
函数
最后,让我们看看 highlightCode
函数。
function highlightCode(pre, highlightRanges, lineNumberRowsContainer) {
const ranges = highlightRanges.split(",").filter(val => val);
const preWidth = pre.scrollWidth;
for (const range of ranges) {
let [start, end] = range.split("-");
if (!start || !end) {
start = range;
end = range;
}
for (let i = +start; i <= +end; i++) {
const lineNumberSpan: HTMLSpanElement = lineNumberRowsContainer.querySelector(
`span:nth-child(${i})`
);
lineNumberSpan.style.setProperty(
"--highlight-background",
"rgb(100 100 100 / 0.5)"
);
lineNumberSpan.style.setProperty("--highlight-width", `${preWidth}px`);
}
}
}
我们获取每个范围并读取 <pre>
元素的宽度。然后我们遍历每个范围,找到相关的行号 <span>
,并为它们设置 CSS 自定义属性值。我们设置我们想要的任何高亮颜色,并将宽度设置为 <pre>
元素的总 scrollWidth
值。我保持简单并使用了 "rgb(100 100 100 / 0.5)"
,但您可以随意使用您认为最适合您博客的任何颜色。
这就是它的样子。

没有行号的行高亮
您可能已经注意到,到目前为止,所有这些都依赖于行号的存在。但是,如果我们想要高亮显示行,但不带行号呢?
实现此的一种方法是保持所有内容不变,并添加一个新选项,仅使用 CSS 隐藏这些行号。首先,我们将添加一个新的 CSS 类 .hide-numbers
。
```js[class="line-numbers"][class="hide-numbers"][data-line="3,8-10"]
class Shape {
draw() {
console.log("Uhhh maybe override me");
}
}
class Circle {
draw() {
console.log("I'm a circle! :D");
}
}
```
现在让我们添加 CSS 规则,以便在应用 .hide-numbers
类时隐藏行号。
.line-numbers.hide-numbers {
padding: 1em !important;
}
.hide-numbers .line-numbers-rows {
width: 0;
}
.hide-numbers .line-numbers-rows > span::before {
content: " ";
}
.hide-numbers .line-numbers-rows > span {
padding-left: 2.8em;
}
第一个规则撤消了我们基础代码向右的偏移,以便为行号腾出空间。默认情况下,我选择的 Prism.js 主题的填充为 1em
。行号插件将其增加到 3.8em
,然后使用绝对定位插入行号。我们所做的将填充恢复到 1em
默认值。
第二个规则获取行号的容器,并将其压缩为没有宽度。第三个规则擦除所有行号本身(它们使用 ::before
伪元素生成)。
最后一个规则只是将现在为空的行号 <span>
元素移回它们本应在的位置,以便高亮显示可以按照我们想要的方式定位。同样,对于我的主题,行号通常会添加 3.8em
的左侧填充,我们将其恢复为默认的 1em
。这些新样式添加了另外 2.8em
,因此事物回到了它们应该在的位置,但行号隐藏了。如果您使用不同的插件,您可能需要略微不同的值。
结果如下所示。

复制到剪贴板功能
在我们结束之前,让我们添加一个最后的修饰:一个按钮,允许我们亲爱的读者从我们的代码段中复制代码。这是一个不错的增强功能,可以省去人们手动选择和复制代码段的麻烦。
实际上相当简单。为此有一个 navigator.clipboard.writeText
API。我们将我们想要复制的文本传递给该方法,就是这样。我们可以在每个 <code>
元素旁边插入一个按钮,将代码的文本发送到该 API 调用以复制它。我们已经在修改这些 <code>
元素以高亮显示行,所以让我们在同一个地方集成我们的复制到剪贴板按钮。
首先,从上面的 useEffect
代码中,让我们添加一行。
useEffect(() => {
const allPres = rootRef.current.querySelectorAll("pre");
const cleanup: (() => void)[] = [];
for (const pre of allPres) {
const code = pre.firstElementChild;
if (!code || !/code/i.test(code.tagName)) {
continue;
}
pre.appendChild(createCopyButton(code));
注意最后一行。我们将在 <pre>
元素下方直接将我们的按钮追加到 DOM 中,该元素已经是 position: relative
,允许我们更轻松地定位按钮。
让我们看看 createCopyButton
函数的样子。
function createCopyButton(codeEl) {
const button = document.createElement("button");
button.classList.add("prism-copy-button");
button.textContent = "Copy";
button.addEventListener("click", () => {
if (button.textContent === "Copied") {
return;
}
navigator.clipboard.writeText(codeEl.textContent || "");
button.textContent = "Copied";
button.disabled = true;
setTimeout(() => {
button.textContent = "Copy";
button.disabled = false;
}, 3000);
});
return button;
}
很多代码,但大多数是样板代码。我们创建了按钮,然后为它提供一个 CSS 类和一些文本。当然,我们还创建了一个点击处理程序来执行复制。复制完成后,我们会更改按钮的文本并将其禁用几秒钟,以帮助为用户提供反馈,表明它已成功。
真正的工作在这行代码上。
navigator.clipboard.writeText(codeEl.textContent || "");
我们传递 codeEl.textContent
而不是 innerHTML
,因为我们只想要实际呈现的文本,而不是 Prism.js 添加的所有标记,以便以更好的方式格式化我们的代码。
现在让我们看看如何设置这个按钮的样式。我不是设计师,但这是我想到的。
.prism-copy-button {
position: absolute;
top: 5px;
right: 5px;
width: 10ch;
background-color: rgb(100 100 100 / 0.5);
border-width: 0;
color: rgb(0, 0, 0);
cursor: pointer;
}
.prism-copy-button[disabled] {
cursor: default;
}
看起来像这样。

它可以正常工作!它复制了我们的代码,甚至保留了格式(即换行符和缩进)!
总结
希望这对您有所帮助。Prism.js 是一个很棒的库,但它最初并非为静态网站编写。这篇文章向您介绍了一些技巧和窍门,可以弥合这种差距,并使其与 Next.js 网站良好地协同工作。
真正缺少的是一个真正的现场示例,这样我们就可以尝试该解决方案。图片对任何事情都没有帮助……
我很好奇地想尝试这种方法,因为我认为它会遇到大多数实现的相同问题。
更改字体大小会弄乱行高亮、行号和实际代码行的对齐方式。
如果代码行比可用空间长(换行或水平滚动),则高亮显示可能无法正确覆盖整行。
我还建议不要在项目的根目录导入
prism
css 文件,除非每个页面都使用 markdown。否则,您将使用大量未使用的 css 代码来膨胀很多页面,而 Prism 拥有大量的 CSS。此外,如果所有这些未使用的 CSS 影响了不包含 markdown 但使用pre
或code
HTML 标签的页面的样式,我不会感到惊讶……您好。我有点希望我把它与一个完全可用的独立仓库联系起来。事实上,我可能这周末会这样做。
如果您真的好奇,这一切都是来自我重新编写博客的工作,您可以在这里看到(正在进行中):https://github.com/arackaf/my-blog/tree/features/next-rewrite
也就是说,增加字体大小可以正常工作,即使它会导致水平滚动(整行都被正确高亮显示)。
有没有可能为此创建一个 Github 仓库?我认为从鸟瞰图的角度了解代码是如何工作的会更容易。
尽管如此,这篇文章还是很有见地的。我个人一直使用
highlight.js
,所以看到另一种(流行的)语法高亮显示器被使用很有趣。有点。这是来自我用 Next 重写博客的工作。正在进行中的版本在这里:https://github.com/arackaf/my-blog/tree/features/next-rewrite
谢谢!这对我很有效。
markdownToHtml
文件,在/lib
文件夹中的哪个位置?这可以正常工作,但是行号与代码没有水平对齐。有没有办法解决这个问题?