
🔥 天冷了🔥 烤烤火
代码共享是现代Web开发的一个重要方面,它使开发人员能够合作并互相学习。许多网站通过在其代码片段中添加"复制到剪贴板"按钮,使代码共享变得更加容易。这一功能在移动设备上尤其有用,为用户提供了一种快速简便的方法,无需手动选择文本即可共享代码片段。一些代码高亮库包含此功能,可以通过内置或插件方式实现。
然而,如果你在你的Next.js应用程序中使用MDX,为实现"复制到剪贴板"按钮并没有一种适用于所有情况的解决方案。
本教程将指导你如何为使用Rehype Pretty Code处理的代码片段创建一个"复制到剪贴板"按钮,Rehype Pretty Code是MDX-based Next.js网站中常用的用于语法高亮的Rehype插件。我们将使用Next.js 13+,并启用"app"目录。在本文结束时,你将能够简化代码共享,并提升你的Next.js网站的用户体验。事实上,这篇博客就使用了我们将要创建的功能,所以你可以在实际运行中看到结果。让我们开始吧!
设置
像往常一样,我们需要安装所需的依赖。
css
npm install rehype-pretty-code shiki unist-util-visit
在这个项目中,我们使用Rehype Pretty Code插件,该插件为MD或MDX文件提供语法高亮。此高亮是在构建时执行的,对页面速度产生积极影响。此外,你可能需要一个用于处理MDX内容的库。这篇博客使用Contentlayer,该库对MDX处理提供了强大的支持。然而,请注意它目前处于beta版本,可能并不适合所有人。
如果你想提高Next.js网站的可发现性,我写了一篇关于如何添加站点地图的文章:在Next.js 13中实现动态站点地图:使用Pages目录或App目录
提取代码内容
在我们的案例中,渲染按钮是一项挑战,因为语法高亮是在服务器端执行的。当我们打算在客户端渲染按钮时,代码内容将已经包装在各种标记中,这些标记是语法高亮所必需的。一种潜在的解决方案是在复制时解析代码内容,删除所有HTML标记。然而,这种方法效率低下,因为它需要撤销Rehype Pretty Code为我们完成的一切。
幸运的是,有一种更好的方法来实现我们的目标。我们可以在语法高亮阶段之前提取未经样式化的代码内容,并将其附加为代码节点的属性。然后,我们可以在自定义MDX组件内访问此内容,该组件用于渲染复制按钮。
首先,我们需要创建一个访问器函数,它遍历内容的节点树并从所有代码元素中提取未修改的(原始文本)内容,这些元素嵌套在pre标记中。我们将这个文本内容存储在pre节点本身上。为了遍历节点树,我们将使用unist-util-visit包中的visit函数。这个访问器函数应该被添加到现有的Rehype插件列表中,在Contentlayer的情况下,它是contentlayer.config.js文件。
javascript
{
rehypePlugins: [
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
const [codeEl] = node.children;
if (codeEl.tagName !== "code") return;
node.raw = codeEl.children?.[0].value;
}
});
}
]
}
这将为我们提供一种保留未修改的代码内容的方式,我们可以稍后从节点的raw属性中访问它。
现在我们添加Rehype Pretty Code插件的配置。
javascript
{
rehypePlugins: [
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
const [codeEl] = node.children;
if (codeEl.tagName !== "code") return;
node.raw = codeEl.children?.[0].value;
}
});
},
[
rehypePrettyCode,
{
theme: {
dark: "one-dark-pro",
light: "github-light",
},
// 其他rehypePrettyCode配置
},
],
]
}
值得注意的是,我们在以下代码中同时使用了浅色和深色主题。该插件为每个主题生成两个单独的代码块,通过CSS隐藏其中一个,具体取决于所选的主题。为了在主题之间切换,此博客使用class属性,确保每次只有一个主题可见,具体代码如下:
css
html.light[data-theme="dark"] {
display: none;
}
html.dark[data-theme="light"] {
display: none;
}
重要的是要注意,使用两个主题还有另一个含义:我们必须将raw属性转发到两个不同的pre元素,而不仅仅是一个。为了实现这一点,我们需要在语法高亮完成后实现另一个访问器函数。
javascript
{
rehypePlugins: [
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
const [codeEl]
= node.children;
if (codeEl.tagName !== "code") return;
node.raw = codeEl.children?.[0].value;
}
});
},
[
rehypePrettyCode,
{
theme: {
dark: "one-dark-pro",
light: "github-light",
},
// 其他rehypePrettyCode配置
},
],
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "div") {
if (!("data-rehype-pretty-code-fragment" in node.properties)) {
return;
}
for (const child of node.children) {
if (child.tagName === "pre") {
child.properties["raw"] = node.raw;
}
}
}
});
},
]
}
在这段代码中,我们选择所有包含data-rehype-pretty-code-fragment数据属性的div元素。然后,我们遍历每个div内的pre子元素(每个主题一个)并将原始代码内容作为属性添加到它们中。有了这个实现,用于渲染pre元素的自定义MDX组件将在可用的props中具有raw属性。接下来,我们将这个Pre组件添加到我们的代码中:
ini
export const Pre = ({ children, raw, ...props }) => {
const lang = props["data-language"];
return (
<pre {...props} className={"p-0"}>
<div
className={'code-header'}>
{lang}
</div>
{children}
</pre>
);
};
我们还提取了data-language以显示给定片段的代码语言。现在,我们将其用作MDX渲染器内的组件之一。
javascript
import { useMDXComponent } from "next-contentlayer/hooks";
import { Pre } from "./components/Pre";
const components = {
pre: Pre,
};
export const Mdx = ({ code }) => {
const MDXContent = useMDXComponent(code);
return <MDXContent components={components} />;
};
Contentlayer库处理MDX渲染,我们使用其next-contentlayer包进行Next.js集成。该包提供了useMDXComponent钩子,我们使用它来渲染MDX并通过组件对象传递我们的自定义组件。这允许MDX文件中的每个pre元素都用我们的自定义Pre组件替换。pre标记的内容将通过Pre组件内的children prop访问。
添加CopyButton组件
现在我们准备添加CopyButton组件,它将处理"复制到剪贴板"功能。
为实现"复制到剪贴板"功能,我们可以使用JavaScript访问剪贴板,并使用React渲染UI。在JavaScript中,以前我们会使用document.execCommand("copy")方法将内容复制到剪贴板。然而,这种方法现在不再推荐使用,并且在某些浏览器中不受支持。一个更可靠的方法是使用Clipboard API,该API提供了一组异步方法来读取和写入剪贴板。
javascript
"use client";
import { useState } from "react";
export const CopyButton = ({ text }) => {
const [isCopied, setIsCopied] = useState(false);
const copy = async () => {
await navigator.clipboard.writeText(text);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 10000);
};
return (
<button disabled={isCopied} onClick={copy}>
{isCopied ? "Copied!" : "Copy"}
</button>
);
};
请注意,由于我们使用了app目录,其中所有组件默认为Server Components,我们需要通过"use client"指令明确将此组件标记为客户端组件。
组件本身非常简单。当单击Copy文本时,我们使用Navigator.clipboard API存储代码内容(通过text prop可用)。此外,我们将按钮文本更改为Copied!并设置十秒的超时以将其重置。
最后,我们可以在Pre组件内使用这个"复制到剪贴板"按钮。
javascript
import { CopyButton } from "./CopyButton";
export const Pre = ({ children, raw, ...props }) => {
const lang = props["data-language"] || "shell";
return (
<pre {...props} className={"p-0"}>
<div className={"code-header"}>
{lang}
<CopyButton text={raw} />
</div>
{children}
</pre>
);
};
最终结果应该与本文中代码片段的Copy按钮类似(减去样式)。
总结
"复制到剪贴板"按钮是改进代码片段可访问性的绝佳方式,特别是在移动设备上。然而,当为使用Rehype Pretty Code处理的代码片段创建"复制到剪贴板"按钮时,在MDX-based Next.js应用程序中,需要采用特定的方法。主要的概念是将未经处理的代码内容作为pre元素的属性传递,这样在应用语法高亮后,可以在稍后访问内容。这种方法是有效的,并简化了代码共享,从而提升了在使用MDX和Rehype Pretty Code的Next.js网站上的用户体验。CopyButton组件的概念并非特定于Next.js。它被实现为一个React组件,利用JavaScript在浏览器中提供的Clipboard API。
📌 休息驿站