@lzwme/prerender-kit
v0.1.2
Published
通用预渲染(SSG)工具包。基于无头浏览器将 SPA 路由渲染为静态 HTML,内置增量预渲染能力,支持 vite / webpack / rollup 插件及 CLI 独立使用。
Downloads
64
Readme
@lzwme/prerender-kit
通用的 SPA 预渲染(SSG)工具包:基于无头浏览器将路由渲染为静态 HTML,用于 SEO 与首屏优化。
核心渲染逻辑与构建工具完全解耦——vite / webpack / rollup 仅作为薄适配层,同时提供 CLI 与编程式 API,可独立对接任意已运行的站点。
目录
快速开始
pnpm add -D @lzwme/prerender-kit puppeteer// vite.config.ts
import { createVitePlugin } from '@lzwme/prerender-kit';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
createVitePlugin({
routes: ['/', '/about'],
concurrency: 5,
}),
],
});构建完成后,插件会在 closeBundle 阶段自动启动预览服务、渲染路由并写入 <outDir>/<route>/index.html。
功能特点
- 构建工具无关:核心
Prerenderer不依赖任何构建工具,适配层遵循同一套约定 - 开箱即用:
createVitePlugin/PrerenderWebpackPlugin/createRollupPlugin - 独立使用:CLI 或 API 指定
baseUrl即可预渲染(含已部署站点) - 增量预渲染:产物在有效期内自动跳过,支持
force与maxAge - 断点续传:渲染中断后再次运行可从上次进度继续,配合构建指纹避免复用陈旧产物
- 链接自动发现:
discoverLinks从产物中提取站内链接,逐层扩散 - 一站式收尾:可选 HTML 瘦身、sitemap / robots.txt 生成
- 页面错误可见:收集
pageerror/console.error,可配置failOnPageError - 渲染器可替换:默认 puppeteer(可选依赖),可接入 playwright、SSR 等
- 零依赖内置静态服务:未配置
baseUrl时自动启动 - TypeScript + ESM-only:完整类型定义,Node
>=20.19
如何选择使用方式
| 场景 | 推荐方式 |
| --- | --- |
| Vite 项目构建后自动预渲染 | createVitePlugin |
| Webpack / Rollup 项目 | PrerenderWebpackPlugin / createRollupPlugin |
| 站点已部署或本地已有服务 | CLI:prkit -u <url> -o <dir> ... |
| CI 脚本、自定义流水线 | prerender() / new Prerenderer() |
| 仅优化已有 HTML / 生成 sitemap | optimizeHtmlFiles() / generateSitemap() |
安装
# 作为构建插件(项目内)
pnpm add -D @lzwme/prerender-kit puppeteer
# 作为 CLI 全局安装
pnpm add -g @lzwme/prerender-kit puppeteer
# npm / yarn 亦可
npm i -D @lzwme/prerender-kit puppeteer
puppeteer为可选 peer 依赖,仅在真正执行渲染时动态加载。若通过renderer注入自定义渲染器(如 playwright),可不安装 puppeteer。
指定浏览器可执行文件
未显式配置 launchOptions.executablePath 时,依次读取以下环境变量(命中即止):
PUPPETEER_EXECUTABLE_PATHCHROME_EXECUTABLE_PATHCHROME_EXECUTABLE
export CHROME_EXECUTABLE_PATH=/usr/bin/google-chrome
pnpm build优先级:显式配置 > 环境变量 > puppeteer 自带 Chromium。resolveExecutablePath() 与 EXECUTABLE_PATH_ENV_KEYS 已导出,便于自定义渲染器复用。
使用方式
Vite 插件
import { createVitePlugin } from '@lzwme/prerender-kit';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
createVitePlugin({
routes: ['/', '/about', '/zh', '/zh/about'],
delay: 300,
concurrency: 10,
maxAge: 120, // 产物 2 小时内视为有效,跳过渲染
renderer: undefined, // puppeteer 启动参数
}),
],
});outDir 与 base 默认从 vite 配置自动获取。插件优先使用 vite 的 preview API 提供访问地址,失败时回退内置静态服务,渲染完成后自动关闭。
Webpack 插件
const { PrerenderWebpackPlugin } = require('@lzwme/prerender-kit');
module.exports = {
plugins: [
new PrerenderWebpackPlugin({
routes: ['/', '/about'],
concurrency: 5,
}),
],
};outDir 默认取 compiler.outputPath,在 afterEmit 钩子中执行。
Rollup 插件
import { createRollupPlugin } from '@lzwme/prerender-kit';
export default {
output: { dir: 'dist' },
plugins: [createRollupPlugin({ routes: ['/', '/about'] })],
};outDir 默认取 writeBundle 的 outputOptions.dir。
CLI
命令名:prerender-kit 或别名 prkit。
# 对线上站点预渲染
prkit -u https://example.com -o ./dist / /about /product
# 对本地已启动的服务预渲染
prkit -u http://127.0.0.1:3000 -o ./dist -r routes.txt
# 未提供 baseUrl 时,以 static-dir 为根目录启动内置静态服务
prkit -o ./dist -s ./dist -n 8 --max-age 60 / /about
# 使用配置文件(可导出完整 PrerenderOptions)
prkit -c prerender.config.jsroutes.txt 示例(# 开头为注释,也支持 .json 数组或 .js 导出数组):
/
/about
/product/compress-image常用 CLI 参数:--force、--resume、--build-id <id>、--discover-links、--optimize、--sitemap --site-url <url>、--max-age 0(每次全量渲染)。完整参数列表请运行 prkit --help。
编程式 API
import { Prerenderer, prerender } from '@lzwme/prerender-kit';
const result = await prerender({
routes: ['/', '/about'],
outDir: 'dist/web',
baseUrl: 'https://example.com', // 不设置则自动启动内置静态服务
concurrency: 10,
maxAge: 60,
delay: 300,
callback: (html, route) => html.replace('<title></title>', `<title>${route}</title>`),
});
console.log(result.rendered, result.skipped, result.failed);配置参考
以下参数适用于插件、CLI(-c 配置文件)与编程式 API,为同一份 PrerenderOptions。
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| routes | string[] | 必填 | 需要预渲染的路由 |
| outDir | string | 必填(适配器可自动推断) | 产物输出目录 |
| baseUrl | string | - | 已运行站点的访问地址。设置后不再启动本地服务 |
| base | string | / | 站点部署的 base 路径 |
| hashHistory | boolean | false | hash 路由模式 |
| staticDir | string | outDir | 未提供 baseUrl 时内置静态服务的根目录 |
| staticPort | number | 随机 | 内置静态服务端口 |
| concurrency | number | 5 | 并发渲染数量 |
| force | boolean | false | 强制重新渲染,忽略已有产物 |
| maxAge | number | 60 | 产物有效期(分钟),0 表示总是重新渲染 |
| resume | boolean \| ResumeOptions | false | 断点续传,中断后从上次进度继续,详见 断点续传 |
| buildId | string | 自动计算 | 构建指纹。变化时断点状态失效并全量重渲染 |
| delay | number | - | 页面加载完成后的额外等待(ms) |
| waitUntil | string | domcontentloaded | 页面等待策略,networkidle0 可等待异步接口 |
| waitForSelector | string | body | 渲染前等待出现的选择器 |
| viewport | {width,height} | 1024×768 | 视口大小 |
| removeStyle | boolean | true | 移除内联 <style> 标签 |
| removeBaseUrl | boolean | true | 移除产物中的渲染站点地址 |
| replaceUrl | string \| string[] | - | 额外需要从产物中移除的地址/字符串 |
| discoverLinks | boolean | false | 自动发现站内链接并加入预渲染队列 |
| discoverFilter | (url) => boolean | - | 自定义链接过滤 |
| maxRoutes | number | 0(不限制) | 预渲染路由总数上限 |
| failOnPageError | boolean | false | 页面存在运行时错误时视为渲染失败 |
| optimize | boolean \| HtmlOptimizeOptions | false | HTML 瘦身,详见 HTML 瘦身 |
| sitemap | boolean \| SitemapOptions | false | 生成 sitemap,详见 Sitemap |
| callback | (html, route) => string \| void | - | 产物落地前的自定义后处理 |
| renderer | Renderer \| (() => Renderer) | puppeteer | 自定义渲染器 |
| outputFile | (route, outDir) => string | <outDir>/<route>/index.html | 自定义产物路径,返回空字符串表示跳过 |
| logger | Logger \| false | console | 自定义日志,false 为静默 |
进阶能力
增量预渲染
预渲染通常是构建中最耗时的环节。默认策略下,产物存在且最后修改时间在 maxAge 分钟(默认 60)内则跳过该路由:
- 全部命中时,不会启动预览服务与浏览器,构建几乎零额外开销
- 部分命中时,仅渲染缺失与已过期的路由
force: true(或 CLI--force)强制全量重新渲染maxAge: 0(或 CLI--max-age 0)表示每次都重新渲染
断点续传
路由数量多、单页渲染耗时长时,一次预渲染可能持续几十分钟。构建被 Ctrl+C、CI 超时或进程崩溃中断后,
resume 可让下一次运行从中断处继续,而不是从头再来:
await prerender({
routes,
outDir: 'dist/web',
resume: true, // 状态文件默认 <outDir>/.prerender-state.json
// buildId: process.env.GIT_COMMIT_SHA, // 建议 CI 显式指定
// resume: { file: 'node_modules/.cache/prerender-kit/state.json' },
});CLI:prkit --resume [--build-id <id>] ...
设计要点:
| 机制 | 说明 |
| --- | --- |
| 实时落盘 | 状态按时间节流实时写入,进程被强杀最多丢失最近 0.5 秒内完成的进度 |
| 原子写入 | 产物先写 .tmp 再 rename,中断不会留下半截 HTML 被误判为「已完成」 |
| 失败自动重试 | 上次失败的路由保持在队列中,下次运行自动重试 |
| 构建指纹 | buildId 或 assets/ 文件名(自带 content hash)作为指纹,变化时状态失效并全量重渲染 |
| 产物校验 | 状态命中但产物文件被删除、或 mtime/size 与记录不一致(被外部改写)时,重新渲染该路由 |
开启 resume 后以状态为唯一判据,maxAge 不再参与判断(maxAge 仅在未开启 resume 时生效)。
这不是功能退化而是修正:新鲜度只能说明「文件新」,无法说明「是谁写的」,
典型场景就是 / 的产物等于构建入口 index.html——每次构建都会覆写它,
按新鲜度会永远判定为「已渲染」,导致首页始终拿不到预渲染内容。
为什么需要「构建指纹」:仅凭「产物存在」无法区分「本次构建渲染的」与「上次构建遗留的陈旧产物」。 指纹不一致时,即使产物仍在
maxAge有效期内也会重新渲染。指纹来源刻意不包含
<outDir>/index.html——预渲染/会覆写该文件, 若纳入指纹会导致同一份构建的两次运行指纹不同、状态自我失效。 无assets/目录且未指定buildId时指纹为空,退化为「仅按完成状态续跑」,建议在 CI 显式传入buildId。
相关 API 已导出:loadState / saveState / createState / isRouteResumable / resolveSignature / resolveStateFile / writeFileAtomic。
链接自动发现
开启 discoverLinks 后,只需配置入口路由,工具会像爬虫一样逐层发现站内链接:
await prerender({
routes: ['/'],
outDir: 'dist',
baseUrl: 'https://example.com',
discoverLinks: true,
discoverFilter: (url) => !url.startsWith('/admin'),
maxRoutes: 500,
});执行策略:同一层内并发渲染,渲染完成后从产物中提取新链接作为下一层,直至没有新路由或达到上限。
链接发现规则:
- 必须有
href,不带download,target若存在则必须为_self - 排除
mailto:/tel:/javascript:/#等非页面链接 - 默认仅保留站内链接,并剥离
query与hash - 命中缓存而被跳过的路由,也会读取已有产物继续发现链接
extractLinks(html, options) 已单独导出,可在自定义 callback 或 SSR 渲染器中复用。
页面运行时错误
预渲染时页面报错往往被静默忽略,导致产物不完整却「构建成功」。本工具会收集 pageerror 与 console.error,输出到 result.pageErrors:
const result = await prerender({ routes, outDir, failOnPageError: true });
if (result.failed.length) process.exitCode = 1;默认 puppeteer 渲染器在并发场景下按页面 URL 隔离错误归属。自定义渲染器实现 drainErrors(url?) 时,建议同样按 URL 区分,避免错误串扰。
HTML 瘦身
预渲染产物常包含运行时会被重新生成的内容(内联 SVG、大段内联样式、class 属性、注释与空白)。开启 optimize 可在落盘前裁掉这些冗余:
// 推荐组合
await prerender({ routes, outDir, optimize: true });
// 按需组合
await prerender({
routes,
outDir,
optimize: {
removeInlineSvg: true,
removeEmptyWrappers: false,
removeInlineStyles: true,
inlineStylesMinLength: 1000,
removeClassAttributes: true,
minify: true,
loadingIndicator: { target: '<div id="app">' },
},
});| 开关 | 默认(启用 optimize 时) | 说明 |
| --- | --- | --- |
| removeInlineSvg | true | 移除 <svg>...</svg> |
| removeEmptyWrappers | false | 清理确认为空的成对包裹层 |
| removeInlineStyles | true | 移除超过阈值的 <style> |
| inlineStylesMinLength | 1000 | 设为 0 则移除全部内联样式 |
| removeClassAttributes | true | 移除 class="...",保护 script/style 内部 |
| minify | true | 移除注释并压缩标签间空白 |
| loadingIndicator | false | 在挂载点后注入首屏 loading 指示器 |
也可作为独立步骤使用(与预渲染解耦):
import { optimizeHtmlFiles } from '@lzwme/prerender-kit';
optimizeHtmlFiles({
dir: 'dist/web',
removeInlineSvg: true,
removeClassAttributes: true,
minify: true,
exclude: [/static\//],
writeOnlyWhenSmaller: true,
});单项能力同样独立导出:preserveTagBlocks / removeInlineSvg / removeInlineStyles / removeClassAttributes / minifyHtml / addLoadingIndicator / optimizeHtml。
minify仅压缩标签之间的空白,文本节点内部的空白会保留。
Sitemap 与 robots.txt
预渲染后工具已掌握全部路由与产物文件,正是生成 sitemap 的最佳时机:
await prerender({
routes,
outDir: 'dist',
sitemap: {
siteUrl: 'https://example.com',
languages: ['zh', 'en', 'zh-TW'],
priority: { '/': 1.0, '/image-compress': 0.9 },
changeFreq: { '/': 'daily' },
exclude: [/^\/admin/, '/login'],
robots: { rules: [{ userAgent: '*', disallow: ['/admin'] }] },
gzip: true,
},
});也可完全独立调用:
import { generateSitemap } from '@lzwme/prerender-kit';
// 不传 routes 时扫描 outDir 下的 HTML 产物
generateSitemap({ siteUrl: 'https://example.com', outDir: 'dist', languages: ['zh', 'en'] });| 配置项 | 默认 | 说明 |
| --- | --- | --- |
| siteUrl | - | 站点地址,必填 |
| routes | 扫描 outDir | 需要收录的路由 |
| outDir / outFile | <outDir>/sitemap.xml | 产物目录与输出文件 |
| base | / | 子路径部署时的 base |
| exclude | - | 排除规则(字符串或正则) |
| priority / changeFreq | 0.5 / weekly | 支持按基础路由的对象或函数 |
| lastmod | file | 取产物 HTML 修改时间;也可 today / none / 固定日期 / 函数 |
| languages | - | 多语言列表,输出 hreflang alternate |
| languageRoute | prefix | prefix / suffix / 自定义函数 |
| xDefault | true | 是否输出 x-default |
| maxUrlsPerFile | 45000 | 超出则拆分并生成 index;0 不拆分 |
| robots | false | 是否生成 robots.txt |
| gzip | false | 是否同时输出 .gz |
CLI:--sitemap --site-url https://example.com --languages zh,en --robots。
默认从产物目录扫描路由,避免收录预渲染失败的页面;
lastmod默认取产物文件真实修改时间。
自定义渲染器
渲染器只需实现 Renderer 接口。以下示例为 Playwright:
import type { Renderer } from '@lzwme/prerender-kit';
const playwrightRenderer = (): Renderer => {
let browser: Awaited<ReturnType<typeof import('playwright').chromium.launch>> | undefined;
return {
name: 'playwright',
async launch() {
const { chromium } = await import('playwright');
browser = await chromium.launch();
},
async render(url, options) {
const page = await browser!.newPage();
await page.goto(url, { waitUntil: options?.waitUntil });
if (options?.delay) await new Promise((r) => setTimeout(r, options.delay));
const html = await page.content();
await page.close();
return html;
},
async close() {
await browser?.close();
},
};
};
await prerender({ routes, outDir, renderer: playwrightRenderer() });若应用支持 SSR,也可完全跳过浏览器:
import { prerender } from '@lzwme/prerender-kit';
const ssrRenderer = (): Renderer => ({
name: 'ssr',
async launch() {},
async render(url) {
const { prerender: appPrerender } = await import('./dist-ssr/entry-server.js');
const { html } = await appPrerender(new URL(url).pathname);
return html;
},
async close() {},
});环境与兼容性
Node 版本
要求 Node >=20.19(见 package.json 的 engines 字段)。
ESM-only
本包仅提供 ESM 产物:
- ESM 项目(推荐):
import { createVitePlugin } from '@lzwme/prerender-kit' - CJS 项目:Node 20.19+ / 22.12+ 起
require(esm)默认可用;更低版本需await import() - TypeScript + CJS 配置:建议改为 ESM 配置,或使用动态
import()
选择 ESM-only 的理由:避免双产物带来的「双包实例」问题;原生 import() 可正常加载 puppeteer、vite 等 ESM 包;与现代 Node 及 Vite 生态对齐。
附录
与 vite-prerender-plugin 的对比
vite-prerender-plugin 是同类方案中的优秀实现,二者定位互补:
| 维度 | vite-prerender-plugin | @lzwme/prerender-kit |
| --- | --- | --- |
| 渲染方式 | 进程内 SSR | 无头浏览器(默认 puppeteer,可替换) |
| 框架要求 | 应用需提供 prerender 入口 | 零要求,任意 SPA 均可 |
| 速度 | 快(无浏览器) | 较慢,可用增量缓解 |
| 构建工具 | 仅 vite | vite / webpack / rollup / CLI / API |
| 增量/缓存 | 无 | force / maxAge |
| 独立使用 | 不支持 | 支持(任意 baseUrl) |
从 vite-plugin-seo-prerender 迁移
| vite-plugin-seo-prerender | @lzwme/prerender-kit |
| --- | --- |
| routes | routes |
| puppeteer | renderer / createPuppeteerRenderer({ launchOptions }) |
| network: true | waitUntil: 'networkidle0' |
| removeStyle | removeStyle |
| delay | delay |
| concurrency | concurrency |
| callback | callback |
| hashHistory | hashHistory |
| - | 新增 baseUrl、force、maxAge、resume、buildId、outputFile、renderer |
publicHtml、scss 等与预渲染无关的能力不再内置,建议交由 callback 或独立工具处理。
开发
corepack enable # 首次启用,使用 pnpm@11
pnpm install
pnpm build # 输出 ESM 产物到 dist/
pnpm test # 单元测试(内置静态服务 + fetch 渲染器,无需 puppeteer)
pnpm lint # biome 检查
pnpm check:tsc # 类型检查
pnpm verify # lint + 类型检查 + 测试