自定义
自定义主题
可以通过创建 .vitepress/theme/index.js 或 .vitepress/theme/index.ts 文件 (即“主题入口文件”) 来启用自定义主题:
.
├─ docs # 项目根目录
│ ├─ .vitepress
│ │ ├─ theme
│ │ │ └─ index.js # 主题入口
│ │ └─ config.js # 配置文件
│ └─ index.md
└─ package.json当检测到存在主题入口文件时,VitePress 总会使用自定义主题而不是默认主题
主题接口
VitePress 自定义主题是一个对象,该对象具有如下接口:
interface Theme {
/**
* 每个页面的根布局组件
* @required
*/
Layout: Component
/**
* 增强 Vue 应用实例
* @optional
*/
enhanceApp?: (ctx: EnhanceAppContext) => Awaitable<void>
/**
* 扩展另一个主题,在我们的主题之前调用它的 `enhanceApp`
* @optional
*/
extends?: Theme
}
interface EnhanceAppContext {
app: App // Vue 应用实例
router: Router // VitePress 路由实例
siteData: Ref<SiteData> // 站点级元数据
}主题入口文件需要将主题作为默认导出来导出:
// .vitepress/theme/index.js
// 可以直接在主题入口导入 Vue 文件。VitePress 已预先配置 @vitejs/plugin-vue
import Layout from './Layout.vue'
export default {
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
}默认导出是自定义主题的唯一方式,并且只有 Layout 属性是必须的。所以从技术上讲,一个 VitePress 主题可以是一个单独的 Vue 组件。在组件内部,它的工作方式就像是一个普通的 Vite + Vue 3 应用。注意:主题还需要保证 SSR 兼容
构建布局
最基本的布局组件需要包含一个 content 组件:
<!-- .vitepress/theme/Layout.vue -->
<template>
<h1>Custom Layout!</h1>
<!-- 此处将渲染 markdown 内容 -->
<Content />
</template>上面的布局只是将每个页面的 markdown 渲染为 HTML。添加的第一个改进是处理 404 错误:
<script setup>
import { useData } from 'vitepress'
const { page } = useData()
</script>
<template>
<h1>Custom Layout!</h1>
<div v-if="page.isNotFound">
Custom 404 page!
</div>
<Content v-else />
</template>useData() 提供所有的运行时数据,以便根据不同条件渲染不同的布局。通过利用这个数据,可以让用户单独控制每个页面的布局。例如,用户可以指定一个页面是否使用特殊的主页布局:
---
layout: home
---并且可以调整主题进行处理:
<script setup>
import { useData } from 'vitepress'
const { page, frontmatter } = useData()
</script>
<template>
<h1>Custom Layout!</h1>
<div v-if="page.isNotFound">
Custom 404 page!
</div>
<div v-if="frontmatter.layout === 'home'">
Custom home page!
</div>
<Content v-else />
</template>使用自定义主题
要使用外部主题,请导入它并重新导出:
// .vitepress/theme/index.js
import Theme from 'awesome-vitepress-theme'
export default Theme如果主题需要扩展:
// .vitepress/theme/index.js
import Theme from 'awesome-vitepress-theme'
export default {
extends: Theme,
enhanceApp(ctx) {
// ...
}
}如果主题需要特殊的 VitePress 配置,也需要在配置中扩展:
// .vitepress/config.ts
import baseConfig from 'awesome-vitepress-theme/config'
export default {
// 扩展主题的基本配置(如需要)
extends: baseConfig
}最后,如果主题为其主题配置提供了类型:
// .vitepress/config.ts
import baseConfig from 'awesome-vitepress-theme/config'
import { defineConfigWithTheme } from 'vitepress'
import type { ThemeConfig } from 'awesome-vitepress-theme'
export default defineConfigWithTheme<ThemeConfig>({
extends: baseConfig,
themeConfig: {
// 类型为 `ThemeConfig`
}
})扩展默认主题
VitePress 默认的主题已经针对文档进行了优化,并且可以进行自定义
但是有一些情况仅靠配置是不够的。例如:
- 需要调整 CSS 样式
- 需要修改 Vue 应用实例,例如注册全局组件
- 需要通过 layout 插槽将自定义内容注入到主题中
这些高级自定义配置将需要使用自定义主题来“拓展”默认主题
自定义 CSS
可以通过覆盖根级别的 CSS 变量来自定义默认主题的 CSS:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './custom.css'
export default DefaultThemecustom.css 文件
/* .vitepress/theme/custom.css */
:root {
--vp-c-brand-1: #646cff;
--vp-c-brand-2: #747bff;
}查看默认主题 CSS 变量来获取可以被覆盖的变量
布局插槽
默认主题的 <Layout/> 组件有一些插槽,能够被用来在页面的特定位置注入内容。下面这个例子展示了将一个组件注入到 outline 之前:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import MyLayout from './MyLayout.vue'
export default {
extends: DefaultTheme,
// 使用注入插槽的包装组件覆盖 Layout
Layout: MyLayout
}<!--.vitepress/theme/MyLayout.vue-->
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #aside-outline-before>
My custom sidebar top content
</template>
</Layout>
</template>也可以使用渲染函数。
// .vitepress/theme/index.js
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import MyComponent from './MyComponent.vue'
export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'aside-outline-before': () => h(MyComponent)
})
}
}默认主题布局的全部可用插槽如下:
当 layout: 'doc' (默认) 在 frontmatter 中被启用时:
doc-topdoc-bottomdoc-footer-beforedoc-beforedoc-aftersidebar-nav-beforesidebar-nav-afteraside-topaside-bottomaside-outline-beforeaside-outline-afteraside-ads-beforeaside-ads-after
当 layout: 'home' 在 frontmatter 中被启用时:
-
home-hero-before -
home-hero-info-before -
home-hero-info -
home-hero-info-after -
home-hero-actions-after -
home-hero-image -
home-hero-after -
home-features-before -
home-features-after -
当
layout: 'page'在 frontmatter 中被启用时:page-toppage-bottom
-
当未找到页面 (404) 时:
not-found
-
总是启用:
layout-toplayout-bottomnav-bar-title-beforenav-bar-title-afternav-bar-content-beforenav-bar-content-afternav-screen-content-beforenav-screen-content-after
重写内部组件
可以使用 Vite 的 aliases 来用自定义组件替换默认主题的组件:
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitepress'
export default defineConfig({
vite: {
resolve: {
alias: [
{
find: /^.*\/VPNavBar\.vue$/,
replacement: fileURLToPath(
new URL('./components/CustomNavBar.vue', import.meta.url)
)
}
]
}
}
})构建时数据加载
VitePress 提供数据加载的功能,它允许加载任意数据并从页面或组件中导入它。数据加载只在构建时执行:最终的数据将被序列化为 JavaScript 包中的 JSON。数据加载可以被用于获取远程数据,也可以基于本地文件生成元数据
用于数据加载的文件必须以 .data.js 或 .data.ts 结尾。该文件应该提供一个默认导出的对象,该对象具有 load() 方法:
export default {
load() {
return {
hello: 'world'
}
}
}数据加载模块只在 Node.js 中执行,因此可以按需导入 Node API 和 npm 依赖。在 .md 页面和 .vue 组件中使用 data 具名导出从该文件中导入数据:
<script setup>
import { data } from './example.data.js'
</script>
<pre>{{ data }}</pre>VitePress 在后台调用 load() 方法,并通过名为 data 的具名导出隐式地暴露了结果。获取数据的方式也可以是异步的:
export default {
async load() {
// 获取远程数据
return (await fetch('...')).json()
}
}本地文件生成数据
当需要基于本地文件生成数据时,需要在 data loader 中使用 watch 选项,以便这些文件改动时可以触发热更新。下面的例子展示使用 csv-parse 加载 CSV 文件并将其转换为 JSON。因为此文件仅在构建时执行,因此不会将 CSV 解析器发送到客户端
import fs from 'node:fs'
import { parse } from 'csv-parse/sync'
export default {
watch: ['./data/*.csv'],
load(watchedFiles) {
// watchFiles 是一个所匹配文件的绝对路径的数组。
// 生成一个博客文章元数据数组
// 可用于在主题布局中呈现列表。
return watchedFiles.map((file) => {
return parse(fs.readFileSync(file, 'utf-8'), {
columns: true,
skip_empty_lines: true
})
})
}
}createContentLoader
当构建一个内容为主的站点时,经常需要创建一个“归档”或“索引”页面:可以列出内容中的所有可用条目的页面,例如博客文章或 API 页面。可以直接使用数据加载 API 实现这一点,但由于这会经常使用,VitePress 还提供一个 createContentLoader 辅助函数来简化这个过程:
import { createContentLoader } from 'vitepress'
export default createContentLoader('posts/*.md', /* options */)该辅助函数接受一个相对于源目录的 glob 模式,并返回一个 { watch, load } 数据加载对象,该对象可以用作数据加载文件中的默认导出。它还基于文件修改时间戳实现了缓存以提高开发性能。
请注意,数据加载仅适用于 Markdown 文件——匹配的非 Markdown 文件将被跳过。加载的数据将是一个类型为 ContentData[] 的数组:
interface ContentData {
// 页面的映射 URL,如 /posts/hello.html(不包括 base)
// 手动迭代或使用自定义 `transform` 来标准化路径
url: string
// 页面的 frontmatter 数据
frontmatter: Record<string, any>
// 只有启用了相关选项,才会出现以下内容
// 我们将在下面讨论它们
src: string | undefined
html: string | undefined
excerpt: string | undefined
}默认情况下只提供 url 和 frontmatter。这是因为加载的数据将作为 JSON 内联在客户端 bundle 中,我们需要谨慎考虑其大小。下面的例子展示使用数据构建最小的博客索引页面:
<script setup>
import { data as posts } from './posts.data.js'
</script>
<template>
<h1>All Blog Posts</h1>
<ul>
<li v-for="post of posts">
<a :href="post.url">{{ post.frontmatter.title }}</a>
<span>by {{ post.frontmatter.author }}</span>
</li>
</ul>
</template>选项
默认数据可能不适合所有需求——可以选择使用选项转换数据:
import { createContentLoader } from 'vitepress'
export default createContentLoader('posts/*.md', {
includeSrc: true, // 包含原始 markdown 源?
render: true, // 包含渲染的整页 HTML?
excerpt: true, // 包含摘录?
transform(rawData) {
// 根据需要对原始数据进行 map、sort 或 filter
// 最终的结果是将发送给客户端的内容
return rawData.sort((a, b) => {
return +new Date(b.frontmatter.date) - +new Date(a.frontmatter.date)
}).map((page) => {
page.src // 原始 markdown 源
page.html // 渲染的整页 HTML
page.excerpt // 渲染的摘录 HTML(第一个 `---` 上面的内容)
return {/* ... */}
})
}
})使用示例:
import { createContentLoader } from 'vitepress'
interface Post {
title: string
url: string
date: {
time: number
string: string
}
excerpt: string | undefined
}
declare const data: Post[]
export { data }
export default createContentLoader('posts/*.md', {
excerpt: true,
transform(raw): Post[] {
return raw
.map(({ url, frontmatter, excerpt }) => ({
title: frontmatter.title,
url,
excerpt,
date: formatDate(frontmatter.date)
}))
.sort((a, b) => b.date.time - a.date.time)
}
})
function formatDate(raw: string): Post['date'] {
const date = new Date(raw)
date.setUTCHours(12)
return {
time: +date,
string: date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
}为 data loader 导出类型
当使用 TypeScript 时,可以像这样为 loader 和 data 导出类型:
import { defineLoader } from 'vitepress'
export interface Data {
// data 类型
}
declare const data: Data
export { data }
export default defineLoader({
// 类型检查加载器选项
watch: ['...'],
async load(): Promise<Data> {
// ...
}
})配置
要获取 data loader 中的配置信息,可以使用如下代码:
import type { SiteConfig } from 'vitepress'
const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG