Skip to content

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>

开始写插件了

流程

    1. 先写调用,调用有 2 处
    1. 再写插件,插件有 2 处
    1. 最后写组件,组件有 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

:::

哈哈哈 测试通过