首頁 > web前端 > css教學 > Next.js中負責的賭博

Next.js中負責的賭博

Joseph Gordon-Levitt
發布: 2025-03-21 10:57:11
原創
825 人瀏覽過

Responsible Markdown in Next.js

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
登入後複製

解析Markdown

我們將使用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。我們可以在渲染中自己完成。

渲染Markdown

現在我們已經準備好我們的酷炫樹了,我們可以按照我們的意圖來渲染它。讓我們創建一個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中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板