在markdown中支持可交互组件
编写markdown文档时,有时候希望直接展示一个可交互的组件,类似于ElementUI
、Antd
等主流的组件库的文档交互。由于vite已经逐渐取代webpack称为组件库开发的流行方式,本文将研究一种使用vite插件实现在markdown中实现可交互组件的思路。
比如下面的markdown文件
# hello
这个是vue md 文件
```vue
import Button from './components/Button.vue'
```
期望在渲染这个文件的时候,能够将vue代码块作为一个组件展示。
这在编写各种组件库文档时是比较方便的。
本项目完整源码
参考
- vite 插件开发文档
- varlet-cli,本项目参考了varlet-cli的实现
处理markdown文件中的vue组件
要达到这个目的,就需要在解析的时候,将这种特殊的代码标记出来,然后将整个markdown作为一个组件文件导出。
vite插件的transform
钩子可以实现这个功能。
markdown转html可以用现成的工具markdown-it,这样我们就可以将md文件当做是vue组件了
const markdown = require("markdown-it");
function transformMarkdown(source) {
const md = markdown({
html: true,
});
const html = md.render(source);
// 拼一个sfc文件,然后交给vue插件处理相关的问题
const template = `
<template>
${html}
</template>
`;
return {
code: template,
};
}
module.exports = function () {
return {
name: "vite-plugin-markdown-extend",
enforce: "pre",
transform(source, id) {
if (!/\.md$/.test(id)) {
return;
}
return transformMarkdown(source);
},
};
};
拼完vue组件后,只需要使用vue插件进行解析和编译即可(可以通过vite vue插件的inclue配置)
然后在vite.config中配置插件
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import md from "vite-plugin-markdown-extend";
export default defineConfig({
plugins: [
md(),
vue({
include: [/\.vue$/, /\.md$/], // 注意这里把md文件也包含进来
}),
],
});
然后就可以把markdown文件当做vue组件引入了
import { createApp } from 'vue'
import readme from './readme.md'
createApp(readme).mount('#app')
解析markdown中的组件
在上面将md文件转成了vue组件,其核心思路就是markdown文件->html字符串->sfc文件->vue插件处理vue组件。
按照这个思路,如果要渲染markdown中的vue代码块,只需要在拼html字符串的时候,将代码块作为一个组件标签替换就行了
首先约定一下格式,比如下面这段markdown中的代码
# hello
这个是test md 文件
```vue
import Button from './components/Button.vue'
```
渲染出来的模板字符串为
<h1>hello</h1>
<p>这个是vue md 文件</p>
<Button />
其中vue的代码块会被编译成<Button></Button>
,这样就可以把代码块当做一个正常的Vue组件渲染出来了。
我们来实现这个解析
function parseComponent(source) {
const reComponent = /import (.+) from (['"]).+(\2)/g;
const reCodeBlock = /```vue((.|\r|\n)*?)```/g;
const imports = new Set();
const components = new Set();
source = source.replace(reCodeBlock, (_, code) => {
return code.replace(reComponent, (match, $1) => {
imports.add(match);
components.add($1);
return `<${$1} />`;
});
});
return {
imports: Array.from(imports),
components: Array.from(components),
source,
};
}
这样解析就可以获得
{
imports: [ "import Button from './components/Button.vue'" ], // 用在script头部引入
components: [ 'Button' ], // 组件名称,用在components局部组件配置项
source: '# hello\n\n这个是test md 文件\n\n\n<Button />\n' // 模板文件
}
得到了这些数据,再来拼接sfc模板
function markdown2vue(source) {
const { source: fSource, imports, components } = parseComponent(source);
const md = markdown({
html: true,
});
const html = md.render(fSource);
const template = `
<template>${html}</template>
<script>
// 引入依赖
${imports.join("\n")}
export default {
components: {
${components.join(",")} // 注册局部组件
}
}
</script>
`;
return {
code: template,
};
}
最后得到的sfc文件就是
<template>
<h1>hello</h1>
<p>这个是test md 文件</p>
<p>按钮</p>
<Button />
</template>
<script>
import Button from './components/Button.vue'
export default {
components: {
Button
}
}
</script>
然后再把这个文件交给vite/vue
处理就可以了,最后渲染结果如下图所示
大功告成!!
处理markdown中的react组件
我们可以按照相同的思路来支持在markdown中展示react组件。
其思路基本一致:markdown->含组件标记的html字符串->拼接(j|t)sx
文件->vite/react插件
按照这个思路实现时,碰到了一个问题是@vitejs/plugin-react
传入的inclue参数不支持选择其他类型的文件,比如下面的vite.config.js
配置
react({
include: [/\.jsx$/, /\.md$/],
}),
传入的\.md$
配置项并不会生效,导致在插件中将md文件转成jsx之后,会提示js语法错误。
因此不能简单的像vue那样直接把md文件转成jsx文件,
在插件中将md文件转成jsx之后,会提示js语法错误,还需要手动通过babel将jsx编译成js
const markdown = require("markdown-it");
const babel = require("@babel/core");
const jsx = require("@babel/plugin-transform-react-jsx");
const importMeta = require("@babel/plugin-syntax-import-meta");
function markdown2react(source) {
const { source: fSource, imports, components } = parseComponent(source);
const md = markdown({
html: true,
});
const html = md.render(fSource);
const template = `
import React from 'react';
${imports.join("\n")}
export default ()=>{
return (<div>${html}</div>)
}
`;
const plugins = [
importMeta,
jsx,
];
const result = babel.transformSync(template, {
babelrc: false,
ast: true,
plugins,
sourceMaps: false,
sourceFileName: "123",
configFile: false,
});
return {
code: result.code,
map: result.map,
};
}
需要注意的是,输出的js文件中需要手动加入import React from 'react';
剩下的流程就与Vue的基本一致了,我们甚至还可以扩展用来支持其他框架语法的功能。
一些约定
上面展示了Button
组件的基础使用。在实际项目中,组件往往提供了各种props,如何展示组件的props呢?
一种方案是增加新的语法标记,支持传入props、监听event等,这需要扩展新的标记,也不太方便在markdown中维护
因此为了简单,我们约定markdown组件只负责引入,不负责具体的使用。
如果需要展示下面类型的组件使用
<Button color="blue"></Button>
需要在一个vue文件中编写对应的代码,比如这个文件叫BlueButton.vue
<template>
<Button color="blue"></Button>
</template>
然后再markdown中编写如下代码,将demo组件引入进来即可
```vue
import BlueButton from './components/BlueButton.vue'
```
小结
本文实现了一种在markdown文件中直接展示vue或react组件的思路。主要是借助vite的transform钩子,将md文件替换成对应框架可以识别的组件,然后通过注册局部组件的方式渲染出来。
感觉整个思路还是比较清晰的,接下来可以尝试写一个类似于vitepress的脚手架工具,快速编写可交互的markdown文档。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。