🚩 背景
在 Vue3 业务项目中,常见做法是将复用组件集中放到 src/components
目录。但随着多人并行开发,逐渐出现以下痛点:
- 🤷♂️ 不知道已有封装(重复造轮子)
- 🧪 组件封装质量参差不齐,缺乏复用指引
- 📄 大量组件无使用文档 / 无交互示例
- 🔍 逐个打开文件效率低
- 🗣️ 口头沟通成本高,给人添麻烦
引入独立组件库(例如 storybook / docs site)成本过高,不符合仅为"项目内业务组件"做快速可见化的诉求,因此需要一个"足够轻"且"低侵入"的内部文档解决方案。
🎯 目标(Design Goals)
目标 | 说明 |
---|---|
低侵入 | 不新增独立入口,不污染生产包体 |
零上手成本 | 开发者只需新增/维护 .md 文件 |
自动化收集 | 自动扫描 components 下 Markdown 文档 |
支持热更新 | 开发态修改文档立即生效 |
支持组件示例 | Markdown 内可内联 Vue 组件预览 |
平滑演进 | 未来可拓展"示例 + 源码复制 + 搜索"等功能 |
🧩 方案概述
核心思想:仅在开发环境动态注入一个内部路由 /playDoc
,该页面会:
- 使用
import.meta.glob
递归扫描src/components/**/*.md
- 借助
unplugin-vue-markdown
将.md
编译为 Vue 组件 - 将 Markdown 渲染为动态组件并支持切换
- 后续扩展:内联示例、源码折叠、预览/复制等
✅ 优势:无需建立二次入口、无需新开端口、无需发布,生产环境自动剔除。
最初的想法是做成多入口文件,单独启动预览,实践中发现有点复杂,除了要加一套入口文件和项目配置外,有的依赖包必须要在 vite.config.dev.ts
中导入,否则影响构建,改动较多所以放弃了。
🏗️ 实现步骤
1. 安装依赖
bash
pnpm add -D unplugin-vue-markdown
2. Vite 插件配置
ts
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import Markdown from "unplugin-vue-markdown/vite";
export default defineConfig(({ mode }) => {
const isDev = mode === "development";
return {
plugins: [
vueJsx(),
vue({
include: [/\.vue$/, /\.md$/], // 让 .md 也走 Vue 编译
}),
isDev &&
Markdown({
// 最简单就是什么都不配置,也可根据文档按需扩展 markdown-it 插件
// headEnabled: false,
// wrapperClasses: "md-doc-body",
// markdownItSetup(md) {
// 示例:支持 :::tip 容器、目录、task list 等
// md.use(require("markdown-it-anchor")).use(require("markdown-it-task-lists"));
},
}),
]
};
});
3. 类型声明
src/types/shims.d.ts
ts
declare module "*.vue" {
import type { Component } from "vue";
const component: Component;
export default component;
}
declare module "*.md" {
import type { Component } from "vue";
const component: Component;
export default component;
}
4. 动态开发路由注入
ts
import type { RouteRecordRaw } from "vue-router";
const baseRoutes: RouteRecordRaw[] = [
// ...你的真实业务路由
];
const devDocRoute: RouteRecordRaw[] =
import.meta.env.DEV
? [
{
path: "/playDoc",
name: "PlayDoc",
component: () => import("@/components/PlayDoc.vue"),
meta: { hidden: true, title: "组件文档" },
},
]
: [];
export default [...baseRoutes, ...devDocRoute];
5. 文档页面组件(核心实现)
创建 src/components/PlayDoc.vue
,组件内容借助 AI 实现。(简单示例)
vue
<template>
<div class="play-doc">
<div class="sidebar">
<h3>组件文档</h3>
<ul class="doc-list">
<li
v-for="doc in docFiles"
:key="doc.path"
:class="{ active: currentDoc === doc.path }"
@click="loadDoc(doc)"
>
{{ doc.name }}
</li>
</ul>
</div>
<div class="content">
<div v-if="currentDocComponent" class="doc-content">
<component :is="currentDocComponent" />
</div>
<div v-else class="empty">选择一个文档查看</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import "element-plus/dist/index.css";
interface DocFile {
name: string;
path: string;
module: () => Promise<any>;
}
const docFiles = ref<DocFile[]>([]);
const currentDoc = ref<string>("");
const currentDocComponent = ref<any>(null);
// 动态获取 components 目录下的 md 文件
const getDocFiles = () => {
const modules = import.meta.glob("/src/components/**/*.md");
console.log(modules, "modules");
const files: DocFile[] = [];
Object.entries(modules).forEach(([path, moduleLoader]) => {
const name = path.split("/").pop()?.replace(".md", "") || "";
files.push({
name,
path,
module: moduleLoader as () => Promise<any>,
});
});
docFiles.value = files;
if (files.length > 0) {
loadDoc(files[0]); // 默认加载第一个文档
}
};
const loadDoc = async (doc: DocFile) => {
try {
currentDoc.value = doc.path;
const module = await doc.module();
currentDocComponent.value = module.default;
} catch (error) {
console.error("加载文档失败:", error);
}
};
onMounted(() => {
getDocFiles();
});
</script>
6. 示例组件文档(开发者需要编写的 .md)
md
# FancyButton 按钮
用于演示的业务按钮组件。
## ✅ 特性
- 支持主题类型
- 支持加载状态
- 自动继承原生 button 属性
## 🔌 基础用法
.
.
.
.
.
📂 目录结构
bash
src/
components/
PlayDoc.vue # 文档入口(仅开发态路由引用)
FancyButton/
index.vue
FancyButton.md # 组件文档
UserAvatar/
index.vue
UserAvatar.md
charts/
BarChart.vue
BarChart.md
命名规范:
- 每个"可复用业务组件"目录下放置同名
.md
- 无文档的组件会在后续统计中提示(可扩展自动检测)
注意事项和拓展:
项 | 说明 |
---|---|
生产环境剔除 | 路由通过 import.meta.env.DEV 条件控制 |
风格隔离 | PlayDoc.vue 设置样式时,不要影响到引入的子组件 |
Markdown 能力 | 学习下插件用法,或集成别的插件,增强代码预览 |
✅ 总结
该方案通过"开发态路由 + Markdown 编译为 Vue 组件"实现了一个:
- 不额外开启端口
- 不改变生产构建
- 几乎零上手成本
- 可持续迭代增强
的内部组件文档系统。适合业务项目在"尚未抽象到组件库层级"的组件复用与提效。
🚀 先让文档"存在且可见",再逐步"结构化 + 自动化"。
后续继续补充......