Skip to content

VitePress 博客集成到 Vue + Vite 项目的完整解决方案

摘要:本文档提供了一种将 VitePress 静态博客无缝集成到主 Vue + Vite 项目中的专业方案。该方案支持开发环境下通过 /blog/ 路径访问博客,同时保持主应用的单页应用(SPA)特性。

📋 概述

本文档提供了一种将 VitePress 静态博客无缝集成到主 Vue + Vite 项目中的专业方案。该方案支持开发环境下通过 /blog/ 路径访问博客,同时保持主应用的单页应用(SPA)特性。

🎯 核心目标

  • ✅ 开发环境:通过 http://localhost:5173/blog/ 访问 VitePress 博客
  • ✅ 保留主 Vue 应用的 SPA 路由功能
  • ✅ 支持博客内容的热更新(重新构建后)
  • ✅ 无路由冲突、无资源加载问题
  • ✅ 生产环境友好,支持静态部署

📁 项目结构

Desktop/long/
├── my-blog-frontend/          # 主 Vue + Vite 项目
│   ├── public/
│   │   ├── blog/             # 博客构建产物目录(自动生成)
│   │   │   ├── index.html
│   │   │   └── assets/
│   │   │       └── ...
│   │   └── vite.svg
│   ├── src/
│   │   └── ...
│   ├── package.json
│   └── vite.config.ts        # 主项目配置

└── my-vitepress-blog/        # 独立 VitePress 博客项目
    ├── docs/
    │   ├── .vitepress/
    │   │   └── config.ts     # VitePress 配置
    │   ├── index.md
    │   └── ...
    ├── deploy.mjs           # 部署脚本
    ├── package.json
    └── README.md

🚀 实施步骤

1. VitePress 基础配置

文件: my-vitepress-blog/docs/.vitepress/config.ts

typescript
import { defineConfig } from 'vitepress'

export default defineConfig({
  // 🎯 关键配置:基础路径必须与部署路径一致
  base: '/blog/',
  
  // 🏷️ 基础元数据
  title: '技术博客',
  description: '个人技术博客与知识库',
  lang: 'zh-CN',
  
  // 🎨 主题配置
  themeConfig: {
    nav: [
      { text: '首页', link: '/' },
      { text: '归档', link: '/archives' }
    ],
    sidebar: [
      {
        text: '指南',
        items: [
          { text: '快速开始', link: '/guide/getting-started' }
        ]
      }
    ],
    socialLinks: [
      { icon: 'github', link: 'https://github.com/yourusername' }
    ],
    footer: {
      message: '基于 VitePress 构建',
      copyright: `Copyright © ${new Date().getFullYear()} 你的名字`
    }
  },
  
  // ⚡ 性能优化
  markdown: {
    lineNumbers: true, // 显示代码行号
  },
  
  // 🔧 构建配置
  build: {
    outDir: '../.vitepress/dist',
    assetsDir: 'assets',
    emptyOutDir: true
  }
})

2. 增强型部署脚本

文件: my-vitepress-blog/deploy.mjs

javascript
#!/usr/bin/env node

/**
 * VitePress 部署脚本
 * 将构建产物复制到主项目的 public/blog 目录
 * 
 * 使用方式:node deploy.mjs [--watch]
 * 参数:
 *   --watch   监听文件变化并自动重新构建
 */

import { execSync, spawn } from 'child_process'
import { cpSync, rmSync, existsSync, mkdirSync } from 'fs'
import { resolve, dirname, relative } from 'path'
import { fileURLToPath } from 'url'
import { watch } from 'chokidar'
import chalk from 'chalk'

// ✅ 获取当前脚本目录(ESM 兼容)
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

// 🔧 配置区域 - 根据实际情况修改
const CONFIG = {
  // 主项目绝对路径
  mainProjectPath: resolve(__dirname, '../my-blog-frontend'),
  
  // VitePress 项目配置
  vitepressConfig: {
    sourceDir: resolve(__dirname, 'docs'),
    distDir: resolve(__dirname, 'docs/.vitepress/dist'),
    configFile: resolve(__dirname, 'docs/.vitepress/config.ts')
  },
  
  // 目标目录
  targetDir: resolve(__dirname, '../my-blog-frontend/public/blog'),
  
  // 是否启用详细日志
  verbose: true
}

// 📊 日志工具
const logger = {
  info: (msg) => console.log(chalk.blue('ℹ'), msg),
  success: (msg) => console.log(chalk.green('✓'), msg),
  warning: (msg) => console.log(chalk.yellow('⚠'), msg),
  error: (msg) => console.log(chalk.red('✗'), msg),
  verbose: (msg) => CONFIG.verbose && console.log(chalk.gray('·'), msg)
}

