🤔你知道如何渲染markdown到页面上吗第二种方式,解锁巅峰体验。只有你想不到,没有它做不到 1. 支持上面所有功能
前言
渲染 markdown到页面上有很多方式第一种方式可以在 React 组件中,拿到 markdown 字符串,然后使用相关的 npm 依赖,将 markdwon 字符串转成 html 字符串,或者直接转成 React 组件,然后渲染到页面上
第二种方式借助 vite 插件解析 markdown,直接将 markdown 解析成一个 js 文件,项目就可以直接引入了
分析
第一种方式
第一种方式,完全依托于浏览器环境,功能也被限制在浏览器中,所做的很有限。支持:
- markdown 转 html 字符串
- markdown 转 React 组件
- 部分 React 代码块渲染成组件
- 在线编辑
缺点:
- 不支持
<code src="./Foo.tsx"/>
引用组件 - 不支持代码块中相对路径引用其他组件,仅支持 alias 引用
- 时刻维护一个 import 表,应对代码中会 import 的所有依赖
第二种方式
第二种方式,解锁巅峰开发体验- 支持上面所有功能
- 支持
<code src="./Foo.tsx"/>
格式引用组件 - 支持代码块中,使用相对路径引用其他组件
- 不用手动维护一个 import 表,交由代码自动维护
步骤分析
这里只讲第二种方式
将第二种方式的实现步骤拆开来:
- 准备一个 react 项目,以及 md 文件
- 写一个 vite 插件,处理 md 后缀的文件,将文件变成 js 文件
- 写一个 rehype 插件,将 markdown 字符串变成 html 字符串
- 写一个 esbuild 函数,编译 vite 插件中生成的 js 代码
好,下面一步一步实现
初始化项目
npm create vite

删掉不必要的代码:

准备一个 markdown 文件:
# Foo
这是一个测试的markdown文件
然后在 App.tsx 文件中引入 Foo.md:
import "./App.css";
import FooDoc from "./doc/Foo.md";
function App() {
return (
<>
<FooDoc />
</>
);
}
export default App;
编译器报错, TS 不认识 md 后缀的文件:
需要在.d.ts
文件中声明该模块:
现在 ts 认识了:
启动项目:
npm run dev
不出意外,报错了:
我们看看浏览器得到的是什么:
毫无疑问,浏览器无法识别 md 字符串,所以会报错。
接下来我们要写一个 vite 插件,来处理 md 格式文件,将 md 文件变成 js 文件。
处理 md 后缀的文件
创建文件srcipt/vite/VitePluginMarkdown.ts
:
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 没有关系。
这样可不行,下面处理实际的 markdown 内容。
Markdown 转 HTML
markdown 转 html,我们需要借助一些 npm 依赖:npm i remark remark-gfm remark-rehype rehype-raw rehype-stringify -D
创建markdowm2html.ts
:
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
流水线,前一个插件处理完了,产物会交给下一个产物。
我来解释下这些插件都是做什么的:
remark
: 一个用于处理 Markdown 的库。它提供了一个插件系统,可以通过插件来扩展其功能。remark-gfm
: 一个 Remark 插件,用于支持 GitHub Flavored Markdown (GFM) 语法扩展,例如表格、任务列表和删除线等。remark-rehype
: 一个 Remark 插件,用于将 Markdown 抽象语法树 (AST) 转换为 HTML 抽象语法树 (AST)。rehype-raw
: 一个 Rehype 插件,用于处理 HTML 字符串并将其转换为 Rehype 的 AST。它允许在 Markdown 中嵌入原始 HTML。rehype-stringify
: 一个 Rehype 插件,用于将 Rehype 的 AST 转换为 HTML 字符串。
这里点到为止,更多内容可以查阅相关资料
总的来说,markdown2html
函数,就是将 markdown
字符串变成 html
字符串。
在 vite 插件中引入:
打印结果:
看着确实转换成功了。但是这个还不能直接给浏览器。要知道,Foo.md
是以 React
组件的形式被使用的,所以我们要保证给到浏览器的,也是一个 js 文件
,且其中 export default 一个 React
组件。
React 组件有两种,类组件和函数组件。对于函数组件,说白了就是一个返回 React Element 的函数。
所以接下来,我们还要做两步
- 将上面的 html 字符串封装成一个 js 文件
- 将 jsx 转换成
React.createElement()
, 因为浏览器不认识 jsx
script/esbuild/esbuildTransform.ts
:
import * as esbuild from "esbuild";
const esbuildTransform = (code: string) => {
return esbuild.transform(code, {
loader: "tsx",
format: "esm",
});
};
export default esbuildTransform;
首先组装字符串,然后将 _code
用 esbuild
编译,最后将编译结果返回给浏览器。esbuild 编译的代码很简单,且 esbuild
不需要额外 install,因为 vite
的开发依赖于 esbuild
看看效果:
显示没有问题,浏览器收到的内容是:
收到的内容确实是一个 js 文件,并且其中的 jsx 都被转成了 React.createElement
现在 md 文件可以像一个正常的 react 文件被其他的 react 文件引用了。但 md 文件中内容有些单调,没有代码块。
添加代码块:
# Foo
这是一个测试的markdown文件
## 这是第一个代码块
```ts
import React from 'react';
const Demo1 = ()=>{
return <div>Demo1</div>
}
export default Demo1
``
保存刷新浏览器:
报错了。原因是代码块中的内容:
代码块中的内容并没有被正确的识别为简单的字符串,而是作为了有意义的代码 token。
我们要专门处理下
处理代码块
思路:将整个 `code` 包裹的代码块提取出来,替换成 react 组件,这个组件专门用来显示代码。且显示的代码有语法高亮的效果。先创建这个 react 组件 ShowCode.tsx
:
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
标签:
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
渲染成 html
,其中借助了 rehype
插件,以及 vite
插件实现这一点。整个内容很简单的
下篇文章实现将 markdown 中的变成真正的组件,实现 react 组件嵌套功能。
转载自:https://juejin.cn/post/7416625208767905828