个人网站的富文本渲染
·Shinji最近在重新开发个人网站,博客文章的渲染做了一点研究,最后选定了 markdown 作为内容格式,简单聊聊。
富文本渲染主要有两种方案
- HTML 字符串
- virtual DOM
markdown 可以算作 HTML 的子方案,virtual DOM 的路线也演化出一些私有数据格式,比如 portabletext,Delta。
回到我个人的场景里,文章首先是在 Obsidian 里编辑,定稿后发布到 Supabase,所以数据源肯定是 markdown,markdown 的语法简单,且所见即所得,同时大多数平台编辑器都支持以 markdown 的形式进行输入;而 virtual DOM 格式对人类不友好,在需要直接编辑格式数据的情况下 virtual DOM 便直接被排除了;更重要的是数据迁移难度,现在各种云服务使用私有格式存储,如果数据完全依赖于某个平,只能希望该平台活得比你久,一旦需要迁移,必然会面临非常困难的情况。
markdown 本质上是类似于 HTML 的标记语言,规定一些书写格式来对应 HTML 的标签,比如 # h1
是 <h1>h1<h1>
,渲染规则其实是匹配格式转换成 HTML 代码,非规定格式直接复制过去,所以,markdown 也可以说是 HTML 的简写形式。但是 markdown 原生语法非常少,如果希望展示一些更丰富的内容时就会感到捉襟见肘,遑论交互,所以 必然要对 markdown 语法进行扩展。
在这个领域中,已经形成了一个非常强大的技术生态 unifiedjs ,它是一套处理结构化文本的方案体系,能够处理 Markdown、HTML、自然语言等,它可以将文本内容编译为语法树并进行渲染,拥有数百个包来处理中间的转换过程,并且在渲染阶段可以自定义渲染方式。旗下 markdown 相关插件集合称为 remark,提供了 150 多个可供选择的插件,可以扩展支持 emoji,数学公式,化学方程式,TOC 等。
一个简单的例子:
import React, { Fragment } from 'react';
import { Remark } from 'react-remark';
import remarkGemoji from 'remark-gemoji';
import rehypeSlug from 'rehype-slug';
import rehypeAutoLinkHeadings from 'rehype-autolink-headings';
// ...
<Remark
remarkPlugins={[remarkGemoji]}
remarkToRehypeOptions={{ allowDangerousHtml: true }}
rehypePlugins={[rehypeSlug, rehypeAutoLinkHeadings]}
rehypeReactOptions={{
components: {
p: (props) => <p className="custom-paragraph" {...props} />,
},
}}
>
{markdownSource}
</Remark>;
扩展后的 markdown 已经非常强大了,在内容展示层面几乎可以满足所有需求,但是细看可以发现,它其实还是在 HTML 和 CSS 的范围内,停留在静态编译阶段,运行时还是一片空白,前端最强大的 JavaScript 生态并没有发挥出来。
这里补充一个前端的技术 JSX
JSX 是 JavaScript 语法扩展,可以让你在 JavaScript 文件中书写类似 HTML 的标签。虽然还有其它方式可以编写组件,但大部分 React 开发者更喜欢 JSX 的简洁性,并且在大部分代码库中使用它。
用大白话说就是你可以在 JSX 中把 HTML 和 JavaScript 写在一起,前面说了 markdown 可以说是 HTML 的简写形式,那么把 JSX 中的 HTML 替换成 markdown 理论上也是可以的,所以就出现了一项新技术MDX。
MDX 允许你在 Markdown 文档中无缝地插入 JSX 代码。 你还可以导入(import)组件,例如交互式图表或弹框,并将它们嵌入到内容当中。
这样一来,等于是把博客文章放进一个页面的运行时里,拥有完整的上下文,且能够像 React 或 Vue 一样编写,然后就是发挥你想象力的时刻了,无论是文本内容动态生成图表,还是直接写功能 demo 等等都变得非常简单。
import { notFound } from 'next/navigation'
import * as runtime from 'react/jsx-runtime'
import { compile, run } from '@mdx-js/mdx'
import { Fragment } from 'react'
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
// get your post data
if (post === null) {
notFound()
}
const content = String(
await compile(post.content, {
outputFormat: 'function-body',
development: false,
}
)
const contentModule = await run(content, runtime)
const PostContent = contentModule ? contentModule.default : Fragment
return (
<>
<article>
<h1>{post.title}</h1>
<PostContent />
</article>
</>
)
}
至此,一套基于 markdown ,可扩展,功能完善的渲染方案就出炉了。