在 React 中渲染 Markdown
在我的博客中使用的是unified来渲染markdown, 使用react-markdown的插件生态.
这主要和我的需求有关, 我的按照我自己的习惯, 比较喜欢使用Markdown来记录, 所以后端的数据库里存的也是Markdown形式的字符串.
为什么不直接使用后端渲染 Markdown 呢?
我的后端用的是python因为python的Markdown生态属实一般, 之前试过python的一个Markdown模块, 那个模块渲染出来的Markdown有一点问题, 不能被MathJAX渲染成数学公式. 所以这部分工作交由前端来完成.
使用 unified
unified项目开源地址: github.com/unifiedjs/unified .
unified 最有特色的就是他丰富的插件生态了, 我目前所需要的功能基本上装上对应的插件就能够解决, 使用插件的方式也很简单, 直接在unified()后面加上.use(<插件>)就可以了. 比如说我的渲染markdown, 并返回渲染后的 HTML 字符串的代码就是:
export async function markdownToHtml(markdown: string): Promise<string> {
const result = await unified()
.use(remarkParse)
.use(remarkMath)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeKatex)
.use(rehypeHighlight)
.use(rehypeHighlightCodeLines, { showLineNumbers: true })
.use(rehypeStringify)
.process(markdown);
return result.toString();
}
想要直接输出渲染的HTML字符串的话就是:
export function Page() {
const html = await markdownToHtml(markdownContent);
return <article dangerouslySetInnerHTML={{ __html: html }} />
}
之后按照自己的需求添加样式即可, 比如说我使用的样式就是Github的, 来自github-markdown-css这个包.
自定义标签属性
上述流程虽然用起来很方便, 但是如果需要自定义的话就比较麻烦, 具体的实现的话可以使用unist-util-visit这个包, 这个包会遍历HTML抽象语法树上的节点, 按照需要修改自己需要的部分就可以了:
import { Element } from "mdx/types";
import { visit } from "unist-util-visit";
export function rehypeCustomAttrs() {
return (tree: any) => {
visit(tree, "element", (node: Element) => {
if (node.tagName === "a") {
const internalLinkPattern =
/^(data-footnote-backref|user-content-fnref)/;
const className = node.properties?.className as string[] | undefined;
const id = node.properties?.id as string | undefined;
const isInternalLink =
className?.some((cls) => internalLinkPattern.test(cls)) ||
internalLinkPattern.test(id ?? "");
node.properties = {
...node.properties,
...(!isInternalLink && {
target: "_blank",
rel: "noopener noreferrer",
}),
};
}
});
};
}
有了这个自定义插件之后直接添加到unified中的处理链中就可以了. 就像这样:
export async function markdownToHtml(markdown: string): Promise<string> {
const result = await unified()
.use(remarkParse)
.use(remarkMath)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeCustomAttrs)
.use(rehypeKatex)
.use(rehypeHighlight)
.use(rehypeHighlightCodeLines, { showLineNumbers: true })
.use(rehypeStringify)
.process(markdown);
return result.toString();
}
题外话
为什么不直接使用MDX呢, 因为这个东西我也没搞明白, 虽然MDX在Next.js中也是有原生支持的, 但是渲染结果貌似有点区别. 并且, 因为我的前端也是使用了turbopack, 这个东西是Rust写的"增量捆绑器"(incremental bundler), 本地编译确实很快. 但是他是Rust, 这也就意味着不能直接传TypeScript的模块给他, 详见Configuring: MDX | Next.js, 然后就有一个尴尬的事情, 我本地运行的时候需要使用字符串, 而构建的时候需要改为 TS 模块...并且使用MDX的话Markdown文件就需要放在前端作为一个模块了(其实about页面就是用MDX写的).