手撸组件库之文档工具
背景
因为最近失业中,便闲来无事,就整了个React组件库(不想面试),主要不喜欢antd,还有一些其他原因。也发过沸点,也有一些好奇实现方案的,所以便有了本文。
作为一个组件库,还是要有个文档的,所以搜了下市面的一些文档方案:
vitepressstorybookdumi
vitepress是基于vite和vue实现,基于SSR进行渲染,适合静态文档,如果需要支持react的话,需要魔改一些东西,懒得折腾。
至于storybook和dumi,个人原因不太喜欢。所以就打算自己撸一个,顺便了解下文档工具是怎么实现的。
这边和大部分组件库一样,都将通过markdown去实现文档工具,最终要把markdown渲染到页面上。
效果图
最终的一个效果图:

实现
要如何把markdown渲染到页面呢?
我们知道markdown最终会解析成html,可以根据这点做文章。由于我这里用的是webpack,所以将基于webpack进行说明。
webpack有个重要的概念是loader,loader的主要作用是对接收到的内容做转换处理,并返回出去,传递给下一个loader,而且每个loader的职责是单一的。所以我们需要实现loader,用于对markdwon做转换处理。
一个简单的loader
// 自己的loader
module.exports = functin mdLoader (source) {
return source
}
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.md$/,
use: path.resolve(__dirname, '../loader/mdLoader.js')
}
]
}
}
这边会用到几个库
- markdown-it 一个用于解析
markdown语法的库 - markdown-it-container 用于自定义
markdown语法 - markdown-it-anchor 给文档添加锚点
- markdown-it-toc-done-right 给文档添加目录
- gray-matter 支持在md写YAML Front matter
一个button组件的文档:

由于我们需要把markdown渲染到页面上,并且匹配对应路由,所以我们需要通过loader去实现一个组件。
module.exports = function mdLoader (source) {
// 初始化markdown-it
const md = new MarkdownIt({
html: true,
breaks: true,
typographer: true,
highlight(str, lang) {
// 高亮代码块
const template = `
<HighlightCode
lang={\`${lang}\`}
code={\`${str}\`}
>
</HighlightCode>
`
return template
}
})
const mdToHtml = md
.render(source)
.replace(/<hr>/g, '<hr />')
.replace(/<br>/g, '<br />')
.replace(/class=/g, 'className=')
// 返回的react component 用于做路由
return `
import * as React from 'react'
import Block from 'Block'
import { CodeExample } from 'CodeExample'
import { HighlightCode } from 'bangumi-ui'
function MdReact () {
return <div className='b-md-container markdown-body'>${mdToHtml}</div>
}
export default MdReact
`
}
上面就是我们实现的一个react component,并且通过markdown-it把markdown渲染成了html,并放入了我们写好的一个react component字符串中。
目前这个东西算是完成了一小部分,但是这样依然是不够的,我们还需要改造下webpack config。
// webpack.config.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.md$/,
use: ['babel-loader', path.resolve(__dirname, '../loader/mdLoader.js')]
}
]
}
// ...
}
现在的话应该是这个样子,这里使用babel-loader的主要作用是通过把我们自己写好的loader处理后的结果传递给babel-loader,babel-loader会进一步转换,变成可以被React所认识的格式。
至此,你所写的markdown就可以被react所渲染了,只要配好router就可以。
至于anchor、toc参考github文档说明,这里简单说下container,container的作用是自定义代码块,这点在vitepress、vuepresss有被使用,指warning、success、danger这是里面提前定义好的内容。
这是container的语法,demo是你自定义的名字。
::: demo
:::
实现:
const md = new MarkdownIt({
html: true,
breaks: true,
typographer: true,
highlight(str, lang) {
const template = `
<HighlightCode
lang={\`${lang}\`}
code={\`${str}\`}
>
</HighlightCode>
`
// process.exit()
return template
}
})
.use(container, 'desc', {
// 渲染时候会执行该函数,tokens是解析后的一个语法结构,index是当前的一个开始和结束下标
render(tokens, index) {
if (tokens[index].nesting === 1) {
// opening tag 开始标记
return `<Block>`
} else {
// closing tag 结束标记
return '</Block>'
}
}
})
还有一个是gray-matter,这个是因为组件在渲染的时候,会找不到该组件,我用来自定义引入的。
module.exports = function mdLoader (source) {
// 初始化markdown-it
const md = new MarkdownIt({
html: true,
breaks: true,
typographer: true,
highlight(str, lang) {
// 高亮代码块
const template = `
<HighlightCode
lang={\`${lang}\`}
code={\`${str}\`}
>
</HighlightCode>
`
return template
}
})
const { content, data } = matter(source)
const mdToHtml = md
.render(content)
.replace(/<hr>/g, '<hr />')
.replace(/<br>/g, '<br />')
.replace(/class=/g, 'className=')
// 返回的react component 用于做路由
return `
import * as React from 'react'
import Block from 'Block'
import { CodeExample } from 'CodeExample'
import { HighlightCode } from 'bangumi-ui'
${data.import}
function MdReact () {
return <div className='b-md-container markdown-body'>${mdToHtml}</div>
}
export default MdReact
`
}
至此一个loader的编写就算结束了,这是一个完整的代码。

end
这篇文主要是说了下markdown是如何被渲染的,简单的了解下组件库的文档工具都是一个怎么样的实现方式,并且也撸了一个loader用于解析我们的md文件。顺便也提到了loader的工作原理。
转载自:https://juejin.cn/post/7002898476388319269