Markdown 確實是一種很棒的格式。它足夠接近純文本,任何人都可以快速學習,並且它的結構足夠好,可以被解析並最終轉換為任何你想要的格式。
然而:解析、處理、增強和轉換Markdown 需要代碼。在客戶端部署所有這些代碼會付出代價。它本身並不巨大,但仍然是幾十KB的代碼,僅用於處理Markdown,而沒有其他用途。
在本文中,我將解釋如何在Next.js 應用程序中將Markdown 保持在客戶端之外,使用Unified/Remark 生態系統(真的不知道該用哪個名字,這太令人困惑了)。
其思路是在Next.js 的getStaticProps
函數中僅使用Markdown,以便在構建過程中完成此操作(如果使用Vercel 的增量構建,則在Next 無服務器函數中完成),但絕不在客戶端使用。我想getServerSideProps
也行,但我認為getStaticProps
更可能是常見的用例。
這將返回一個由解析和處理Markdown 內容生成的AST(抽象語法樹,也就是說,一個描述我們內容的大型嵌套對象),客戶端只需負責將該AST 渲染成React 組件。
我想我們甚至可以直接在getStaticProps
中將Markdown 渲染為HTML 並返回它以使用dangerouslySetInnerHtml
渲染,但我們不是那種人。安全很重要。還有,用我們自己的組件以我們想要的方式渲染Markdown 的靈活性,而不是將其渲染為純HTML。說真的,朋友們,不要那樣做。 ?
export const getStaticProps = async () => { // 從某個地方獲取Markdown 內容,例如CMS 或其他內容。就本文而言,這並不重要。也可以從文件中讀取。 const markdown = await getMarkdownContentFromSomewhere() const ast = parseMarkdown(markdown) return { props: { ast } } } const Page = props => { // 這通常也包含你的佈局等等,但這里為了簡單起見省略了。 return<markdownrenderer ast="{props.ast}"></markdownrenderer> } export default Page
我們將使用Unified/Remark 生態系統。我們需要安裝unified 和remark-parse,僅此而已。解析Markdown 本身相對簡單:
import { unified } from 'unified' import remarkParse from 'remark-parse' const parseMarkdown = content => unified().use(remarkParse).parse(content) export default parseMarkdown
現在,讓我費了好長時間才明白的是,為什麼我的額外插件(如remark-prism 或remark-slug)不能像這樣工作。這是因為Unified 的.parse(..)
方法不會使用插件處理AST。顧名思義,它只將Markdown 字符串內容解析成樹。
如果我們希望Unified 應用我們的插件,我們需要Unified 經歷他們所謂的“運行”階段。通常,這是通過使用.process(..)
方法而不是.parse(..)
方法來完成的。不幸的是, .process(..)
不僅解析Markdown 並應用插件,還會將AST 字符串化為另一種格式(例如,通過remark-html 使用HTML,或通過remark-react 使用JSX)。而這並不是我們想要的,因為我們想要保留AST,但在它被插件處理之後。
<code>| ........................ process ........................... | | .......... parse ... | ... run ... | ... stringify ..........| -------- ----------输入->- | 解析器| ->- 语法树->- | 编译器| ->- 输出-------- | ---------- X | -------------- | 变换器| --------------</code>
因此,我們需要做的就是運行解析和運行階段,但不運行字符串化階段。 Unified 沒有提供執行這三個階段中的兩個階段的方法,但它為每個階段提供了單獨的方法,因此我們可以手動執行:
import { unified } from 'unified' import remarkParse from 'remark-parse' import remarkPrism from 'remark-prism' const parseMarkdown = content => { const engine = unified().use(remarkParse).use(remarkPrism) const ast = engine.parse(content) // Unified 的*process* 包含三個不同的階段:解析、運行和字符串化。我們不想經歷字符串化階段,因為我們想保留AST,所以我們不能調用`.process(..)`。但是,僅調用`.parse(..)` 不夠,因為插件(因此是Prism)在運行階段執行。所以我們需要手動調用運行階段(為簡單起見,同步進行)。 // 請參閱:https://github.com/unifiedjs/unified#description return engine.runSync(ast) }
瞧!我們將Markdown 解析成了語法樹。然後,我們在該樹上運行了我們的插件(這里為了簡單起見同步完成,但你可以使用.run(..)
異步完成)。但是,我們沒有將我們的樹轉換成其他語法,如HTML 或JSX。我們可以在渲染中自己完成。
現在我們已經準備好我們的酷炫樹了,我們可以按照我們的意圖來渲染它。讓我們創建一個MarkdownRenderer
組件,它接收樹作為ast
屬性,並使用React 組件渲染它。
const getComponent = node => { switch (node.type) { case 'root': return ({ children }) => {children}> case 'paragraph': return ({ children }) =><p> {children}</p> case 'emphasis': return ({ children }) => <em>{children}</em> case 'heading': return ({ children, depth = 2 }) => { const Heading = `h${depth}` return<heading> {children}</heading> } case 'text': return ({ value }) => {value}> /* 在此處處理所有類型…… */ default: console.log('未處理的節點類型', node) return ({ children }) => {children}> } } const Node = ({ node }) => { const Component = getComponent(node) const { children } = node return children ? ( <component> {children.map((child, index) => ( <node key="{index}" node="{child}"></node> ))} </component> ) : ( <component></component> ) } const MarkdownRenderer = ({ ast }) =><node node="{ast}"></node> export default React.memo(MarkdownRenderer)
我們渲染器的邏輯大部分都位於Node
組件中。它根據AST 節點的type
鍵找出要渲染的內容(這是我們的getComponent
方法處理每種類型的節點),然後渲染它。如果節點有子節點,它會遞歸地進入子節點;否則,它只渲染組件作為最終的葉子節點。
根據我們使用的Remark 插件,在嘗試渲染頁面時,我們可能會遇到以下問題:
錯誤:序列化.content[0].content.children[3].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hName(來自“/”中的getStaticProps)時出錯。原因:undefined 無法序列化為JSON。請使用null 或省略此值。
發生這種情況是因為我們的AST 包含值未定義的鍵,這並不是可以安全地序列化為JSON 的內容。 Next 為我們提供了解決方案:我們可以完全省略該值,或者如果我們多少需要它,則將其替換為null。
但是,我們不會手動修復每條路徑,因此我們需要遞歸遍歷該AST 並將其清理乾淨。我發現當使用remark-prism(一個啟用代碼塊語法高亮的插件)時會發生這種情況。該插件確實會向節點添加一個[data]
對象。
我們可以做的是在返回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(remarkParse).use(remarkPrism) const ast = engine.parse(content) const processedAst = engine.runSync(ast) cleanNode(processedAst) return processedAst }
我們可以做的最後一件事是刪除存在於每個節點上的position
對象,該對象保存Markdown 字符串中的原始位置。它不是一個大的對象(它只有兩個鍵),但是當樹變大時,它會迅速累加。
const cleanNode = node => { delete node.position // ... 其他清理邏輯}
就是這樣!我們設法將Markdown 處理限制在構建/服務器端代碼中,因此我們不會向瀏覽器發送不必要的Markdown 運行時,這會不必要地增加成本。我們將數據樹傳遞給客戶端,我們可以遍歷它並將其轉換為我們想要的任何React 組件。
希望這有幫助。 :)
以上是Next.js中負責的賭博的詳細內容。更多資訊請關注PHP中文網其他相關文章!