139 lines
4.0 KiB
TypeScript
139 lines
4.0 KiB
TypeScript
/**
|
||
* Vite 插件:集成 @prerenderer/prerenderer 实现预渲染
|
||
*/
|
||
import type { Plugin } from 'vite'
|
||
import type { PrerendererOptions, RenderedRoute } from '@prerenderer/prerenderer'
|
||
import Prerenderer from '@prerenderer/prerenderer'
|
||
import PuppeteerRenderer from '@prerenderer/renderer-puppeteer'
|
||
import { generatePrerenderRoutes } from './generate-prerender-routes.js'
|
||
import path from 'node:path'
|
||
import fs from 'node:fs'
|
||
|
||
interface PrerenderPluginOptions {
|
||
/**
|
||
* 需要预渲染的路由列表
|
||
* 如果不提供,将从工具 store 自动生成
|
||
*/
|
||
routes?: string[]
|
||
|
||
/**
|
||
* 预渲染器选项
|
||
*/
|
||
rendererOptions?: {
|
||
/**
|
||
* 渲染超时时间(毫秒)
|
||
*/
|
||
maxConcurrentRoutes?: number
|
||
|
||
/**
|
||
* 渲染超时时间(毫秒)
|
||
*/
|
||
timeout?: number
|
||
|
||
/**
|
||
* 等待选择器出现后再渲染
|
||
*/
|
||
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
|
||
}
|
||
|
||
/**
|
||
* 是否在开发模式下启用(默认 false)
|
||
*/
|
||
dev?: boolean
|
||
}
|
||
|
||
export function vitePluginPrerender(options: PrerenderPluginOptions = {}): Plugin {
|
||
const {
|
||
routes: customRoutes,
|
||
rendererOptions = {},
|
||
dev = false
|
||
} = options
|
||
|
||
let outputDir: string
|
||
|
||
return {
|
||
name: 'vite-plugin-prerender',
|
||
apply: 'build',
|
||
|
||
configResolved(config) {
|
||
outputDir = config.build.outDir || 'dist'
|
||
},
|
||
|
||
async closeBundle() {
|
||
// 只在生产构建时执行预渲染
|
||
if (dev && process.env.NODE_ENV === 'development') {
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 获取需要预渲染的路由列表
|
||
const routes = customRoutes || generatePrerenderRoutes()
|
||
|
||
if (!routes || routes.length === 0) {
|
||
return
|
||
}
|
||
|
||
// 确保输出目录存在
|
||
if (!fs.existsSync(outputDir)) {
|
||
console.error(`输出目录不存在: ${outputDir}`)
|
||
return
|
||
}
|
||
|
||
// 配置 prerenderer
|
||
const prerenderer = new Prerenderer({
|
||
staticDir: outputDir,
|
||
renderer: new PuppeteerRenderer({
|
||
maxConcurrentRoutes: rendererOptions.maxConcurrentRoutes || 4,
|
||
timeout: rendererOptions.timeout || 30000,
|
||
waitUntil: rendererOptions.waitUntil || 'networkidle0',
|
||
// 渲染选项:等待页面加载完成
|
||
renderAfterTime: 3000, // 等待 3 秒确保Vue组件挂载和SEO标签注入完成
|
||
// 注入一些配置,确保页面正确渲染
|
||
inject: {
|
||
__PRERENDER__: true
|
||
}
|
||
})
|
||
})
|
||
|
||
// 初始化并执行预渲染
|
||
await prerenderer.initialize()
|
||
|
||
const renderedRoutes = await prerenderer.renderRoutes(routes)
|
||
|
||
// 处理渲染结果:保存为扁平化结构的 HTML 文件
|
||
for (const renderedRoute of renderedRoutes) {
|
||
// 使用 originalRoute 而不是 route(因为 route 可能被重定向)
|
||
const originalRoute = renderedRoute.originalRoute || renderedRoute.route
|
||
const routePath = originalRoute === '/'
|
||
? 'index.html'
|
||
: `${originalRoute.slice(1)}.html`
|
||
|
||
const outputPath = path.join(outputDir, routePath)
|
||
|
||
// 确保目录存在
|
||
const dirPath = path.dirname(outputPath)
|
||
if (dirPath !== outputDir) {
|
||
fs.mkdirSync(dirPath, { recursive: true })
|
||
}
|
||
|
||
// 保存 HTML 内容
|
||
if (renderedRoute.html) {
|
||
fs.writeFileSync(outputPath, renderedRoute.html, 'utf-8')
|
||
}
|
||
}
|
||
|
||
// 清理资源
|
||
await prerenderer.destroy()
|
||
} catch (error) {
|
||
// 不抛出错误,避免中断构建流程
|
||
// 如果预渲染失败,至少还有 SPA 版本可以工作
|
||
const message = error instanceof Error ? error.message : String(error)
|
||
console.error('❌ 预渲染失败:', message)
|
||
if (error instanceof Error && error.stack) {
|
||
console.error(error.stack)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|