Vitepress 自己写 MarkDown 组件
需要安装的包
- package.json
bash
{
"devDependencies": {
"@types/markdown-it": "^14.1.2",
"@types/markdown-it-container": "^2.0.11",
"@types/node": "^22.5.4",
"@vitejs/plugin-vue": "^5.1.2",
"postcss": "^8.4.47",
"sass": "^1.96.0",
"typescript": "^5.5.3",
"vite": "^5.4.1",
"vite-plugin-dts": "^3.7.3",
"vitepress": "^1.6.4",
"vue-tsc": "^2.0.29"
},
"dependencies": {
"element-plus": "^2.8.2",
"markdown-it": "^14.1.0",
"markdown-it-container": "^4.0.0",
"vue": "^3.5.3"
},
"scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
}
}写你自己需要展示的组件
提示
我这里拿 button 举例
(1) 新增展示用组件
- docs/examples/button/index.vue(展示组件调用)
vue
<script setup lang="ts">
const handleClick = () => {
window.alert("测试结果");
};
</script>
<template>
<button class="tk-button" @click="handleClick">primary</button>
</template>
<style lang="scss" scoped>
.tk-button {
color: #ffffff;
background-color: #395ae3;
box-shadow: 0 2px 0 rgba(5, 145, 255, 0.1);
font-size: 14px;
height: 32px;
padding: 4px 15px;
border-radius: 6px;
outline: none;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
touch-action: manipulation;
line-height: 1.5714285714285714;
&:hover {
background-color: #5a79f4;
}
}
</style>(2) 新增源代码展示组件
- docs/examples/button/index.md (查看源代码调用)
vue
<script setup lang="ts">
const handleClick = () => {
window.alert("hello");
};
</script>
<template>
<button class="tk-button" @click="handleClick">primary</button>
</template>
<style lang="scss" scoped>
.tk-button {
color: #ffffff;
background-color: #395ae3;
box-shadow: 0 2px 0 rgba(5, 145, 255, 0.1);
font-size: 14px;
height: 32px;
padding: 4px 15px;
border-radius: 6px;
outline: none;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
touch-action: manipulation;
line-height: 1.5714285714285714;
&:hover {
background-color: #5a79f4;
}
}
</style>开始写插件了
流程
- 先写调用,调用有 2 处
- 再写插件,插件有 2 处
- 最后写组件,组件有 1 处
(1) 调用
- 调用第一处 docs/.vitepress/theme/index.ts
js
/// <reference types="vite/client" />
import type { Theme } from "vitepress";
import DefaultTheme from "vitepress/theme";
import type { Component } from "vue";
// 增加codebox 组件
import mdVueDemo from "./plugins/Code/mdVueDemo";
const modules = import.meta.glob<Component>("../../examples/**/*", {
eager: true,
import: "default",
});
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.use(mdVueDemo, { modules });
},
} satisfies Theme;- 调用第二处 docs/.vitepress/config.mts
ts
import { defineConfig } from "vitepress";
// 新增加的
import mdVueDemoPlugin from "./theme/plugins/Code/mdVueDemoPlugin";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "My Awesome Project",
description: "A VitePress Site",
// 新增加的
markdown: {
config: (md) => {
md.use(mdVueDemoPlugin, { root: "docs" }); // 可以通过root指定vitepress启动目录,默认是docs
},
},
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Home", link: "/" },
{ text: "Examples", link: "/markdown-examples" },
],
sidebar: [
{
text: "Examples",
items: [
{ text: "Markdown Examples", link: "/markdown-examples" },
{ text: "Runtime API Examples", link: "/api-examples" },
],
},
],
socialLinks: [
{ icon: "github", link: "https://github.com/vuejs/vitepress" },
],
},
});(2) 插件
- 插件第一处 docs/.vitepress/theme/plugins/Code/mdVueDemoPlugin.ts
js
import markdownItContainer from "markdown-it-container";
import path from "path";
import fs from "fs";
interface Options {
root?: string;
}
export default (md: any, options?: Options) => {
md.use(markdownItContainer, "demo", {
validate: (params: string) => {
return params.trim().match(/^demo\s*(.*)$/);
},
render(tokens: any[], idx: number) {
const record = tokens[idx];
if (record.nesting === 1) {
// 获取到所有的传递过来的数据
const filePath = tokens[idx + 2].content;
let result = filePath.split("\n");
console.log(result);
let vuePath = "";
let vueDoc = "";
let vueLink = "";
result.forEach((item: any, index: number) => {
if (item.includes("vuePath")) {
vuePath = item.split("-")[1].trim();
}
if (item.includes("vueDoc")) {
vueDoc = item.split("-")[1].trim();
}
if (item.includes("vueLink")) {
vueLink = item.split("-")[1].trim();
}
});
// 读取路径
const sourcePath = path.resolve(options?.root || "docs", vueDoc);
const source = fs.readFileSync(sourcePath, "utf-8");
const componentName = vuePath.split(".")[0].replaceAll("/", "-");
const fileType = vuePath.split(".")[1];
const codeRender = encodeURIComponent(
md.render(`\`\`\` ${fileType}\n${source}\`\`\``)
);
return `<DemoContainer code="${codeRender}" link="${vueLink}" :expand="${record.info.includes(
"expand"
)}">
<template #source><${componentName}/></template>`; // 开始标签
} else {
// 处理结束标签
return "</DemoContainer>\n"; // 结束标签
}
},
});
};- 插件第二处 docs/.vitepress/theme/plugins/Code/mdVueDemo.ts
js
import DemoContainer from "../../components/CodeBox/index.vue";
type Options = {
modules: Record<string, any>,
};
export default (app: any, options: Options) => {
const components = Object.entries(options.modules).map(([path, module]) => {
const componentName = path
.replaceAll("../", "")
.replaceAll("./", "")
.replaceAll("/", "-")
.replace(/\.\w+$/, "");
return {
name: componentName,
component: module,
};
});
app.component("DemoContainer", DemoContainer);
components.forEach(({ name, component }) => {
app.component(name, component);
});
};(3) 容器组件
- docs/.vitepress/theme/components/CodeBox/index.vue
vue
<template>
<ElConfigProvider namespace="vd">
<div id="vp-demo">
<div class="vp-demo-source vp-raw">
<slot name="source" />
</div>
<div class="vp-demo-actions">
<ElTooltip
v-for="{ message, icon, onClick } in actions"
:content="message"
>
<div class="vp-demo-actions-icon" @click="onClick">
<component :is="icon" />
</div>
</ElTooltip>
</div>
<div v-if="visible" v-html="sourceCode"></div>
<div v-if="visible" class="hidecode" @click="hiddenCode">
<div class="arrowup">
<ArrowUp></ArrowUp>
</div>
<div class="codetext">隐藏源代码23</div>
</div>
</div>
</ElConfigProvider>
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } from "vue";
import CodeIcon from "./IconComponents/CodeIcon.vue";
import CopyIcon from "./IconComponents/CopyIcon.vue";
import ArrowUp from "./IconComponents/ArrowUp.vue";
import Refresh from "./IconComponents/Refresh.vue";
import Url from "./IconComponents/Url.vue";
import { ElMessage, ElTooltip, ElConfigProvider } from "element-plus";
import "./element.css";
const props = defineProps<{ code: string; expand: boolean; link: string }>();
const visible = ref(false);
const sourceCode = computed(() => decodeURIComponent(props.code));
const actions = [
{
message: "复制代码",
icon: CopyIcon,
onClick: () => {
navigator.clipboard.writeText(sourceCode.value).then(() => {
ElMessage.success("已复制!");
});
},
},
{
message: "查看源代码",
icon: CodeIcon,
onClick: () => (visible.value = !visible.value),
},
{
message: "刷新",
icon: Refresh,
onClick: () => {
window.location.reload();
},
},
{
message: "查看源代码",
icon: Url,
onClick: () => {
console.log("查看源代码");
console.log(props.link);
window.open(props.link, "_blank");
},
},
];
watchEffect(() => {
visible.value = props.expand;
});
// 隐藏代码
const hiddenCode = () => {
visible.value = false;
};
</script>
<style lang="scss">
#vp-demo {
border: 1px solid #ccc;
.vp-demo-source {
padding: 20px;
border-bottom: 1px solid #ccc;
}
.vp-demo-actions {
height: 50px;
padding: 0 15px;
// border-bottom: 1px solid #4c4d4f;
display: flex;
justify-content: right;
gap: 15px;
align-items: center;
.vp-demo-actions-icon {
cursor: pointer;
color: #999;
width: 1.2em;
}
}
.language-vue {
margin: 0;
border-radius: 0;
}
.hidecode {
text-align: center;
line-height: 45px;
font-size: 15px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
.arrowup,
.codetext {
color: #0078d4;
}
}
.arrowup {
flex: 0 0 16px;
height: 16px;
margin-right: 15px;
}
}
div[class*="language-"] {
margin: 0px !important;
}
}
</style>element.css 自己可以去官网上下载
IconComponents/CodeIcon.vue
剩下的 ArrowUp.vue, CopyIcon.vue, Refresh.vue, Url.vue 代码都是一样的
html
<template>
<svg
t="1765702973558"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="6381"
>
<path
d="M158.72 71.92064c-3.2 11.20256 12.89216 27.5968 25.6 30.72l97.28 20.48c-145.408 78.848-230.4 229.9392-230.4 397.03552 0 249.86624 206.5664 452.88448 460.8 452.88448s460.8-203.01824 460.8-452.88448a452.77696 452.77696 0 0 0-226.432-389.632c-11.136-6.25152-28.32896-3.75808-34.688 7.95648a28.08832 28.08832 0 0 0 10.24 35.84 400.24064 400.24064 0 0 1 199.68 348.16c0 224.1024-181.59104 399.36-409.6 399.36s-409.6-175.2576-409.6-399.36c0-155.38688 81.13152-295.58784 220.16-363.52l-25.6 128c-3.1744 12.4928 6.8864 24.71424 19.59424 27.83232a23.71072 23.71072 0 0 0 28.60544-17.1776l42.10176-175.68768v-5.46304a22.71744 22.71744 0 0 0-18.26816-22.64576l-178.75456-41.38496C171.27936 46.22336 161.92 60.7232 158.72 71.92064z"
p-id="6382"
fill="currentColor"
></path>
</svg>
</template>
<script setup lang="ts"></script>使用
bash
## 调用结果
## 使用
中间`-`不能省略 他就是分隔符号
- vuePath 就是渲染组建的文件
- vueDoc 就是渲染组件的文档
- vueLink 就是希望跳转到哪个链接
::: demo
vuePath - examples/button/index.vue
vueDoc - examples/button/index.md
vueLink - https://www.baidu.com
:::
哈哈哈 测试通过