/** * 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) } } } } }