// 🔍 路径检查
function validatePaths() {
  logger.verbose('正在验证路径...')
  
  const errors = []
  
  // 检查 VitePress 源目录
  if (!existsSync(CONFIG.vitepressConfig.sourceDir)) {
    errors.push(`VitePress 源目录不存在: ${CONFIG.vitepressConfig.sourceDir}`)
  }
  
  // 检查 VitePress 配置文件
  if (!existsSync(CONFIG.vitepressConfig.configFile)) {
    errors.push(`VitePress 配置文件不存在: ${CONFIG.vitepressConfig.configFile}`)
  }
  
  // 检查主项目目录
  if (!existsSync(CONFIG.mainProjectPath)) {
    errors.push(`主项目目录不存在: ${CONFIG.mainProjectPath}`)
  }
  
  // 检查主项目 public 目录
  const publicDir = resolve(CONFIG.mainProjectPath, 'public')
  if (!existsSync(publicDir)) {
    logger.warning(`主项目 public 目录不存在,将自动创建`)
    try {
      mkdirSync(publicDir, { recursive: true })
    } catch (err) {
      errors.push(`无法创建 public 目录: ${err.message}`)
    }
  }
  
  if (errors.length > 0) {
    errors.forEach(error => logger.error(error))
    throw new Error('路径验证失败')
  }
  
  logger.success('路径验证通过')
  return true
}

// 🛠️ 构建 VitePress
function buildVitePress() {
  logger.info('开始构建 VitePress 博客...')
  
  try {
    // 检查 package.json 中的构建脚本
    const packageJsonPath = resolve(__dirname, 'package.json')
    if (existsSync(packageJsonPath)) {
      const packageJson = JSON.parse(execSync('cat package.json', { encoding: 'utf-8' }))
      if (!packageJson.scripts || !packageJson.scripts.build) {
        throw new Error('package.json 中未找到构建脚本')
      }
    }
    
    // 执行构建命令
    logger.verbose('执行构建命令: npm run build')
    execSync('npm run build', { 
      stdio: 'inherit',
      cwd: __dirname 
    })
    
    // 验证构建结果
    if (!existsSync(CONFIG.vitepressConfig.distDir)) {
      throw new Error(`构建失败:未找到输出目录 ${CONFIG.vitepressConfig.distDir}`)
    }
    
    const indexPath = resolve(CONFIG.vitepressConfig.distDir, 'index.html')
    if (!existsSync(indexPath)) {
      throw new Error(`构建失败:未生成 index.html`)
    }
    
    logger.success(`构建完成,输出目录: ${CONFIG.vitepressConfig.distDir}`)
    return true
  } catch (error) {
    logger.error(`构建失败: ${error.message}`)
    throw error
  }
}

// 📦 复制构建产物
function copyBuildArtifacts() {
  logger.info('复制构建产物到主项目...')
  
  try {
    // 清理目标目录
    if (existsSync(CONFIG.targetDir)) {
      logger.verbose(`清理目录: ${CONFIG.targetDir}`)
      rmSync(CONFIG.targetDir, { recursive: true, force: true })
    }
    
    // 创建目标目录
    mkdirSync(CONFIG.targetDir, { recursive: true })
    
    // 复制文件
    logger.verbose(`从 ${CONFIG.vitepressConfig.distDir} 复制到 ${CONFIG.targetDir}`)
    cpSync(CONFIG.vitepressConfig.distDir, CONFIG.targetDir, { 
      recursive: true,
      dereference: true
    })
    
    // 验证复制结果
    const files = execSync(`find "${CONFIG.targetDir}" -type f | wc -l`, { encoding: 'utf-8' }).trim()
    logger.success(`复制完成,共 ${files} 个文件`)
    
    // 输出访问信息
    console.log('\n' + chalk.cyan('='.repeat(50)))
    console.log(chalk.bold.green('🎉 博客部署成功!'))
    console.log(chalk.cyan('='.repeat(50)))
    console.log(`📁 博客文件位置: ${relative(process.cwd(), CONFIG.targetDir)}`)
    console.log(`🌐 本地访问地址: ${chalk.underline('http://localhost:5173/blog/')}`)
    console.log(`💡 提示: 请重启主项目的开发服务器`)
    console.log(chalk.cyan('='.repeat(50)) + '\n')
    
    return true
  } catch (error) {
    logger.error(`复制文件失败: ${error.message}`)
    throw error
  }
}

