年终提效 & 项目包装必备,VitePress 保姆级指南,内含国际化、组件和代码自动展示

1,361 阅读11分钟

今天是2024年的最后一天,在这里提前祝大家2025新年快乐!

年终将至,很多小伙伴可能正在忙着准备公司年终总结和汇报。大家年初定下的KPI完成得怎么样了?我已经顺利完成了所有任务,并完成了汇报。在我的年终汇报中,作为一名程序员,我没有单纯地列举写了多少代码,做了哪些库,而是将所有工作通过 VitePress 进行了整合和包装。在汇报时,我直接拿出精心准备的文档,轻松展示了一番,效果还不错哦 😂

言归正传,接下来,我将全面介绍VitePress,并与大家分享如何用它展示组件库文档中的组件示例功能。

一、什么是 VitePress

VitePress 是由 Vue.js 官方团队开发的一款基于 Vite 的静态站点生成器。它通过 Vue 3 提供了现代化的开发体验,并且专注于文档的构建,支持 Markdown 格式的内容编辑。

  • 性能优异:基于 Vite,启动和构建速度非常快。
  • 支持 Vue 3:可以轻松将 Vue 组件嵌入文档中,适合开发者编写互动式文档。
  • 简单易用:配置简洁,易于上手,适合快速构建小型站点。

screencapture-vitepress-dev-zh-2024-12-30-14_51_32.png

二、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' }
      ]
    }
  }
})

在上述配置中,你可以:

  • 设置站点的 titledescription,分别用于定义文档站点的标题和描述。
  • 配置 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 会自动高亮显示代码块,并使其更具可读性。

image.png

更多高亮效果,参考 vitepress.dev/zh/guide/ma…

4. 全局注册 Vue 组件

如果你希望在所有文档中都使用某个组件,可以将其注册为全局组件。在 VitePress 中,你可以通过扩展默认主题来实现这一点。具体的注册方式可以参考 VitePress 官方文档

六、使用插件和自动化展示 Vue 组件

在前端组件库的文档中,我们经常见到组件的示例和代码。

image.png

为了在 VitePress 中展示可交互的 Vue 组件及其源代码,我们将使用两个插件:markdown-itmarkdown-it-container。这两个插件将帮助我们在 Markdown 中嵌入自定义组件,并且支持高亮显示源代码。

作者实现的效果如下:

screencapture-starter-lib-vue3-netlify-app-zh-components-say-hello-2024-12-30-14_54_45.png

1. 安装插件

首先,安装 markdown-itmarkdown-it-container 插件:

npm install markdown-it markdown-it-container shiki @types/markdown-it

2. 注册插件

.vitepress/plugins 目录下,创建 index.tsmarkdown.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 格式。

仓库地址:github.com/starter-col…

在线演示:starter-lib-vue3.netlify.app/

screencapture-starter-lib-vue3-netlify-app-zh-components-say-hello-2024-12-30-14_54_45.png

七、总结

VitePress 是一个基于 Vite 的静态网站生成器,专注于提供快速构建和优异的开发体验。它通过支持 Markdown 文件与 Vue 组件的结合,使得开发者能够轻松创建交互性强的文档网站。VitePress 内置了国际化支持,能够方便地管理多语言版本的站点,且主题定制能力强,用户可以根据需求调整站点的外观和功能。通过插件系统,开发者还可以扩展更多自定义功能,如代码高亮、交互示例等。此外,VitePress 生成的站点是纯静态的,部署非常便捷,适用于 GitHub Pages、Netlify 等平台。

参考