PDF 导出与离线文档
将 VitePress 文档导出为 PDF 或生成离线可访问的文档,方便用户在没有网络的环境下阅读。
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Puppeteer | 渲染精确、支持复杂样式 | 需要无头浏览器、速度慢 | 需要精确还原页面样式 |
| Prince XML | 专业排版、支持分页 | 商业软件 | 专业出版物 |
| md-to-pdf | 简单快捷 | 样式还原度低 | 简单文档 |
| PWA 离线 | 自动更新、原生体验 | 仍需浏览器 | 离线阅读首选 |
| HTML 离线包 | 无需服务器、可直接打开 | 交互受限 | 完全离线分发 |
方案一:使用 Puppeteer 导出 PDF
安装依赖
bash
npm install -D puppeteer创建导出脚本
typescript
// scripts/export-pdf.mts
import puppeteer from 'puppeteer'
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
interface PdfOptions {
url: string
output: string
format?: 'A4' | 'Letter'
margin?: { top: string; right: string; bottom: string; left: string }
printBackground?: boolean
}
async function exportPdf(options: PdfOptions) {
const {
url,
output,
format = 'A4',
margin = { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
printBackground = true
} = options
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
})
const page = await browser.newPage()
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 60000
})
// 等待页面完全渲染
await page.waitForSelector('.VPContent', { timeout: 10000 })
// 隐藏不需要的元素
await page.addStyleTag({
content: `
.VPNav,
.VPSidebar,
.VPFooter,
.back-to-top,
.aside {
display: none !important;
}
.VPContent {
padding: 0 !important;
}
`
})
await page.pdf({
path: output,
format,
margin,
printBackground
})
await browser.close()
console.log(`✅ PDF 已导出: ${output}`)
}
// 批量导出
const pages = [
{ url: 'http://localhost:5173/guide/what-is-vitepress', output: 'pdf/guide/what-is-vitepress.pdf' },
{ url: 'http://localhost:5173/guide/installation', output: 'pdf/guide/installation.pdf' },
{ url: 'http://localhost:5173/basics/markdown', output: 'pdf/basics/markdown.pdf' }
]
async function main() {
// 确保输出目录存在
const dirs = new Set(pages.map(p => path.dirname(p.output)))
dirs.forEach(dir => fs.mkdirSync(dir, { recursive: true }))
// 启动开发服务器(如果未运行)
console.log('📋 开始导出 PDF...')
for (const page of pages) {
try {
await exportPdf(page)
} catch (err) {
console.error(`❌ 导出失败 ${page.url}:`, err)
}
}
console.log('🎉 全部导出完成!')
}
main()添加 npm 脚本
json
{
"scripts": {
"export:pdf": "node scripts/export-pdf.mts"
}
}方案二:使用 md-to-pdf
安装
bash
npm install -D md-to-pdf配置文件
typescript
// .md2pdf.ts
import { PdfConfig } from 'md-to-pdf'
const config: PdfConfig = {
pdf_options: {
format: 'A4',
margin: {
top: '20mm',
right: '15mm',
bottom: '20mm',
left: '15mm'
},
printBackground: true
},
stylesheet: ['.vitepress/theme/styles/pdf.css'],
body_class: ['pdf-export'],
css: `
.pdf-export {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
}
.pdf-export h1 { font-size: 24px; }
.pdf-export h2 { font-size: 20px; }
.pdf-export h3 { font-size: 16px; }
.pdf-export pre {
background: #f6f8fa;
padding: 12px;
border-radius: 6px;
font-size: 13px;
}
.pdf-export code {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 3px;
}
`
}
export default config导出脚本
bash
npx md-to-pdf docs/guide/*.md --pdf-dir pdf/guide方案三:PWA 离线访问
PWA 方案允许用户在首次访问后离线浏览文档:
安装依赖
bash
npm install -D @vite-pwa/vitepress配置 PWA
typescript
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { withPwa } from '@vite-pwa/vitepress'
export default withPwa(defineConfig({
pwa: {
registerType: 'autoUpdate',
manifest: {
name: 'VitePress 学习指南',
short_name: 'VP 指南',
theme_color: '#646cff',
icons: [
{
src: '/pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: '/pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{css,js,html,svg,png,ico,txt,woff2}']
}
}
}))提示
PWA 方案是离线阅读的首选方案,用户首次访问后文档会自动缓存,后续可离线访问。详见 PWA 最佳实践。
方案四:HTML 离线包
将构建产物打包为可直接打开的 HTML 文件:
构建脚本
typescript
// scripts/offline-package.mts
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
async function createOfflinePackage() {
console.log('📦 开始创建离线文档包...')
// 1. 构建
console.log('1️⃣ 构建站点...')
execSync('npm run build', { stdio: 'inherit' })
// 2. 将相对路径转为内联资源
console.log('2️⃣ 处理资源引用...')
const distDir = 'docs/.vitepress/dist'
// 内联关键 CSS
const htmlFiles = findHtmlFiles(distDir)
for (const file of htmlFiles) {
inlineAssets(file)
}
// 3. 打包
console.log('3️⃣ 打包...')
const outputName = `vitepress-docs-offline-${getDateStr()}.zip`
execSync(`cd ${distDir} && zip -r ../../../${outputName} .`)
console.log(`✅ 离线文档包已创建: ${outputName}`)
}
function findHtmlFiles(dir: string): string[] {
const results: string[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
results.push(...findHtmlFiles(fullPath))
} else if (entry.name.endsWith('.html')) {
results.push(fullPath)
}
}
return results
}
function inlineAssets(htmlPath: string) {
let content = fs.readFileSync(htmlPath, 'utf-8')
// 将外部 CSS 转为内联
content = content.replace(
/<link[^>]*href="([^"]+\.css)"[^>]*>/g,
(_match, cssPath) => {
const cssFile = path.join(path.dirname(htmlPath), cssPath)
if (fs.existsSync(cssFile)) {
const css = fs.readFileSync(cssFile, 'utf-8')
return `<style>${css}</style>`
}
return _match
}
)
fs.writeFileSync(htmlPath, content, 'utf-8')
}
function getDateStr(): string {
const d = new Date()
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`
}
createOfflinePackage()添加页眉页脚
为 PDF 导出添加页眉和页脚:
typescript
// 在 Puppeteer 的 pdf 选项中添加
await page.pdf({
path: output,
format: 'A4',
margin: {
top: '25mm',
right: '15mm',
bottom: '25mm',
left: '15mm'
},
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 9px; width: 100%; text-align: center; color: #888;">
VitePress 学习指南
</div>
`,
footerTemplate: `
<div style="font-size: 9px; width: 100%; text-align: center; color: #888;">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
`
})自动化导出
CI/CD 集成
yaml
# .github/workflows/export-pdf.yml
name: Export PDF
on:
workflow_dispatch:
release:
types: [published]
jobs:
export:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Export PDF
run: npm run export:pdf
- name: Upload PDF
uses: actions/upload-artifact@v4
with:
name: pdf-documents
path: pdf/最佳实践
| 实践 | 说明 |
|---|---|
| 隐藏导航元素 | PDF 中隐藏导航栏、侧边栏等交互元素 |
| 优化打印样式 | 使用 @media print 添加打印专用样式 |
| 分页控制 | 使用 break-before / break-after 控制分页 |
| 字体嵌入 | 确保中文字体正确嵌入 PDF |
| 链接处理 | 将外部链接保留为可点击的超链接 |