插件开发指南
除了使用现有插件,你还可以开发自己的 VitePress 插件来扩展功能。本文将介绍插件开发的完整流程。
插件类型
VitePress 插件主要分为以下几类:
版本说明
本文档基于 VitePress v1.0.0+ 编写。vitepress 钩子需要 v1.0.0 及以上版本。
| 类型 | 说明 | 示例 | 版本要求 |
|---|---|---|---|
| Vite 插件 | 利用 Vite 构建能力扩展功能 | 文件转换、模块解析 | - |
| Markdown 插件 | 扩展 Markdown 语法 | 自定义容器、语法高亮 | - |
| 主题插件 | 扩展主题功能 | 自定义组件、布局插槽 | - |
| 构建钩子 | 在构建过程执行操作 | 数据生成、文件处理 | v1.0.0+ |
开发 Vite 插件
基本结构
ts
// my-vitepress-plugin/index.ts
import type { Plugin, ResolvedConfig } from 'vite'
import type { SiteConfig } from 'vitepress'
interface MyPluginOptions {
enabled: boolean
include?: string[]
exclude?: string[]
}
export function myVitePressPlugin(options: MyPluginOptions = { enabled: true }): Plugin {
let config: ResolvedConfig
let siteConfig: SiteConfig
return {
name: 'vitepress-plugin-my-feature',
// 插件执行顺序
enforce: 'pre', // 'pre' | 'post'
// 初始化时获取配置
configResolved(resolvedConfig) {
config = resolvedConfig
},
// VitePress 特有钩子(v1.0.0+)
vitepress: {
// 在配置解析后执行
configResolved(vpConfig) {
siteConfig = vpConfig
console.log('VitePress 配置:', siteConfig.title)
}
},
// 构建开始
buildStart() {
if (!options.enabled) return
console.log('插件开始构建...')
},
// 转换模块
transform(code, id) {
if (!options.enabled) return null
// 检查是否需要处理
if (options.exclude?.some(pattern => id.includes(pattern))) {
return null
}
// 转换逻辑
if (id.endsWith('.md')) {
// 处理 Markdown 文件
return transformMarkdown(code, options)
}
return null
},
// 构建结束
closeBundle() {
console.log('构建完成')
}
}
}
function transformMarkdown(code: string, options: MyPluginOptions): string {
// 自定义转换逻辑
return code
}使用插件
ts
// docs/.vitepress/config.mts
import { myVitePressPlugin } from 'vitepress-plugin-my-feature'
export default defineConfig({
vite: {
plugins: [
myVitePressPlugin({
enabled: true,
include: ['docs/**/*.md'],
exclude: ['docs/drafts/**']
})
]
}
})Markdown 插件开发
创建自定义容器
ts
// docs/.vitepress/plugins/custom-containers.ts
import type MarkdownIt from 'markdown-it'
import type Renderer from 'markdown-it/lib/renderer'
import type Token from 'markdown-it/lib/token'
interface ContainerOptions {
marker?: string
validate?: (params: string) => boolean
render?: (tokens: Token[], idx: number, options: any, env: any, self: Renderer) => string
}
export function customContainers(md: MarkdownIt) {
// 定义容器类型
const containers = [
{
name: 'theorem',
marker: ':',
title: '定理',
icon: '📐'
},
{
name: 'proof',
marker: ':',
title: '证明',
icon: '📝'
},
{
name: 'example',
marker: ':',
title: '示例',
icon: '💡'
}
]
containers.forEach(({ name, marker, title, icon }) => {
md.use(require('markdown-it-container'), name, {
validate(params: string) {
return params.trim().match(new RegExp(`^${name}\\s+(.*)$`))
},
render(tokens: Token[], idx: number) {
const m = tokens[idx].info.trim().match(new RegExp(`^${name}\\s+(.*)$`))
if (tokens[idx].nesting === 1) {
// 开始标签
const customTitle = m?.[1] || title
return `<div class="custom-container ${name}">
<p class="custom-container-title">
<span class="custom-container-icon">${icon}</span>
${md.utils.escapeHtml(customTitle)}
</p>
<div class="custom-container-content">
`
} else {
// 结束标签
return '</div></div>'
}
}
})
})
}使用:
ts
// docs/.vitepress/config.mts
import { customContainers } from './plugins/custom-containers'
export default defineConfig({
markdown: {
config(md) {
customContainers(md)
}
}
})代码块增强
ts
// docs/.vitepress/plugins/code-enhance.ts
import type MarkdownIt from 'markdown-it'
export function codeEnhance(md: MarkdownIt) {
// 保存原始渲染器
const defaultRender = md.renderer.rules.fence!
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const info = token.info ? token.info.trim() : ''
// 添加文件名支持
const filenameMatch = info.match(/(\w+):([^\s]+)/)
if (filenameMatch) {
const lang = filenameMatch[1]
const filename = filenameMatch[2]
token.info = lang
// 在代码块前添加文件名
const filenameHtml = `<div class="code-filename">
<span class="code-filename-icon">📄</span>
<span class="code-filename-text">${md.utils.escapeHtml(filename)}</span>
</div>`
return filenameHtml + defaultRender(tokens, idx, options, env, self)
}
// 添加行号高亮
const highlightMatch = info.match(/\{([0-9,-]+)\}/)
if (highlightMatch) {
const lines = parseLineRanges(highlightMatch[1])
// 添加高亮标记...
}
return defaultRender(tokens, idx, options, env, self)
}
}
function parseLineRanges(range: string): number[] {
const lines: number[] = []
range.split(',').forEach(part => {
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number)
for (let i = start; i <= end; i++) lines.push(i)
} else {
lines.push(Number(part))
}
})
return lines
}构建钩子插件
createContentLoader 扩展
ts
// docs/.vitepress/plugins/data-loader.ts
import { createContentLoader } from 'vitepress'
import type { SiteConfig } from 'vitepress'
export function customDataLoader() {
return {
name: 'vitepress-plugin-data-loader',
// v1.0.0+ 支持 vitepress 钩子
vitepress: {
configResolved(config: SiteConfig) {
// 扩展数据加载器
config.defineLoader('custom-posts', async () => {
const posts = await createContentLoader('posts/*.md', {
includeSrc: true,
transform(raw) {
return raw
.filter(page => !page.frontmatter.draft)
.sort((a, b) => {
const dateA = new Date(a.frontmatter.date)
const dateB = new Date(b.frontmatter.date)
return dateB.getTime() - dateA.getTime()
})
.map(page => ({
title: page.frontmatter.title,
excerpt: page.excerpt,
date: page.frontmatter.date,
tags: page.frontmatter.tags || [],
readingTime: calculateReadingTime(page.src!),
wordCount: page.src?.length || 0
}))
}
}).load()
return posts
})
}
}
}
}
function calculateReadingTime(content: string): number {
const wordsPerMinute = 200
const words = content.split(/\s+/).length
return Math.ceil(words / wordsPerMinute)
}构建后处理
ts
// docs/.vitepress/plugins/post-build.ts
import type { Plugin } from 'vite'
import fs from 'fs/promises'
import path from 'path'
interface PostBuildOptions {
generateSearchIndex?: boolean
optimizeImages?: boolean
generateSitemap?: boolean
}
export function postBuild(options: PostBuildOptions = {}): Plugin {
return {
name: 'vitepress-plugin-post-build',
apply: 'build',
async closeBundle() {
const outDir = path.resolve('docs/.vitepress/dist')
// 生成搜索索引
if (options.generateSearchIndex) {
await generateSearchIndex(outDir)
}
// 优化图片
if (options.optimizeImages) {
await optimizeImages(outDir)
}
// 生成站点地图
if (options.generateSitemap) {
await generateSitemapFile(outDir)
}
}
}
}
async function generateSearchIndex(outDir: string) {
// 生成自定义搜索索引
const indexPath = path.join(outDir, 'search-index.json')
const index = { pages: [] }
await fs.writeFile(indexPath, JSON.stringify(index, null, 2))
console.log('搜索索引已生成')
}
async function optimizeImages(outDir: string) {
// 使用 sharp 等工具优化图片
console.log('图片优化完成')
}
async function generateSitemapFile(outDir: string) {
// 生成站点地图
console.log('站点地图已生成')
}主题插件开发
自定义组件注册
ts
// docs/.vitepress/plugins/theme-components.ts
import type { Plugin } from 'vite'
import fs from 'fs'
import path from 'path'
export function themeComponents(): Plugin {
return {
name: 'vitepress-plugin-theme-components',
config() {
return {
resolve: {
alias: {
'@theme-components': path.resolve(__dirname, '../theme/components')
}
}
}
},
transform(code, id) {
// 自动注册 components 目录下的组件
if (id.includes('theme/index.ts')) {
const componentsDir = path.resolve(__dirname, '../theme/components')
const components = fs.readdirSync(componentsDir)
.filter(file => file.endsWith('.vue'))
.map(file => path.basename(file, '.vue'))
const imports = components
.map(name => `import ${name} from '@theme-components/${name}.vue'`)
.join('\n')
const registrations = components
.map(name => `app.component('${name}', ${name})`)
.join('\n')
return code.replace(
'// COMPONENTS: AUTO IMPORT',
`${imports}\n\n${registrations}`
)
}
return null
}
}
}插件发布
包结构
vitepress-plugin-my-feature/
├── src/
│ ├── index.ts # 入口文件
│ ├── plugin.ts # 插件核心逻辑
│ ├── types.ts # 类型定义
│ └── utils.ts # 工具函数
├── dist/ # 构建产物
├── package.json
├── tsconfig.json
├── README.md
└── LICENSEpackage.json 配置
json
{
"name": "vitepress-plugin-my-feature",
"version": "1.0.0",
"description": "A VitePress plugin for custom features",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./style.css": "./dist/style.css"
},
"files": [
"dist"
],
"keywords": [
"vitepress",
"plugin",
"vite"
],
"peerDependencies": {
"vitepress": ">=1.0.0"
},
"devDependencies": {
"vitepress": "^1.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --watch"
}
}类型定义
ts
// src/types.ts
import type { Plugin, ResolvedConfig } from 'vite'
import type { SiteConfig } from 'vitepress'
export interface MyPluginOptions {
/**
* 是否启用插件
* @default true
*/
enabled?: boolean
/**
* 包含的文件模式
*/
include?: string[]
/**
* 排除的文件模式
*/
exclude?: string[]
}
export interface MyPlugin extends Plugin {
vitepress?: {
configResolved?: (config: SiteConfig) => void
}
}
export type MyPluginFactory = (options?: MyPluginOptions) => MyPlugin文档模板
markdown
# vitepress-plugin-my-feature
A VitePress plugin for custom features.
## Installation
\`\`\`bash
npm add -D vitepress-plugin-my-feature
\`\`\`
## Usage
\`\`\`ts
// docs/.vitepress/config.mts
import { myPlugin } from 'vitepress-plugin-my-feature'
export default defineConfig({
vite: {
plugins: [myPlugin()]
}
})
\`\`\`
## Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| enabled | boolean | true | Enable the plugin |
| include | string[] | - | Include patterns |
| exclude | string[] | - | Exclude patterns |
## License
MIT最佳实践
1. 命名规范
- 包名使用
vitepress-plugin-前缀 - 函数名使用驼峰式命名
- 配置选项使用小写驼峰
2. 类型安全
ts
// 提供完整的类型定义
export interface PluginOptions {
// ...
}
// 导出类型供用户使用
export type { PluginOptions }3. 错误处理
ts
export function myPlugin(options: PluginOptions = {}): Plugin {
return {
name: 'vitepress-plugin-my-feature',
buildStart() {
// 验证选项
if (options.something && typeof options.something !== 'string') {
throw new Error('[vitepress-plugin-my-feature] something must be a string')
}
}
}
}4. 调试支持
ts
export function myPlugin(options: PluginOptions = {}): Plugin {
const debug = options.debug || process.env.DEBUG === 'true'
return {
name: 'vitepress-plugin-my-feature',
transform(code, id) {
if (debug) {
console.log(`[my-plugin] Processing: ${id}`)
}
// ...
}
}
}5. 性能优化
ts
export function myPlugin(options: PluginOptions = {}): Plugin {
// 缓存处理结果
const cache = new Map<string, string>()
return {
name: 'vitepress-plugin-my-feature',
transform(code, id) {
// 检查缓存
if (cache.has(id)) {
return cache.get(id)!
}
// 处理并缓存
const result = processCode(code)
cache.set(id, result)
return result
}
}
}示例:完整的计数器插件
ts
// src/index.ts
import type { Plugin } from 'vite'
import type { SiteConfig } from 'vitepress'
export interface CounterOptions {
/** 是否显示字数统计 */
words?: boolean
/** 是否显示阅读时间 */
readingTime?: boolean
/** 每分钟阅读字数 */
wordsPerMinute?: number
}
declare module 'vitepress' {
interface PageData {
wordCount?: number
readingTime?: number
}
}
export function counter(options: CounterOptions = {}): Plugin {
const {
words = true,
readingTime = true,
wordsPerMinute = 200
} = options
return {
name: 'vitepress-plugin-counter',
enforce: 'pre',
// v1.0.0+ 支持
vitepress: {
configResolved(config: SiteConfig) {
// 在页面数据中添加统计信息
config.transformPageData = (pageData) => {
const content = pageData.content || ''
if (words) {
pageData.wordCount = content.length
}
if (readingTime) {
const words = content.length
pageData.readingTime = Math.ceil(words / wordsPerMinute)
}
}
}
}
}
}版本兼容性说明
版本要求
- 本文档基于 VitePress v1.0.0+ 编写
vitepress钩子需要 v1.0.0 及以上版本- Markdown 插件兼容所有版本
- Vite 插件兼容所有版本
| 特性 | 最低版本 | 说明 |
|---|---|---|
| Vite 插件 | v0.20.0 | 标准 Vite 插件接口 |
| Markdown 插件 | v0.20.0 | markdown-it 扩展 |
vitepress 钩子 | v1.0.0 | VitePress 特有钩子 |
transformPageData | v1.0.0 | 页面数据转换 |
defineLoader | v1.0.0 | 自定义数据加载器 |