Appearance
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-dev3. 主项目 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 router5. 主项目入口文件配置
文件: 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 博客集成解决方案,涵盖了从开发环境到生产环境的全部配置。通过合理的路径配置、中间件处理和自动化脚本,实现了博客与主应用的无缝集成。
关键要点
- 路径一致性:确保 VitePress 的
base配置与部署路径一致 - 路由隔离:避免 Vue Router 处理博客路径,由静态文件服务处理
- 自动化部署:使用部署脚本实现构建产物的自动复制
- 开发体验:支持监听模式,实现热更新
- 生产优化:Nginx 配置支持静态资源缓存和 Gzip 压缩