jcloud/frontend/scripts/vite-plugin-prerender.ts
2025-12-27 21:35:18 +08:00

139 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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