// 👁️ 监听模式
function startWatchMode() {
  logger.info('启动监听模式...')
  
  const watcher = watch([
    resolve(__dirname, 'docs/**/*.md'),
    resolve(__dirname, 'docs/**/*.vue'),
    resolve(__dirname, 'docs/.vitepress/**/*')
  ], {
    ignored: /(^|[\/\\])\../, // 忽略隐藏文件
    persistent: true,
    ignoreInitial: true
  })
  
  let isBuilding = false
  let rebuildTimeout = null
  
  watcher.on('all', (event, path) => {
    const relativePath = relative(__dirname, path)
    logger.verbose(`检测到文件变化: ${relativePath} (${event})`)
    
    // 防抖处理,避免频繁构建
    if (rebuildTimeout) {
      clearTimeout(rebuildTimeout)
    }
    
    if (!isBuilding) {
      rebuildTimeout = setTimeout(async () => {
        try {
          isBuilding = true
          console.log('\n' + chalk.yellow('🔄 检测到文件变化,开始重新构建...'))
          await buildAndDeploy()
          console.log(chalk.green('✅ 重新构建完成\n'))
        } catch (error) {
          logger.error(`重新构建失败: ${error.message}`)
        } finally {
          isBuilding = false
        }
      }, 1000) // 1秒防抖
    }
  })
  
  console.log('\n' + chalk.bgBlue.black(' 📝 VitePress 监听模式已启动 '))
  console.log(chalk.blue('正在监听文件变化...'))
  console.log(chalk.gray('按 Ctrl+C 退出监听模式\n'))
}

// 🚀 主执行函数
async function buildAndDeploy() {
  try {
    validatePaths()
    buildVitePress()
    copyBuildArtifacts()
    return true
  } catch (error) {
    logger.error(`部署过程出错: ${error.message}`)
    process.exit(1)
  }
}

// 📝 脚本入口
async function main() {
  console.log(chalk.cyan.bold('\n🚀 VitePress 博客部署工具'))
  console.log(chalk.gray('版本 2.0.0 | 增强型部署脚本\n'))
  
  // 解析命令行参数
  const args = process.argv.slice(2)
  const isWatchMode = args.includes('--watch') || args.includes('-w')
  
  if (args.includes('--help') || args.includes('-h')) {
    console.log(chalk.bold('使用说明:'))
    console.log('  node deploy.mjs           # 执行一次构建部署')
    console.log('  node deploy.mjs --watch   # 监听模式,文件变化时自动构建')
    console.log('  node deploy.mjs --help    # 显示帮助信息\n')
    return
  }
  
  if (args.includes('--version') || args.includes('-v')) {
    console.log('版本: 2.0.0')
    console.log('作者: 你的团队')
    console.log('描述: VitePress 博客自动化部署工具\n')
    return
  }
  
  try {
    // 执行构建部署
    await buildAndDeploy()
    
    // 如果启用监听模式
    if (isWatchMode) {
      startWatchMode()
    }
  } catch (error) {
    logger.error(`执行失败: ${error.message}`)
    process.exit(1)
  }
}

// 执行主函数
main()

安装依赖:

bash
cd my-vitepress-blog
npm install chalk chokidar --save-dev

3. 主项目 Vite 配置

文件: my-blog-frontend/vite.config.ts

typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
import { join } from 'path'
import { existsSync, readFileSync } from 'fs'

