Skip to content

插件开发指南

除了使用现有插件,你还可以开发自己的 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
└── LICENSE

package.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.0markdown-it 扩展
vitepress 钩子v1.0.0VitePress 特有钩子
transformPageDatav1.0.0页面数据转换
defineLoaderv1.0.0自定义数据加载器

下一步

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献