likes
comments
collection
share

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

作者站长头像
站长
· 阅读数 21

前言

渲染 markdown到页面上有很多方式

第一种方式可以在 React 组件中,拿到 markdown 字符串,然后使用相关的 npm 依赖,将 markdwon 字符串转成 html 字符串,或者直接转成 React 组件,然后渲染到页面上

第二种方式借助 vite 插件解析 markdown,直接将 markdown 解析成一个 js 文件,项目就可以直接引入了

分析

第一种方式

第一种方式,完全依托于浏览器环境,功能也被限制在浏览器中,所做的很有限。

支持:

  1. markdown 转 html 字符串
  2. markdown 转 React 组件
  3. 部分 React 代码块渲染成组件
  4. 在线编辑

缺点:

  1. 不支持 <code src="./Foo.tsx"/>引用组件
  2. 不支持代码块中相对路径引用其他组件,仅支持 alias 引用
  3. 时刻维护一个 import 表,应对代码中会 import 的所有依赖

第二种方式

第二种方式,解锁巅峰开发体验
  1. 支持上面所有功能
  2. 支持<code src="./Foo.tsx"/>格式引用组件
  3. 支持代码块中,使用相对路径引用其他组件
  4. 不用手动维护一个 import 表,交由代码自动维护

步骤分析

这里只讲第二种方式

将第二种方式的实现步骤拆开来:

  1. 准备一个 react 项目,以及 md 文件
  2. 写一个 vite 插件,处理 md 后缀的文件,将文件变成 js 文件
  3. 写一个 rehype 插件,将 markdown 字符串变成 html 字符串
  4. 写一个 esbuild 函数,编译 vite 插件中生成的 js 代码

好,下面一步一步实现

初始化项目

npm create vite

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

删掉不必要的代码:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

准备一个 markdown 文件:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

# Foo

这是一个测试的markdown文件

然后在 App.tsx 文件中引入 Foo.md:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

import "./App.css";
import FooDoc from "./doc/Foo.md";

function App() {
  return (
    <>
      <FooDoc />
    </>
  );
}

export default App;

编译器报错, TS 不认识 md 后缀的文件:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

需要在.d.ts文件中声明该模块:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

现在 ts 认识了:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

启动项目:

npm run dev

不出意外,报错了:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

我们看看浏览器得到的是什么:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

毫无疑问,浏览器无法识别 md 字符串,所以会报错。

接下来我们要写一个 vite 插件,来处理 md 格式文件,将 md 文件变成 js 文件。

处理 md 后缀的文件

创建文件srcipt/vite/VitePluginMarkdown.ts:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

import { Plugin } from "vite";

const VitePluginMarkdown = (): Plugin => {
  return {
    name: "vite-plugin-markdown",
    transform(code, id) {
      if (id.endsWith(".md")) {
        return {
          code: "export default ()=>'I am a markdown doc.'",
        };
      }
    },
  };
};

export default VitePluginMarkdown;

这是一个简易的 vite 插件,其中的 transform 函数是用来处理不同格式文件的。我们可以在其中把 md 文件识别出来,然后返回给浏览器一串 js 代码,这样浏览器就不会报错了。

将该插件放到配置文件中:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

保存,刷新浏览器:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

显示成功,看看浏览器得到的内容:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

正好是我们插件返回的内容。

浏览器虽然显示成功了,但和实际的 markdown 没有关系。

这样可不行,下面处理实际的 markdown 内容。

Markdown 转 HTML

markdown 转 html,我们需要借助一些 npm 依赖:
npm i remark remark-gfm remark-rehype rehype-raw  rehype-stringify  -D

创建markdowm2html.ts:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

import { remark } from "remark";
import remarkGfm from "remark-gfm";
import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeStringify from "rehype-stringify";

const markdown2html = (code: string) => {
	return remark()
		.use(remarkGfm)
		.use(remarkRehype, { allowDangerousHtml: true })
		.use(rehypeRaw)
		.use(rehypeStringify, { allowDangerousHtml: true })
		.process(code);
};

export default markdown2html;

这里借用了诸多 npm 依赖,都是 unified 插件,用来处理 markdown 或者 html ,处理流程有点像 gulp 流水线,前一个插件处理完了,产物会交给下一个产物。

我来解释下这些插件都是做什么的:

  1. remark: 一个用于处理 Markdown 的库。它提供了一个插件系统,可以通过插件来扩展其功能。
  2. remark-gfm: 一个 Remark 插件,用于支持 GitHub Flavored Markdown (GFM) 语法扩展,例如表格、任务列表和删除线等。
  3. remark-rehype: 一个 Remark 插件,用于将 Markdown 抽象语法树 (AST) 转换为 HTML 抽象语法树 (AST)。
  4. rehype-raw: 一个 Rehype 插件,用于处理 HTML 字符串并将其转换为 Rehype 的 AST。它允许在 Markdown 中嵌入原始 HTML。
  5. rehype-stringify: 一个 Rehype 插件,用于将 Rehype 的 AST 转换为 HTML 字符串。

这里点到为止,更多内容可以查阅相关资料

总的来说,markdown2html 函数,就是将 markdown 字符串变成 html 字符串。

在 vite 插件中引入:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

打印结果:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

看着确实转换成功了。但是这个还不能直接给浏览器。要知道,Foo.md 是以 React 组件的形式被使用的,所以我们要保证给到浏览器的,也是一个 js 文件,且其中 export default 一个 React 组件。