export default defineConfig(({ command, mode }) => {
  const isDevelopment = mode === 'development'
  
  return {
    // 🌐 服务器配置
    server: {
      port: 5173,
      host: true,
      open: false,
      cors: true,
      
      // 🔌 代理配置(根据实际情况调整)
      proxy: {
        '/api': {
          target: 'http://localhost:3000',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, '')
        }
      }
    },
    
    // 📦 构建配置
    build: {
      outDir: 'dist',
      assetsDir: 'assets',
      sourcemap: isDevelopment,
      rollupOptions: {
        output: {
          // 代码分割配置
          manualChunks: {
            'vue-vendor': ['vue', 'vue-router', 'pinia'],
            'ui-vendor': ['element-plus', 'axios']
          }
        }
      },
      // 生产环境移除 console 和 debugger
      terserOptions: isDevelopment ? undefined : {
        compress: {
          drop_console: true,
          drop_debugger: true
        }
      }
    },
    
    // 🔌 插件配置
    plugins: [
      vue(),
      
      // 📝 博客静态文件服务中间件(仅开发环境)
      isDevelopment && {
        name: 'vite-plugin-blog-server',
        configureServer(server) {
          const blogMiddleware = (req, res, next) => {
            const url = req.url || ''
            
            // 处理 /blog 重定向到 /blog/
            if (url === '/blog') {
              res.writeHead(302, { 
                Location: '/blog/',
                'Cache-Control': 'no-cache'
              })
              res.end()
              return
            }
            
            // 处理 /blog/ 根路径
            if (url === '/blog/') {
              const indexPath = join(process.cwd(), 'public/blog/index.html')
              if (existsSync(indexPath)) {
                res.setHeader('Content-Type', 'text/html; charset=utf-8')
                res.setHeader('Cache-Control', 'no-cache')
                res.end(readFileSync(indexPath, 'utf-8'))
                return
              }
            }
            
            // 处理 /blog/*.html 其他页面
            if (url.startsWith('/blog/') && url.endsWith('.html')) {
              const filePath = join(process.cwd(), 'public/blog', url.replace('/blog/', ''))
              if (existsSync(filePath)) {
                res.setHeader('Content-Type', 'text/html; charset=utf-8')
                res.setHeader('Cache-Control', 'no-cache')
                res.end(readFileSync(filePath, 'utf-8'))
                return
              }
            }
            
            next()
          }
          
          // 插入中间件,在 Vite 静态文件服务之前处理
          server.middlewares.use(blogMiddleware)
        }
      },
      
      // 🛠️ 开发工具(根据需求选择)
      mode === 'development' && require('@vitejs/plugin-vue-devtools')(),
    ].filter(Boolean),
    
    // 🎯 解析配置
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url)),
        '~': fileURLToPath(new URL('./public', import.meta.url))
      },
      extensions: ['.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    },
    
    // 📝 环境变量配置
    envPrefix: ['VITE_', 'VUE_'],
    
    // 🎨 CSS 配置
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@import "@/styles/variables.scss";`
        }
      }
    }
  }
})

4. 主项目路由配置(Vue Router)

文件: my-blog-frontend/src/router/index.ts

typescript
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '@/views/HomeView.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: HomeView,
    meta: {
      title: '首页',
      requiresAuth: false
    }
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('@/views/AboutView.vue'),
    meta: {
      title: '关于我们',
      requiresAuth: false
    }
  },
  // 📌 注意:不要为 /blog* 定义路由,由静态文件服务处理
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('@/views/NotFoundView.vue'),
    meta: {
      title: '页面未找到'
    }
  }
]

const router = createRouter({
  // ⚠️ 使用与 Vite base 配置一致的 history 模式
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  
  // 📝 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

// 🛡️ 路由守卫
router.beforeEach((to, from, next) => {
  // 设置页面标题
  const title = to.meta.title as string || 'Vue 应用'
  document.title = `${title} - 我的网站`
  
  // 处理 /blog 路径 - 直接放行,由静态文件服务处理
  if (to.path.startsWith('/blog')) {
    // 不进行路由跳转,让静态文件服务处理
    window.location.href = to.fullPath
    return
  }
  
  // 其他路由逻辑...
  next()
})

export default router

5. 主项目入口文件配置

文件: my-blog-frontend/src/main.ts

typescript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

// 全局样式
import './styles/global.css'

const app = createApp(App)

// 状态管理
const pinia = createPinia()
app.use(pinia)

// 路由
app.use(router)

// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
  console.error('全局错误:', err)
  console.error('Vue 实例:', instance)
  console.error('错误信息:', info)
}

// 生产环境禁用警告
if (import.meta.env.PROD) {
  app.config.warnHandler = () => {}
}

app.mount('#app')

6. 自动化脚本优化

文件: my-blog-frontend/package.json(部分)

json
{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "deploy:blog": "cd ../my-vitepress-blog && node deploy.mjs",
    "dev:with-blog": "npm run deploy:blog && vite",
    "watch:blog": "cd ../my-vitepress-blog && node deploy.mjs --watch",
    "build:all": "npm run deploy:blog && npm run build"
  }
}

常见问题及解决方案

问题现象可能原因解决方案
访问 /blog/ 显示主应用首页Vue Router 劫持了请求确保路由配置中没有 /blog* 路由
博客页面白屏,无样式VitePress base 配置错误检查 VitePress config 中 base: '/blog/'
资源文件 404构建后未重新部署运行 npm run deploy:blog 重新部署
热更新不工作VitePress 监听模式未启用使用 npm run watch:blog 启动监听
路由跳转异常路由守卫处理不当检查 router.beforeEach 中的 /blog 处理逻辑
生产环境无法访问Nginx 配置错误检查 Nginx location /blog/ 配置

📝 总结

本方案提供了一个完整的 VitePress 博客集成解决方案,涵盖了从开发环境到生产环境的全部配置。通过合理的路径配置、中间件处理和自动化脚本,实现了博客与主应用的无缝集成。

关键要点

  1. 路径一致性:确保 VitePress 的 base 配置与部署路径一致
  2. 路由隔离:避免 Vue Router 处理博客路径,由静态文件服务处理
  3. 自动化部署:使用部署脚本实现构建产物的自动复制
  4. 开发体验:支持监听模式,实现热更新
  5. 生产优化:Nginx 配置支持静态资源缓存和 Gzip 压缩

Released under the MIT License.