所以,你已经决定用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
现在进入markdownToHtml 文件,在/lib 文件夹中,打开 remark-prism。
import remarkPrism from "remark-prism";
// later ...
.use(remarkPrism, { plugins: ["line-numbers"] })
根据你所使用的review-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的 "明晚 "主题,所以我的导入看起来像这样:
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 时导入了支持行号的插件。解决方案就藏在review-prism的README中。
不要忘记在你的样式表中包含适当的css。
换句话说,我们需要在生成的<pre> 标签上强制添加一个.line-numbers CSS类,我们可以这样做:

这样一来,我们现在就有了行号了!

注意,根据我所拥有的Prism.js版本和我选择的 "明晚 "主题,我需要将其添加到我们上面开始的prism-overrides.css 文件中:
.line-numbers span.line-numbers-rows {
margin-top: -1px;
}
你可能不需要这个,但你有了它。我们有了行号!
突出显示行
我们的下一个功能将是更多的工作。这就是我们希望能够突出显示,或者叫出代码片段中的某些行。
有一个Prism.js行高亮插件;不幸的是,它没有与review-prism集成。该插件通过分析格式化的代码在DOM中的位置来工作,并根据该信息手动高亮行。这在 remark-prism 插件中是不可能的,因为在插件运行时并没有 DOM。这毕竟是静态网站的生成。Next.js正在通过构建步骤运行我们的Markdown,并生成HTML来渲染博客。所有这些Prism.js代码都是在这个静态网站生成过程中运行的,当时没有DOM。
但是不要害怕!有一个有趣的解决办法。我们可以使用普通的CSS(和少量的JavaScript)来突出显示代码行,这也是一种适合CSS-Tricks的有趣的解决方法。
让我说清楚,这是一项非同小可的工作。如果你不需要突出显示行,那么请跳到下一节。但是,如果没有其他原因,它可以成为一个有趣的示范,说明什么是可能的。
我们的基础CSS
让我们先在我们的prism-overrides.css 样式表中添加以下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主题的padding是1em 。行号插件将其增加到3.8em ,然后用绝对定位插入行号。我们所做的是将padding恢复到默认的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网站很好地配合。