Markdown解析与编译(二)

Markdown 解析与编译(二)

前言

在上一篇我们分别介绍了 mdx 的实时编译和预编译的使用案例,讲了什么时候应该使用实时编译,什么时候应该使用预编译,本小节就来讲讲如何实现 mdx 的预编译与实时编译。

实现 mdx 的预编译

关于 mdx 的预编译,已经在上一小节做过详细介绍,这里就不再赘述,想要实现 mdx 的预编译,可以按照如下步骤来实现。

1. 添加依赖

pnpm install esbuild
pnpm install mdx-bundler

2. 编写预编译函数

export const compileMDX = async (source: any) => {
  const toc: Toc = [];
  const { code, frontmatter } = await bundleMDX({
    source,
    cwd: path.join(root, 'components'),
    mdxOptions(options, frontmatter) {
      options.remarkPlugins = [
        ...(options.remarkPlugins ?? []),
        remarkGfm,
        remarkFrontmatter,
        [remarkTocHeadings, { exportRef: toc }],
      ];
      options.rehypePlugins = [
        ...(options.rehypePlugins ?? []),
        rehypeSlug,
        rehypeExternalLinks,
        rehypeAutolinkHeadings,
        [rehypePrismPlus, { ignoreMissing: true }],
      ];
      return options;
    },
    esbuildOptions: (options) => {
      options.loader = {
        ...options.loader,
        '.js': 'jsx',
      };
      return options;
    },
  });

  return {
    mdxSource: code,
    toc,
    frontMatter: frontmatter,
  };
};

3. 编写 MDXComponents

export const MDXComponents = {
  p: P,
  strong: Strong,
  blockquote: Blockquote,
  ol: OL,
  ul: UL,
  li: LI,
  table: Table,
  th: Th,
  td: Td,
  h1: H1,
  h2: H2,
  h3: H3,
  h4: H4,
  hr: Divider,
  a: Link,
  code: InlineCode,
  pre: CodeBlock,
};

编写这些组件的作用就是我们编写的这些组件会替换掉对应的标签,这个就需要根据你的实际需求去编写相应的组件就好了,以上是我提供的一个案例。

4. 使用我们自定义的组件 MDXComponents

export default function MDXLayout({ content, toc, routeTree }: MDXLayoutProps) {
  const MDXLayout: any = useMemo(() => getMDXComponent(content), [content]);
  return (
    <Docs
      toc={toc}
      routeTree={routeTree}
    >
      <div className='ml-0 max-w-4xl 2xl:mx-auto'>
        <MDXLayout components={MDXComponents} />
      </div>
    </Docs>
  );
}

5. 使用预编译函数实现预编译

export const getStaticProps: GetStaticProps<{
  frontMatter: StarterDocs;
  mdxSource: any;
  toc: any;
  menu: RouteItem;
}> = async ({ params }) => {
  const { starter, docs }: any = params;
  const menu: RouteItem = readJsonFileBySlug(SECTION, starter);
  const docsFile = readMarkdownFileBySlug<StarterDocs>(
    SECTION,
    starter,
    docs + '.md'
  );
  const post: any = await compileMDX(docsFile.content);
  const { mdxSource, toc } = post;
  return { props: { frontMatter: docsFile.data, mdxSource, toc, menu } };
};

实现 mdx 的实时编译

关于 mdx 的实时编译,也已经在上一小节做过详细介绍,这里就不再赘述,想要实现 mdx 的实时编译,可以按照如下步骤来实现。

1. 添加依赖

pnpm install @mdx-js/mdx
pnpm install @mdx-js/react

注意上面的两个依赖与预编译用到的依赖不同。

2. 编写实时编译函数

/**
 * 实时编译 mdx
 * @param mdx 文本
 * @param format,编译结果,可传 md | mdx
 */
export const compileAndRun = async (mdx: any, format: any = 'mdx') => {
  try {
    const remarkPlugins = [];
    remarkPlugins.push(remarkGfm);
    remarkPlugins.push(remarkFrontmatter);
    remarkPlugins.push(remarkMath);

    const c = await compile(mdx, {
      outputFormat: 'function-body',
      remarkPlugins,
      format: format,
      rehypePlugins: [rehypeKatex, [rehypePrismPlus, { ignoreMissing: true }]],
    });

    const x = await run(String(c), runtime);
    return { content: x.default, error: undefined };
  } catch (e: any) {
    return { content: undefined, error: e.message };
  }
};

这里需要注意一个地方就是,format 可以既可以传 mdx 也可以传 md,传 mdx 就是编译 mdx 文件,传 md 就是编译 markdown 文件,

传 markdown 不会出现编译报错,举个例子,如果你传 mdx,你在编辑器中上来就写个 <,那肯定会编译报错,

因为它会把 < 作为 jsx 代码标签来解析,不信可以在这里 去尝试.

而如果你传 md,那它就不会把 < 作为标签来解析,就不会报错,当然,你也可以在 这里 去试一试。

3. 使用实时编译函数

const ChatBot = ({ content, date }: ChatBotProps) => {
  const [{ Content }, setState] = useState<any>({
    Content: undefined,
  });

  useEffect(() => {
    compileAndRun(content, 'md').then(({ content, error }) => {
      setState((state: any) => ({
        Content: content || state.Content,
      }));
    });
  }, [content]);
  //...
};

4. 使用自定义组件

<div className='w-full'>
  {Content ? <Content components={MDXComponents} /> : null}
</div>

在使用实时编译时也可以使用之前编写的自定义组件达到复用代码的效果。

小节

mdx 的实时编译和预编译明显的不同就是安装的依赖不同,这个一定要注意。你可以在你的网站中既使用 mdx 的实时编译又使用 mdx 的预编译,这样你编写的自定义组件还能达到复用的效果。