今天是2024年的最后一天,在这里提前祝大家2025新年快乐!
年终将至,很多小伙伴可能正在忙着准备公司年终总结和汇报。大家年初定下的KPI完成得怎么样了?我已经顺利完成了所有任务,并完成了汇报。在我的年终汇报中,作为一名程序员,我没有单纯地列举写了多少代码,做了哪些库,而是将所有工作通过 VitePress 进行了整合和包装。在汇报时,我直接拿出精心准备的文档,轻松展示了一番,效果还不错哦 😂
言归正传,接下来,我将全面介绍VitePress,并与大家分享如何用它展示组件库文档中的组件示例功能。
一、什么是 VitePress
VitePress 是由 Vue.js 官方团队开发的一款基于 Vite 的静态站点生成器。它通过 Vue 3 提供了现代化的开发体验,并且专注于文档的构建,支持 Markdown 格式的内容编辑。
- 性能优异:基于 Vite,启动和构建速度非常快。
- 支持 Vue 3:可以轻松将 Vue 组件嵌入文档中,适合开发者编写互动式文档。
- 简单易用:配置简洁,易于上手,适合快速构建小型站点。
二、VitePress 环境搭建
1. 安装 VitePress
首先,确保你已经安装了 Node.js 18 及以上版本。
运行下面的命令,在你的项目中安装 VitePress:
npx vitepress init
需要回答一些问题:
┌ Welcome to VitePress!
│
◇ Where should VitePress initialize the config?
│ ./docs
│
◇ Site title:
│ My Awesome Project
│
◇ Site description:
│ A VitePress Site
│
◆ Theme:
│ ● Default Theme (Out of the box, good-looking docs)
│ ○ Default Theme + Customization
│ ○ Custom Theme
└
或者,如果你想将其添加到现有项目中,运行下面的命令,但是这不会自动帮你创建目录。
npm add -D vitepress
2. 创建基本项目结构
如果正在构建一个独立的 VitePress 站点,可以在当前目录 (./) 中搭建站点。但是,如果在现有项目中与其他源代码一起安装 VitePress,建议将站点搭建在嵌套目录 (例如 ./docs) 中,以便它与项目的其余部分分开。
假设选择在 ./docs 中搭建 VitePress 项目,生成的文件结构应该是这样的:
.
├─ docs
│ ├─ .vitepress
│ │ └─ config.js
│ ├─ api-examples.md
│ ├─ markdown-examples.md
│ └─ index.md
└─ package.json
docs 目录作为 VitePress 站点的项目根目录。.vitepress 目录是 VitePress 配置文件、开发服务器缓存、构建输出和可选主题自定义代码的位置。
3. 启动开发服务器
在项目根目录下运行如下命令:
npx vitepress dev docs
服务启动后,可以运行控制台输出的地址,默认是 http://localhost:5173。
4. 配置启动命令
将如下命令配置到你的 package.json 中。
{
...
"scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
...
}
三、VitePress 基础配置
VitePress 的核心配置文件是 .vitepress/config.ts,它允许你自定义站点的基础设置,包括站点标题、描述、导航栏、侧边栏等内容。这些配置项让你能够快速定制文档站点的外观与结构。
1. 配置文件示例
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
// 站点的标题
title: '文档标题',
// 站点的描述
description: '文档描述',
themeConfig: {
// 导航栏配置
nav: [
{ text: '首页', link: '/' },
{ text: '指南', link: '/guide' }
],
// 侧边栏配置
sidebar: {
'/': [
{ text: '首页', link: '/' },
{ text: '指南', link: '/guide' }
]
}
}
})
在上述配置中,你可以:
- 设置站点的
title和description,分别用于定义文档站点的标题和描述。 - 配置
themeConfig来定制导航栏(nav)和侧边栏(sidebar),让文档的结构更加清晰和易于导航。
2. 自定义主题与样式
除了基础配置,VitePress 还支持主题自定义。你可以在 .vitepress/theme/ 目录下创建自定义的样式文件或 Vue 组件,从而调整站点的布局、颜色、字体等外观。
例如,你可以在 index.ts 文件中修改主题:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import './custom.css' // 引入自定义样式
export default {
...DefaultTheme, // 保留默认主题的功能和样式
}
通过这种方式,你不仅能定制文档站点的基本结构,还能调整视觉效果,创建符合需求的独特外观。
四、国际化站点配置
VitePress 内置了国际化支持,配置过程非常简单,无需额外插件。只需要调整目录结构并更新 config.ts 配置文件,即可实现多语言支持。
1. 调整目录结构
首先,按需调整 docs 目录来划分不同语言版本的文档。例如,以下是一个典型的目录结构:
docs/
├─ es/
│ ├─ foo.md
├─ fr/
│ ├─ foo.md
├─ foo.md
为了更清晰的组织不同语言的内容,我们可以将文档目录调整为:
docs/
├─ en/
│ ├─ index.md
├─ zh/
│ ├─ index.md
2. 拆分配置文件
为了更好地管理不同语言的配置,可以将 .vitepress/config.ts 文件拆分成多个文件,每个文件负责一种语言的配置。最终的目录结构如下:
.vitepress/
├─ config/
│ ├─ zh.ts
│ ├─ en.ts
│ ├─ shared.ts
│ ├─ index.ts
拆分后的配置文件变得更加清晰和可扩展,尤其对于大型项目,能提高配置的可维护性与复用性。
3. 入口文件配置
在 .vitepress/config/index.ts 中导入并合并各个语言的配置,以及共享配置,最终输出完整的站点配置。
// .vitepress/config/index.ts
import { defineConfig } from 'vitepress'
import { en } from './en'
import { shared } from './shared'
import { zh } from './zh'
export default defineConfig({
...shared,
locales: {
root: { label: 'English', ...en },
zh: { label: '中文', ...zh },
},
})
4. 共享配置文件
共享配置文件 shared.ts 包含了所有语言通用的设置,比如站点的标题、favicon、社交链接等。它可以提高配置的复用性。
// .vitepress/config/shared.ts
import { defineConfig } from 'vitepress'
export const shared = defineConfig({
title: '文档标题',
rewrites: {
'en/:rest*': ':rest*',
},
lastUpdated: true,
cleanUrls: true,
metaChunk: true,
head: [
['link', { rel: 'icon', href: '/favicon.ico' }],
['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:locale', content: 'en' }],
['meta', { property: 'og:title', content: '文档标题' }],
['meta', { property: 'og:site_name', content: '文档标题' }],
['meta', { property: 'og:image', content: '/logo.png' }],
],
themeConfig: {
logo: '/logo.png',
socialLinks: [
{ icon: 'github', link: 'https://github.com/kieranwv' },
],
search: {
provider: 'local',
},
},
markdown: {
theme: {
light: 'github-light',
dark: 'github-dark',
},
},
})
5. 中文文档配置
zh.ts 配置文件专门用于中文文档的配置,它包括语言设置、描述、导航、侧边栏等内容。
// .vitepress/config/zh.ts
import { defineConfig } from 'vitepress'
import pkg from '../../../package.json'
export const zh = defineConfig({
lang: 'zh-CN',
description: '文档描述',
themeConfig: {
editLink: {
pattern: '',
text: '在 GitHub 上编辑此页面',
},
nav: [
{ text: '顶部导航', link: '/zh/01/01', activeMatch: '/zh/01/' },
{ text: '顶部导航', link: '/zh/02/01', activeMatch: '/zh/02/' },
{
text: `v${pkg.version}`,
items: [
{
text: 'Changelog',
link: '',
},
],
},
],
sidebar: [
{
text: '侧面导航',
collapsed: false,
items: [
{ text: '标题', link: '/zh/01/01' },
{ text: '标题', link: '/zh/01/02' },
],
},
{
text: '侧面导航',
collapsed: false,
items: [
{ text: '标题', link: '/zh/02/01' },
],
},
],
footer: {
message: '基于 MIT 许可证发布。',
copyright: '版权 © 2024-present Kieran Wang',
},
},
})
6. 英文文档配置
对于英文文档,只需修改与语言相关的内容,其他部分可以复用中文配置的内容。
7. 文档书写
完成配置后,只需根据新的目录结构在 docs/ 目录下编写各语言版本的文档。每个语言版本都拥有独立的 Markdown 文件,可以灵活地管理和维护不同语言的内容。
五、使用 Vue 组件
在组件库的文档中,通常会提供 可交互的组件示例 和 源代码示例,以便帮助用户理解组件的使用方法,并能直接复制到自己的项目中。VitePress 提供了一种灵活的方式来展示这些组件及其源代码。
1. 在 VitePress 中使用 Vue 组件
VitePress 使用 Vue 单文件组件(SFC)处理 Markdown 文件,这意味着在 Markdown 中可以直接使用 Vue 的功能,包括动态模板、Vue 组件和通过 <script> 标签添加逻辑。因此,展示 Vue 组件的方式非常简单。
示例:在 Markdown 文件中使用 Vue 组件
首先,你可以直接在 Markdown 文件中编写 Vue 组件的代码。下面是一个包含动态模板和样式的示例:
---
hello: world
---
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
## Markdown 内容
当前计数值是:{{ count }}
<button :class="$style.button" @click="count++">点击增加</button>
<style module>
.button {
color: red;
font-weight: bold;
}
</style>
在这个例子中,<script setup> 用于引入 Vue 的 ref,并通过一个按钮来增加计数值。<style module> 用于模块化样式。
2. 在 Markdown 中使用 Vue 组件
VitePress 允许直接在 .md 文件中导入并使用 Vue 组件。例如,创建一个 Vue 组件 MyButton.vue:
<!-- MyButton.vue -->
<template>
<button @click="increment">{{ count }}</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
接着,在 Markdown 文件中引用这个组件:
<script setup>
import MyButton from '../../components/MyButton.vue'
</script>
# 文档内容
这是一个使用自定义组件的文档示例:
<MyButton />
## 更多文档内容
...
VitePress 会自动识别并渲染该 Vue 组件,显示组件的实际效果。只要组件文件正确导入,VitePress 就会处理它并在文档中呈现。
3. 显示源代码
除了展示可交互的组件外,VitePress 还允许在文档中展示源代码。例如,如果你想展示上面的 Vue 组件源代码,可以使用 VitePress 的内置代码高亮功能。
VitePress 会自动高亮显示代码块,并使其更具可读性。
更多高亮效果,参考 vitepress.dev/zh/guide/ma…。
4. 全局注册 Vue 组件
如果你希望在所有文档中都使用某个组件,可以将其注册为全局组件。在 VitePress 中,你可以通过扩展默认主题来实现这一点。具体的注册方式可以参考 VitePress 官方文档。
六、使用插件和自动化展示 Vue 组件
在前端组件库的文档中,我们经常见到组件的示例和代码。
为了在 VitePress 中展示可交互的 Vue 组件及其源代码,我们将使用两个插件:markdown-it 和 markdown-it-container。这两个插件将帮助我们在 Markdown 中嵌入自定义组件,并且支持高亮显示源代码。
作者实现的效果如下:
1. 安装插件
首先,安装 markdown-it 和 markdown-it-container 插件:
npm install markdown-it markdown-it-container shiki @types/markdown-it
2. 注册插件
在 .vitepress/plugins 目录下,创建 index.ts 和 markdown.ts 文件,来注册和配置这些插件。
.vitepress/config/index.ts
import { defineConfig } from 'vitepress'
import { nav, repoMasterUrl, repoUrl, sidebar } from './constants'
import { MarkdownPlugin } from './plugins'
export default defineConfig({
markdown: {
config: md => MarkdownPlugin(md),
theme: {
light: 'github-light',
dark: 'github-dark',
},
},
})
.vitepress/plugins/markdown.ts
import type MarkdownIt from 'markdown-it'
import type Renderer from 'markdown-it/lib/renderer.mjs'
import type Token from 'markdown-it/lib/token.mjs'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { cwd } from 'node:process'
import markdownItContainer from 'markdown-it-container'
import { createHighlighter } from 'shiki'
interface ContainerOpts {
marker?: string
validate?: (params: string) => boolean
render?: (
tokens: Token[],
index: number,
options: any,
env: any,
self: Renderer
) => string
}
export async function MarkdownPlugin(md: MarkdownIt): Promise<void> {
const highlighter = await createHighlighter({
themes: ['github-light', 'github-dark'],
langs: ['vue', 'vue-html', 'typescript', 'javascript'],
})
md.use(markdownItContainer, 'example', {
validate(params) {
// eslint-disable-next-line regexp/no-super-linear-backtracking
return !!params.trim().match(/^example\s*(.*)$/)
},
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
let source = ''
const sourceFileToken = tokens[idx + 2]
const sourceFile = sourceFileToken.children?.[0].content ?? ''
if (sourceFileToken.type === 'inline') {
source = readFileSync(
resolve(cwd(), 'docs/examples', `${sourceFile}.vue`),
'utf-8',
)
}
if (!source) {
throw new Error(`Incorrect source file: ${sourceFile}`)
}
const shikiSource = highlighter.codeToHtml(source, {
lang: 'vue',
themes: {
light: 'github-light',
dark: 'github-dark',
},
})
return `<Example source="${encodeURIComponent(shikiSource)}" path="${sourceFile}" raw-source="${encodeURIComponent(source)}">`
}
else {
return '</Example>'
}
},
} as ContainerOpts)
}
3. 用于展示组件示例的 Example.vue
Example.vue 组件展示了如何渲染 Vue 组件并切换源代码的显示。
<script setup lang="ts">
/// <reference types="vite/client" />
import { useClipboard } from '@vueuse/core'
import { useData } from 'vitepress'
import { computed, ref } from 'vue'
import { GITHUB_URL } from '../config/constants'
import ExampleComponent from './ExampleComponent.vue'
import ExampleOperate from './ExampleOperate.vue'
import ExampleSourceCode from './ExampleSourceCode.vue'
const props = defineProps({
path: {
type: String,
required: true,
},
source: {
type: String,
required: true,
},
rawSource: {
type: String,
required: true,
},
})
const { isDark } = useData()
const components: Record<string, { default: any }> = import.meta.glob('../../examples/**/*.vue', { eager: true })
const pathComponents = computed(() => {
const _obj: Record<string, any> = {}
Object.keys(components).forEach((key) => {
_obj[key.replace('../../examples/', '').replace('.vue', '')]
= components[key].default
})
return _obj
})
const showCode = ref(false)
function toggleHandle() {
showCode.value = !showCode.value
}
const { copy } = useClipboard({
source: decodeURIComponent(props.rawSource),
read: false,
})
function copyHandle() {
copy()
}
const path = computed(() => {
return props.path.replace(/\/\//g, '/')
})
function openHandle() {
window.open(`${GITHUB_URL}/blob/main/docs/examples/${path.value}.vue`)
}
</script>
<template>
<ClientOnly>
<div class="example" :class="{ dark: isDark }">
<div class="example-component-wrapper">
<ExampleComponent :file="path" :comp="pathComponents[path]" />
</div>
<div class="example-operate-wrapper">
<ExampleOperate @toggle="toggleHandle" @copy="copyHandle" @open="openHandle" />
</div>
<div v-show="showCode" class="example-source-code-wrapper">
<ExampleSourceCode :source="source" />
</div>
</div>
</ClientOnly>
</template>
<style scoped>
.example {
border: 1px solid;
border-color: var(--vp-c-divider);
border-radius: 4px;
padding: 0;
}
.example-component-wrapper,
.example-operate-wrapper {
padding: 0.5rem;
}
.example-source-code-wrapper,
.example-operate-wrapper {
border-top: 1px solid;
border-color: var(--vp-c-divider);
}
.example-operate-wrapper {
background-color: var(--vp-c-bg-soft);
}
</style>
5. ExampleOperate.vue 操作按钮组件
ExampleOperate.vue 提供源码显示、复制及打开功能。
<script setup lang="ts">
import { useData } from 'vitepress'
import { computed, ref } from 'vue'
const emit = defineEmits(['toggle', 'copy', 'open'])
const { lang } = useData()
const code = ref(false)
const copy = ref(false)
const codeText = computed(() => {
if (!code.value) {
return lang.value === 'en-US' ? 'Show Code' : '显示代码'
}
else {
return lang.value === 'en-US' ? 'Hide Code' : '隐藏代码'
}
})
const copyCodeText = computed(() => {
if (!copy.value) {
return lang.value === 'en-US' ? 'Copy Code' : '复制代码'
}
else {
return lang.value === 'en-US' ? 'Copied to clipboard' : '已复制到剪贴板'
}
})
const viewCodeText = computed(() => lang.value === 'en-US' ? 'View Source Code' : '查看源码')
function toggleSourceCode() {
code.value = !code.value
emit('toggle')
}
function copySourceCode() {
copy.value = true
setTimeout(() => {
copy.value = false
}, 3000)
emit('copy')
}
function openSourceCode() {
emit('open')
}
</script>
<template>
<div class="example-operate">
<i class="icon" :title="codeText" @click="toggleSourceCode">
<svg v-if="!code" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 32 32">
<path
fill="currentColor"
d="m31 16l-7 7l-1.41-1.41L28.17 16l-5.58-5.59L24 9zM1 16l7-7l1.41 1.41L3.83 16l5.58 5.59L8 23zm11.42 9.484L17.64 6l1.932.517L14.352 26z"
/>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 32 32">
<path
fill="currentColor"
d="m17.713 13.471l1.863-6.953L17.645 6l-1.565 5.838zm6.494 6.494l1.414 1.414L31 16l-7-7l-1.414 1.414L28.172 16zM30 28.586L3.414 2L2 3.414l5.793 5.793L1 16l7 7l1.414-1.414L3.828 16l5.379-5.379l5.677 5.677l-2.461 9.184l1.932.518l2.162-8.069L28.586 30z"
/>
</svg>
</i>
<i class="icon" :title="copyCodeText" @click="copySourceCode">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 32 32">
<path
fill="currentColor"
d="M28 10v18H10V10zm0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2"
/>
<path fill="currentColor" d="M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z" />
</svg>
</i>
<i class="icon" :title="viewCodeText" @click="openSourceCode">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 32 32">
<path
fill="currentColor" fill-rule="evenodd"
d="M16 2a14 14 0 0 0-4.43 27.28c.7.13 1-.3 1-.67v-2.38c-3.89.84-4.71-1.88-4.71-1.88a3.7 3.7 0 0 0-1.62-2.05c-1.27-.86.1-.85.1-.85a2.94 2.94 0 0 1 2.14 1.45a3 3 0 0 0 4.08 1.16a2.93 2.93 0 0 1 .88-1.87c-3.1-.36-6.37-1.56-6.37-6.92a5.4 5.4 0 0 1 1.44-3.76a5 5 0 0 1 .14-3.7s1.17-.38 3.85 1.43a13.3 13.3 0 0 1 7 0c2.67-1.81 3.84-1.43 3.84-1.43a5 5 0 0 1 .14 3.7a5.4 5.4 0 0 1 1.44 3.76c0 5.38-3.27 6.56-6.39 6.91a3.33 3.33 0 0 1 .95 2.59v3.84c0 .46.25.81 1 .67A14 14 0 0 0 16 2"
/>
</svg>
</i>
</div>
</template>
<style scoped>
.example-operate {
display: flex;
align-items: center;
justify-content: flex-end;
}
.example-operate .icon {
cursor: pointer;
color: #aaa;
position: relative;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.example-operate .icon:hover {
opacity: 0.8;
transition: all 0.3s ease-in-out;
}
.example-operate .icon:hover::after {
cursor: auto;
content: attr(title);
position: absolute;
top: -100%;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.5rem;
background-color: #333;
color: #fff;
border-radius: 0.25rem;
font-size: 0.75rem;
white-space: nowrap;
z-index: 999;
font-style: normal;
}
</style>
6. ExampleComponent.vue 渲染组件
ExampleComponent.vue 负责渲染组件并显示交互效果。
<script setup lang="ts">
defineProps({
file: {
type: String,
required: true,
},
comp: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="example-component">
<ClientOnly>
<component :is="comp" v-if="comp" v-bind="$attrs" />
</ClientOnly>
</div>
</template>
<style scoped>
.example-component {
padding: 1.5rem;
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
</style>
7. ExampleSourceCode.vue 组件
ExampleSourceCode.vue 负责展示源码内容。
<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-code language-vue" v-html="decoded" />
</template>
<style scoped>
.example-source-code {
overflow-x: auto;
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
z-index: 99;
}
.language-vue{
position: unset !important;
}
</style>
通过上述配置,我们不仅能够展示 Vue 组件,还能交互式地查看和复制源代码。
7. 实现效果
作者提供了在线演示地址:starter-lib-vue3.netlify.app/components/…
这是一个 Vue 3 组件库启动模板,提供 VitePress 文档,支持构建 ESM、CJS 和 IIFE 格式。
七、总结
VitePress 是一个基于 Vite 的静态网站生成器,专注于提供快速构建和优异的开发体验。它通过支持 Markdown 文件与 Vue 组件的结合,使得开发者能够轻松创建交互性强的文档网站。VitePress 内置了国际化支持,能够方便地管理多语言版本的站点,且主题定制能力强,用户可以根据需求调整站点的外观和功能。通过插件系统,开发者还可以扩展更多自定义功能,如代码高亮、交互示例等。此外,VitePress 生成的站点是纯静态的,部署非常便捷,适用于 GitHub Pages、Netlify 等平台。