使用markdown快速搭建React组件库文档网站-背景希望能够像写markdown一样编写组件库文档,自动生成组件库文档网站。简单效果演示第一步,配置markdown-s
背景
希望能够像写 markdown 一样编写组件库文档,自动生成组件库文档网站。
简单效果演示
第一步,配置 markdown-site-loader
{
test: /\.md$/,
use: [
{
loader: "markdown-site-loader",
options: {
codeOutputPath: CODE_OUTPUT_PATH,
},
},
],
},
其中 codeOutputPath 是提取的代码的输出路径。
它的内容是 markdown-site-loader 自动输出的,包含 markdown 里面所有的代码片段,以及 index.json 配置文件,你可以通过这个配置文件查找到相应的代码片段。
.
├── 271100111991154798117116116111110471151169711411646109100.tsx
├── 27147113117105991078311697114116471151169711411646109100.tsx
├── 91100111991154798117116116111110471151169711411646109100.tsx
├── 9111547113117105991078311697114116471001011159946109100.tsx
├── 9147113117105991078311697114116471151169711411646109100.tsx
├── 919947100111991154798117116116111110479811611046109100.tsx
└── index.json
第二步,编写 config.ts
这其实也是最后一步,编写一份 config.ts 文件。在文件中指定网站导航的标题(title),路由路径(path),markdown 文件路径(MDPath)。markdown-site 会自动为你做好 markdown 文件查找、解析及渲染逻辑,你只需要专注使用 markdown 编写你的文档内容就可以了!
export default [
{
title: "介绍",
children: [
{
title: "快速上手",
path: "/",
MDPath: "./docs/quickStart/start.md",
},
{
title: "描述",
path: "/quickStart/desc",
MDPath: "./docs/quickStart/desc.md",
},
],
},
{
title: "Button",
children: [
{
title: "开始",
path: "/button/start",
MDPath: "./docs/button/start.md",
},
{
title: "按钮使用",
path: "/button/btn",
MDPath: "./docs/button/btn.md",
},
],
},
];
比如 /docs/quickStart/start.md 的内容如下:
# Dialog
弹窗
## 例子
~~~code
import React from "react";
import { Dialog, Button } from "zent";
const { openDialog } = Dialog;
const Demo: React.FC = () => {
const open = () => {
openDialog({
title: "title1100",
children: ,
});
};
return (
);
};
export default Demo;
~~~
在网站中的呈现效果如下(样式可以自己定制):
技术选型
- unified 让你使用语法树处理 markdown 文本
- react-markdown 将 markdown 渲染成 React 组件
- React.lazy 异步加载组件文件
原理简介
首先使用 unified 编写一个 webpack loader,将 markdown 中的代码提取出来。类似如下的代码片段会被提取出来:
~~~code
import React from "react";
import { Button} from "zent";
const Demo: React.FC = () => {
return ;
};
export default Demo;
~~~
然后使用 react-markdown 将 markdown 渲染成 React 组件(即 html),呈现在网站中。
最后是最关键的一步,由于 react-markdown 在渲染的时候可以判断文本的 language。
比如
~~~code
一些代码...
~~~
文本的 language 就是 code。
所以我们可以在判断 language = code 时,除了正常展示代码以外,再利用前面提取出来的代码,使用 React.lazy 将代码编写的 React 组件也渲染出来。这样就实现了文档网站 即可以显示示例代码,也可以显示示例代码的 UI 及交互效果
的功能。
实现详解
工程由三部分组成。
markdown-site-front 负责网站的展示,如导航、布局、文档内容渲染等。
markdown-site-loader 用来编译 markdown 文件,提取 markdown 中的代码,生成代码配置文件。
markdown-site-shared 没什么特别需要介绍的,存放一些公共常量、ts 类型
三个部分(包)使用 lerna 管理。
├── markdown-site-front
├── markdown-site-loader
└── markdown-site-shared
markdown-site-loader
获取 markdownParser
// markdownParser
import unified from "unified";
import remarkParse from "remark-parse";
export default unified().use(remarkParse).freeze();
获取 markdown 语法树
module.exports = function (source: string) {
const ast = markdownParser.parse(source);
return `export default ${JSON.stringify(source)};`;
};
提取代码,将代码写入 markdown-site-loader option 配置的 codeOutputPath 目录下。为了能够查找到相应的代码文件,同时需要生成一份配置文件(writeCodeConfig)。
const codeConfig: ICodeConfigItem[] = [];
ast.children.forEach((child: IASTChild) => {
const { type, value } = child;
// CODE_IDENTIFIER(default)= "code"
if (type === CODE_IDENTIFIER) {
const position = child.position.start;
const codeFileName = genCodeFileName(resourcePath, position);
writeCodeFile(codeOutputPath, codeFileName, value);
codeConfig.push({
position,
resourcePath,
codePath: `${codeFileName}.tsx`,
});
}
});
writeCodeConfig(codeOutputPath, JSON.stringify(codeConfig));
genCodeFileName 是为了生成代码文件的唯一名称。使用 positionStr + pathCharCodeStr 生成。
import { IASTPosition } from "markdown-site-shared";
const PATH_LIMIT = 20;
const genCodeFileName = (resourcePath: string, position: IASTPosition) => {
const positiOnStr= `${position.line}${position.column}`;
const pathList = resourcePath.split("");
const len = pathList.length;
const pathCharCodeStr = pathList
.slice(len - PATH_LIMIT, len)
.map((char) => char.charCodeAt(0))
.join("");
return positionStr + pathCharCodeStr;
};
export default genCodeFileName;
markdown-site-front
markdown-site-front 的核心逻辑都在 Markdown.tsx 中(后面考虑把它提出来)
首先它会根据传入的 markdown 文件路径获取 markdown 里的内容。
/**
* 1. 动态引入路径不支持传变量,所以用模板字符串
* 2. loader 路径匹配需要后缀,所以用 .md 结尾
* */
const getMarkdown = (path) => {
const pathWithoutSuffix = path.replace(/\.md$/, "");
return new Promise((resolve) => {
import(`${pathWithoutSuffix}.md`).then((module) => resolve(module.default));
});
};
const Markdown: React.FC = ({ path }) => {
const [content, setContent] = useState("");
useEffect(() => {
path && getMarkdown(path).then((content) => setContent(content));
}, []);
if (!content) {
return null;
}
};
export default Markdown;
然后借助 react-markdown 提供的能力,判断 language = "code" 时,除了正常高亮展示代码以外
再根据 codeConfig 获取相应代码的内容(getCodeConf),再使用 React.lazy 展示代码表示的 React 组件。这样就实现了文档既有代码示例,又有相应的UI及交互的功能了!
至此,markdown-site 所有的关键逻辑就介绍完毕了。
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import codeConfig from "./codeDist/index.json";
import { CODE_IDENTIFIER, IASTPosition } from "markdown-site-shared";
const getCodeCOnf= (path: string, position: IASTPosition) => {
const comparePath = path
.split("/")
.filter((item) => !/^\.+$/.test(item))
.join("/");
const cOnf= codeConfig.find((item) => {
return (
item.resourcePath.endsWith(comparePath) &&
item.position.offset === position.offset
);
});
return conf;
};
const Markdown: React.FC = ({ path }) => {
return (
import(`./codeDist/${conf?.codePath}`)
);
return (
<>
>
);
} else {
return {children}
;
}
},
}}
/>
);
};
export default Markdown;
源代码地址