Markdown确实是一种伟大的格式。它足够接近于纯文本,所以任何人都可以很快学会它,而且它的结构化程度很高,可以被解析并最终转换为你的名字。
这就是说:解析、处理、增强和转换Markdown需要代码。将所有这些代码放在客户端是有代价的。它本身并不庞大,但它仍然是几十KB的代码,只用来处理Markdown而不是其他。
在这篇文章中,我想解释一下如何在Next.js应用程序中使用统一/Remark生态系统(真心不知道该用哪个名字,这都是超级混乱的)将Markdown从客户端中分离出来。
总体思路
我们的想法是,只在Next.js的getStaticProps 函数中使用Markdown,所以这是在构建过程中完成的(如果使用Vercel的增量构建,则在Next的无服务器函数中完成),但绝不在客户端使用。我想getServerSideProps 也可以,但我认为getStaticProps 更有可能成为常见的使用情况。
这将返回解析和处理Markdown内容后得到的AST(抽象语法树,也就是描述我们内容的大嵌套对象),而客户端只负责将AST渲染成React组件。
我想我们甚至可以在getStaticProps 中直接将Markdown渲染成HTML,然后返回到dangerouslySetInnerHtml ,但我们不是那种人。安全很重要。还有,我们可以用我们的组件以我们想要的方式渲染Markdown,而不是把它渲染成纯HTML。认真的朋友们,不要这样做。😅
export const getStaticProps = async () => {
// Get the Markdown content from somewhere, like a CMS or whatnot. It doesn’t
// matter for the sake of this article, really. It could also be read from a
// file.
const markdown = await getMarkdownContentFromSomewhere()
const ast = parseMarkdown(markdown)
return { props: { ast } }
}
const Page = props => {
// This would usually have your layout and whatnot as well, but omitted here
// for sake of simplicity of course.
return <MarkdownRenderer ast={props.ast} />
}
export default Page
解析Markdown
我们将使用Unified/Remark的生态系统。我们需要安装unified 和remark-parse ,仅此而已。解析Markdown本身是相对简单的。
import unified from 'unified'
import markdown from 'remark-parse'
const parseMarkdown = content => unified().use(markdown).parse(content)
export default parseMarkdown
现在,我花了很长时间才明白,为什么我的额外插件,如remark-prism 或remark-slug ,不能像这样工作。这是因为统一公司的.parse(..) 方法并不处理带有插件的AST。顾名思义,它只是将Markdown内容的字符串解析成一棵树。
如果我们想让Unified应用我们的插件,我们需要Unified经过他们所说的 "运行 "阶段。通常情况下,这是通过使用.process(..) 方法而不是.parse(..) 方法来完成的。不幸的是,.process(..) 不仅解析 Markdown 并应用插件,而且还将 AST 串化成另一种格式(比如通过remark-html 的 HTML,或通过remark-react 的 JSX)。而这并不是我们想要的,因为我们想要保留AST,但要在它被插件处理之后。
| ........................ process ........................... |
| .......... parse ... | ... run ... | ... stringify ..........|
+--------+ +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
+--------+ | +----------+
X
|
+--------------+
| Transformers |
+--------------+
所以我们需要做的是同时运行解析和运行阶段,而不是字符串化阶段。Unified没有提供一个方法来完成3个阶段中的这2个,但它为每个阶段提供了单独的方法,所以我们可以手动完成。
import unified from 'unified'
import markdown from 'remark-parse'
import prism from 'remark-prism'
const parseMarkdown = content => {
const engine = unified().use(markdown).use(prism)
const ast = engine.parse(content)
// Unified‘s *process* contains 3 distinct phases: parsing, running and
// stringifying. We do not want to go through the stringifying phase, since we
// want to preserve an AST, so we cannot call `.process(..)`. Calling
// `.parse(..)` is not enough though as plugins (so Prism) are executed during
// the running phase. So we need to manually call the run phase (synchronously
// for simplicity).
// See: https://github.com/unifiedjs/unified#description
return engine.runSync(ast)
}
Tada!我们把我们的Markdown解析成一个语法树。然后我们在这个树上运行我们的插件(为了简单起见,这里是同步进行的,但你可以用.run(..) 来异步进行)。但我们并没有将我们的树转换为其他的语法,如HTML或JSX。我们可以在渲染时自己做。
渲染Markdown
现在我们已经准备好了我们很酷的树,我们可以用我们的方式来渲染它。让我们有一个MarkdownRenderer 组件,将树作为一个ast 道具来接收,然后用React组件来渲染它。
const getComponent = node => {
switch (node.type) {
case 'root':
return React.Fragment
case 'paragraph':
return 'p'
case 'emphasis':
return 'em'
case 'heading':
return ({ children, depth = 2 }) => {
const Heading = `h${depth}`
return <Heading>{children}</Heading>
}
/* Handle all types here … */
default:
console.log('Unhandled node type', node)
return React.Fragment
}
}
const Node = node => {
const Component = getComponent(node)
const { children } = node
return children ? (
<Component {...node}>
{children.map((child, index) => (
<Node key={index} {...child} />
))}
</Component>
) : (
<Component {...node} />
)
}
const MarkdownRenderer = props => <Node {...props.ast} />
export default React.memo(MarkdownRenderer)
我们的渲染器的大部分逻辑都在Node 组件中。它根据AST节点的type key(这是我们的getComponent 方法,处理每一种类型的节点)找出要渲染的内容,然后渲染它。如果该节点有子节点,它就递归到子节点中;否则,它就把该组件渲染成最后的叶子。
清理树
根据我们使用的Remark插件,我们在试图渲染我们的页面时可能会遇到以下问题。
错误。在"/"中序列化从
getStaticProps返回的.content[0].content.children[3].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hName时出错。原因:undefined不能被序列化为JSON。请使用null或省略此值。
发生这种情况是因为我们的AST包含的键的值是undefined ,这不是可以安全地序列化为JSON的东西。接下来给了我们解决方案:要么我们完全省略这个值,要么如果我们有点需要它,就用null 来代替它。
不过我们不可能手工修复每一个路径,所以我们需要递归地走一遍AST,把它清理掉。我发现这种情况是在使用remark-prism 的时候发生的,这是一个为代码块启用语法高亮的插件。该插件确实为节点添加了一个[data] 对象。
我们可以做的是,在返回AST之前,先走一遍我们的AST,清理这些节点。
const cleanNode = node => {
if (node.value === undefined) delete node.value
if (node.tagName === undefined) delete node.tagName
if (node.data) {
delete node.data.hName
delete node.data.hChildren
delete node.data.hProperties
}
if (node.children) node.children.forEach(cleanNode)
return node
}
const parseMarkdown = content => {
const engine = unified().use(markdown).use(prism)
const ast = engine.parse(content)
const processedAst = engine.runSync(parsed)
cleanNode(processedAst)
return processedAst
}
我们可以做的最后一件事是移除position 对象,该对象存在于每一个节点上,并保存着Markdown字符串中的原始位置,以便向客户端发送更少的数据。它不是一个很大的对象(它只有两个键),但是当树变大时,它很快就会增加。
const cleanNode = node => {
delete node.position
收尾工作
就这样了,伙计们我们设法将Markdown的处理限制在构建/服务器端的代码中,这样我们就不会将Markdown的运行时间运送给浏览器,而这是不必要的成本。我们向客户端传递了一棵数据树,我们可以把它走到并转换为我们想要的任何React组件。
我希望这有帮助。:)
The postResponsible Markdown in Next.jsappeared first onCSS-Tricks.你可以通过成为MVP支持者来支持CSS-Tricks。