React 组件有两种,类组件和函数组件。对于函数组件,说白了就是一个返回 React Element 的函数。

所以接下来,我们还要做两步

  1. 将上面的 html 字符串封装成一个 js 文件
  2. 将 jsx 转换成 React.createElement(), 因为浏览器不认识 jsx

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

script/esbuild/esbuildTransform.ts

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

import * as esbuild from "esbuild";

const esbuildTransform = (code: string) => {
  return esbuild.transform(code, {
    loader: "tsx",
    format: "esm",
  });
};

export default esbuildTransform;

首先组装字符串,然后将 _codeesbuild 编译,最后将编译结果返回给浏览器。esbuild 编译的代码很简单,且 esbuild 不需要额外 install,因为 vite 的开发依赖于 esbuild

看看效果:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

显示没有问题,浏览器收到的内容是:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

收到的内容确实是一个 js 文件,并且其中的 jsx 都被转成了 React.createElement

现在 md 文件可以像一个正常的 react 文件被其他的 react 文件引用了。但 md 文件中内容有些单调,没有代码块。

添加代码块:

# Foo

这是一个测试的markdown文件

## 这是第一个代码块

```ts
import React from 'react';

const Demo1 = ()=>{
  return <div>Demo1</div>
}

export default Demo1 
``


保存刷新浏览器:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

报错了。原因是代码块中的内容:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

代码块中的内容并没有被正确的识别为简单的字符串,而是作为了有意义的代码 token。

我们要专门处理下

处理代码块

思路:将整个 `code` 包裹的代码块提取出来,替换成 react 组件,这个组件专门用来显示代码。且显示的代码有语法高亮的效果。

先创建这个 react 组件 ShowCode.tsx

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

import { useEffect, useState } from "react";
import Prism from "prismjs";
import "prismjs/components/prism-typescript";

import "./index.scss";
import "prism-themes/themes/prism-one-light.min.css";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";

const ShowCode = (props: { code: string }) => {
  const [newCode, setCode] = useState("");

  useEffect(() => {
    const res = Prism.highlight(
      props.code,
      Prism.languages.typescript,
      "typescript"
    );
    setCode(res);
  }, [props.code]);

  return (
    <div
      className="show-code-container"
      dangerouslySetInnerHTML={{
        __html: newCode,
      }}
    ></div>
  );
};

export default ShowCode;

这个组件接受 code 参数,在 useEffect 中将代码进行高亮处理,并将处理后的代码显示出来。因为处理之后的代码含有 html 标签,所以放在了dangerouslySetInnerHTML 属性中。

语法高亮,借助了 prismjs,以及高亮主题我选择了 prism-themes 中的prism-one-light,最接近 vscode 中的高亮效果。

所以这里需要额外 install 依赖:

npm i prismjs prism-themes

替换的操作需要遍历 rehype hast(html ast), 接下来写一个 rehype 插件wrapperCodeBlock.ts,找到 code 节点(代码块节点),将 code 转换成 showcode 标签:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

import { visit } from "unist-util-visit";
const wrapperCodeBlock = () => {
  return (tree: import("unist").Node) => {
    visit(tree, "element", (node: any) => {
      if (node.tagName == "code" && node.children.length > 0) {
        const codeChildren = node.children[0];
        node.tagName = "showcode";
        node.properties = { code: codeChildren.value };
        node.children = [];
      }
    });
  };
};

export default wrapperCodeBlock;

代码很简单,就是字面的意思。找到 code,将 code 标签替换成 showcode 标签,并且将 code 中的代码提取出来,作为 showcode 的属性。在后面经过 Esbuild 编译后,这个属性会真正变成 react 组件的 props

wrapperCodeBlock 插件应用到 markdown 处理过程:

const markdown2html = (code: string) => {
    ...
    .use(rehypeRaw)
    .use(wrapperCodeBlock)
    .use(rehypeStringify, {
      allowDangerousHtml: true,
    })
    ...
};

位置有讲究,推荐位置如上

vite 插件中,增加 showCode 的 import:

const firstCharUpperCase = (str: string) => {
  return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
};

const handleTagName = (tagName: string) => {
  return tagName.toLowerCase();
};

const VitePluginMarkdown = (): Plugin => {
  return {
    name: "vite-plugin-markdown",
    async transform(code, id) {
      if (id.endsWith(".md")) {
        
        const htmlFile = await markdown2html(code);

        let _code = `
          import React from 'react';
          import Showcode from "@src/components/ShowCode"

          export default function(){
            return <>
              ${htmlFile.value}
            </>
          }
        `;

        _code = switchTagName(Object.keys(dynamicComponents), _code);

        console.log("_code: ", _code);

        const esbuildRes = await esbuildTransform(_code);

        return {
          code: esbuildRes.code,
        };
      }
    }
  };
};

为什么不直接转换成 ShowCode,而是经过一个大小写的过程,因为在上面 rehype 插件的替换过程,会将标签名首字母小写。所以 React 组件标签和 import 的组件名称,统一成首字母大写,其他字母小写。

如果你有更好的办法,请告诉我。

效果图:

🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能

代码被完美地显示了出来

代码地址:GitHub - zenoskongfu/markdown-doc

总结

这篇文章讲述了如何将 markdown 渲染成 html,其中借助了 rehype 插件,以及 vite 插件实现这一点。整个内容很简单的

下篇文章实现将 markdown 中的变成真正的组件,实现 react 组件嵌套功能。

转载自:https://juejin.cn/post/7416625208767905828
评论
请登录