Bobolo
  • Home
  • Me

MDX Page

MDX Page

8 Min Read

Mon Aug 23 2021

Written By Bobolo

MDX Page

如果搭建一个 Blog,就必然需要一些文章的展示,甚至需要如代码高亮或者更多的展示效果,那 Markdown 是一个不错的选择,无论是在展示还是在撰写的过程中。掌握好它对于以后写一些 Document 是有所帮助的。

在项目中使用 Markdown 有多个方式,如直接在 Page 下 import 一些第三方的 Component,如果用 React/Next 的话可以用 React-markdown 等。

还有更方便的 MDX,MDX 是 Markdown 的超集,可以让你直接在 Markdown 文件中编写 JSX/TSX。

在最初使用的是 Next,先已经更新为 Qwik,主要是想了解一下 Qwik 就替换了,两个框架都支持使用 MDX 。

而 Qwik 现在已经提供的 Qwik/MDX,对于一个完整的 Markdown 文章展示页面是足够的,而且 Qwik 也已经默认开启这个功能 。

但是对于要实现一个相对完善的 Blog 来说,单单使用 MDX 可能还是不够的,有几个场景目前是无法满足,需要自己进行补充。

Reason

  • Code Highlighter
  • Components
  • Blog Features

Code Highlighter

在 Blog 如果有需要进行代码展示,在代码高亮上直接使用 MDX 的效果不是很好,因此需要自己去处理或者引入第三方的 pkg,如 React 的话可以使用 react-syntax-highlighter 作为代码高亮的展示,但目前 Qwik 的生态并不是非常的成熟,并没有找到对应的 pkg,因此 Code Highlighter 我只能自己去实现。

到 Page 目录下 创建一个 Mdx Page:

// src/components//markdown/index.tsx
import { component$, useTask$, useSignal } from '@builder.io/qwik'
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import { rehypeSyntaxHighlight } from './highlight'

interface Props {
  content?: string
}

export default component$((props: Props) => {
  const code = useSignal(props.content || '')
  useTask$(async () => {
    const file = await unified()
      .use(remarkParse)
      .use(remarkGfm)
      .use(remarkRehype)
      .use(rehypeStringify)
      .use(rehypeSyntaxHighlight)
      .process(code.value)
    code.value = file.value as string
  })
  return <div dangerouslySetInnerHTML={code.value}></div>
})

稍微简单的介绍一下 unified, 一个使用语法树处理文本的接口

| ........................ process ........................... |
| .......... parse ... | ... run ... | ... stringify ..........|

          +--------+                     +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
          +--------+          |          +----------+
                              X
                              |
                       +--------------+
                       | Transformers |
                       +--------------+

前面部分 unified 已经可以实现解析 markdown -> HTML,但是 HTML 还缺少一些 class/style 等去实现 Code Highlighter。

// src/components//markdown/highlight.ts
import type { Transformer } from 'unified'
import { toString } from 'hast-util-to-string'
import { visit } from 'unist-util-visit'
import { refractor } from 'refractor'
import tsxLang from 'refractor/lang/tsx.js'

export function rehypeSyntaxHighlight(): Transformer {
  refractor.register(tsxLang)
  // ast example
  // {
  //  type: 'root',
  //  children: [
  //    {
  //      type: 'element',
  //      tagName: 'h2',
  //      properties: {},
  //      children: [{
  //        type: 'element',
  //        tagName: 'li',
  //        properties: {},
  //        children: [
  //          {
  //            type: 'text',
  //            value: 'Simplifying the complexity of components',
  //            position: [Object]
  //          }
  //        ],
  //        position: {
  //          start: { line: 175, column: 1, offset: 4449 },
  //          end: { line: 175, column: 43, offset: 4491 }
  //        }
  //      }],
  //      position: [Object]
  //    },
  //    { type: 'text', value: '\n' },
  //  ]
  // }
  return async ast => {
    visit(ast, 'element', (node: any, _index: number, parent: any) => {
      if (
        !parent ||
        parent.tagName !== 'pre' ||
        node.tagName !== 'code' ||
        !Array.isArray(node.properties.className)
      ) {
        return
      }

      for (let i = 0; i < node.properties.className.length; i++) {
        const className = node.properties.className[i]
        const lang = getLanguage(className)
        if (lang && refractor.registered(lang)) {
          node.properties.className[i] = 'language-' + lang
          syntaxHighlight(node, lang)
          return
        }
      }
    })
  }
}

function syntaxHighlight(node: any, lang: string) {
  const code = toString(node)
  const result = refractor.highlight(code, lang)
  if (result && Array.isArray(node.children)) {
    node.children = result.children
  }
}

function getLanguage(className: string) {
  if (typeof className === 'string') {
    className = className.toLowerCase()
    if (className.startsWith('language-')) {
      return className.slice(9)
    }
  }
  return null
}

以上就是一个基本的 Code Highlighter。

Components

默认使用本身的 MDX 是不具备 UI,因此我是选择了引用 Qwik 官方的 Document 样式,这也可以通过在 remarkParse 这些 plugin 中,自己对标签进行处理来解决。

当然如果遇到一些使用第三方的 UI 库也许会存在风格不统一的问题,UI 复杂的时候就变得相对麻烦不少。

而且因 .mdx 文件无法作为 Component 的方式来使用,也许已经有一些 Package 可以使用了,至少目前在 Qwik(0.16.2) 上是不能够的。

选择第三方库时,建议多考虑一下场景是否需要 darkmode 和 bundle size,来选择合适的 UI 库

Blog Features

以上两个问题总的来说都还是比较好解决的,而最重要的一些 Features 才是关键所在。如果全部的文章都使用 MDX ,在很多方面会变得麻烦。

  • 文章在线编辑、删除等
  • 文章的展示和命名等
  • 扩展功能如读文章、评论等

文章的编辑、删除等功能,在没有接口的情况下难以去解决,当然如果不想写接口,也或者可以使用 Github Contents Api,通过去 Create File 或者读取 Github 的 Files 等 api 来实现,但也会是另外的一些内容而且也衍生其他的问题,例如可能存在多个 content 并发或者多个 commit 等。

文章的展示和命名等,同样在没有接口的情况下容易遇到问题,例如在 File 中的 path 是 articles/Test Case/index.mdx,在 Qwik 中是目前无法使用的,哪怕使用 articles/Test%20Case 也无法打开。而且也不方便控制首页的 list 展示。

扩展功能如文章的拼读、评论等功能就更不用说了,在 MDX 中并不好处理,在 .mdx 中进行 fix 可能也会因此导致整个 file 变得复杂,不如直接用 database 搞 API 来的方便,而且如果需要更换技术栈,也会不方便。

不使用一些 api 在很多场景都会不方便,甚至还有一些未提及的场景,这也是我不推荐用 MDX 构建 Blog 的原因

Conclusion

以上就是这个你所看到的页面效果所涉及的代码与配置了,在配置上来说还是比较的简单,也并未引入太多第三方库。

但并不是说 MDX 不适合构建一个 Blog,主要还是看 Blog 需要具备的功能有哪些,如果 Blog 相对简单,只需要进行展示,那直接使用 MDX 应该是非常不错的选择。

Powered by Bobolo

Copyright © Bobolo Blog 2021