目的
- 公共组件文档统一管理避免重复造轮子
- 拓展vitepress支持类似element-plus那样可以查看源码和例子参考如下图:
点击查看演示例子
如何编写文档?
docs/.vitepress/config.ts
配置Nav和Sidebar- 引入例子组建 如
import basic from './basic.vue'
:::demo [路径]
上编写组件路径如:::demo button/basic
:::demo [实例] :::
包裹实例 如:<basic></basic>
完整例子如下
<script setup>
# 引入例子组件
import basic from './basic.vue';
</script>
# 按钮
## 基础用法
# button/basic是docs/component相对路径必填,否则查看不了源码
:::demo button/basic
# 组件例子显示,内部会以slot插入
<basic></basic>
:::
友情链接
实现原理
- 编写markdown-plugin拦截
:::demo button/basic :::
中的button/basic
路径利fs.readFileSync
读取源文件 - vitepress支持vue组件引入importing-components-in-markdown,利用这个只要传入slot就可显示例子
注意:这个实现方式和element-plus的实现方式是有区别的,element-plus只传需要传路径,我的是传路径和组件,而且写法也有些不一样!!!当然本质原理一样都是编写markdown-plugin
具体实现
项目目录结构
1.准备工作
- node 16 +
- 要有
vitepress
基础,及简单配置,不懂去vitepress官网
2.关键库
vitepress
当前选用1.0.0-alpha.10markdown-it-container
更改markdown必要插件escape-html
格式化代码prismjs
格式化代码
3.拓展markdown-plugin
- 参考element-plus源码
// ref https://github.com/vuejs/vitepress/blob/main/src/node/markdown/plugins/highlight.ts
import escapeHtml from 'escape-html'
import prism from 'prismjs'
import path from 'path';
import fs from 'fs';
import mdContainer from 'markdown-it-container';
function wrap(code: string, lang: string): string {
if (lang === 'text') {
code = escapeHtml(code)
}
return `<pre v-pre style="margin: 0;"><code>${code}</code></pre>`
}
// 语法高亮
const highlight = (str: string, lang: string) => {
if (!lang) {
return wrap(str, 'text')
}
lang = lang.toLowerCase()
const rawLang = lang
if (lang === 'vue' || lang === 'html') {
lang = 'markup'
}
if (lang === 'md') {
lang = 'markdown'
}
if (lang === 'ts') {
lang = 'typescript'
}
if (lang === 'py') {
lang = 'python'
}
if (prism.languages[lang]) {
const code = prism.highlight(str, prism.languages[lang], lang)
return wrap(code, rawLang)
}
return wrap(str, 'text')
}
// 配置
export const markdownConfig = (md) => {
md.use(mdContainer, 'demo', {
validate(params) {
return !!params.trim().match(/^demo\s*(.*)$/);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1 /* means the tag is opening */) {
// 取出:::demo 后面的配置,即源码路径
const sourceFile = m && m.length > 1 ? m[1] : '';
const sourceFileToken = tokens[idx + 2];
// 源码文件路径
const filePath = path.resolve(
process.cwd(),
'docs/component',
`${sourceFile}.vue`
);
let source = '';
if (sourceFileToken.type === 'inline') {
source = fs.readFileSync(filePath, 'utf-8');
}
if (!source)
throw new Error(`Incorrect source file: ${sourceFile}`);
return `<Demo source="${encodeURIComponent(
highlight(source, 'vue')
)}" raw-source="${encodeURIComponent(
source
)}" >`;
} else {
return '</Demo>';
}
},
});
}
4.vitepress 配置config
# 关键代码
import { markdownConfig } from './plugins/markdown-plugin';
export default {
markdown: {
config: markdownConfig,
},
}
5.vitepress 配置theme
,添加Demo组件
import DefaultTheme from "vitepress/theme";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "prismjs/themes/prism-funky.min.css";
import VpDemo from '../src/vp-demo.vue';
export default {
...DefaultTheme,
enhanceApp: ({ app }) => {
app.use(ElementPlus);
app.component('Demo',VpDemo)
},
};
6.vp-demo 组件代码
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps({
source: {
type: String,
required: true,
},
});
const decoded = computed(() => {
return decodeURIComponent(props.source);
});
</script>
<template>
<div class="example-source-wrapper">
<div class="example-source" v-html="decoded" />
</div>
</template>
<style scoped lang="scss">
.example-source{
overflow-x: scroll;
position: relative;
color: #fff;
background-color: var(--vp-code-block-bg) !important;
padding: 1em;
}
</style>
7.vp-source-code组件代码
<script setup lang="ts">
import { useClipboard, useToggle } from "@vueuse/core";
import { CaretTop, DocumentCopy } from "@element-plus/icons-vue";
import { ElMessage } from 'element-plus';
import IconCopy from "./icon-copy.vue";
import IconCode from "./icon-code.vue";
import IconTop from "./icon-top.vue";
import Example from "./vp-example.vue";
import SourceCode from "./vp-source-code.vue";
const [sourceVisible, toggleSourceVisible] = useToggle();
const props = defineProps<{
source: string;
rawSource: string;
}>();
const { copy } = useClipboard({
source: decodeURIComponent(props.rawSource),
read: false,
});
const copyCode = async () => {
try {
await copy();
ElMessage({
message: '复制成功!',
type: 'success',
})
} catch (e: any) {}
};
</script>
<template>
<ClientOnly>
<div class="example">
<div class="example-showcase">
<slot></slot>
</div>
<div class="op-btns">
<ElTooltip content="复制代码" :show-arrow="false">
<ElIcon :size="16" class="op-btn" @click="copyCode">
<icon-copy></icon-copy>
</ElIcon>
</ElTooltip>
<ElTooltip content="查看源代码" :show-arrow="false">
<ElIcon :size="16" class="op-btn" @click="toggleSourceVisible()">
<icon-code></icon-code>
</ElIcon>
</ElTooltip>
</div>
<ElCollapseTransition>
<SourceCode v-show="sourceVisible" :source="source" />
</ElCollapseTransition>
<Transition name="el-fade-in-linear">
<div
v-show="sourceVisible"
class="example-float-control"
@click="toggleSourceVisible(false)"
>
<ElIcon :size="16">
<icon-top></icon-top>
</ElIcon>
<span>隐藏源码</span>
</div>
</Transition>
</div>
</ClientOnly>
</template>
<style scoped lang="scss">
.m-0 {
margin: 0;
}
.example-showcase {
padding: 1.5rem;
margin: 0.5px;
}
.example {
border: 1px solid var(--vp-c-divider-light);
border-radius: 4px;
.op-btns {
border-top: 1px solid var(--vp-c-divider-light);
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: flex-end;
height: 2.5rem;
.el-icon {
&:hover {
color: var(--vp-c-brand);
}
}
.op-btn {
margin: 0 0.5rem;
cursor: pointer;
color: var(--vp-c-text-2);
transition: 0.2s;
}
}
&-float-control {
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid var(--vp-c-divider-light);
height: 44px;
box-sizing: border-box;
background-color: var(--vp-c-bg, #fff);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
margin-top: -1px;
color: var(--vp-c-brand);
cursor: pointer;
position: sticky;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
span {
font-size: 14px;
margin-left: 10px;
}
&:hover {
color: var(--vp-c-brand-light);
}
}
}
</style>