提取静态 HTML
从任何 Web 应用程序中提取自包含的静态 HTML 文件。
使用哪种策略
你必须在执行前询问用户选择哪种策略。清晰地呈现选项,推荐策略 A 作为首选默认值,并为每个选项提供简要的优缺点总结,以帮助他们做出明智的决定。
| 策略 A (Puppeteer) | 策略 B (浏览器子代理) | |
|---|---|---|
| 何时使用 | 应用程序在本地运行,无需身份验证 | 需要先与页面交互(点击、填写表单) |
| 保真度 | 最高——解析计算后的样式 | 高——渲染后的 DOM |
| 设置 | 零——无需模拟 | 零——无需模拟 |
| 框架 | 任何 | 任何 |
| 输出 | 写入文件——无大小限制 | 可能在代理上下文中被截断 |
[!WARNING] 检查点——需要用户确认。 你必须询问用户他们偏好哪种策略才能继续。 提供上面的比较表格,推荐策略 A 作为默认值,并 等待用户明确批准。在用户确认之前,请不要自行决定或继续。
策略 A:Puppeteer 快照(推荐)
启动无头 Chrome,捕获完全渲染的 DOM,并生成一个自包含的 HTML 文件,其中所有 CSS 已内联,图片已转为 base64。适用于任何框架——无需 MockPage.jsx。
前提条件
- 应用程序在本地运行(例如,
npm run dev) - 已安装 Node.js 并包含
puppeteer(检查:node -e "require('puppeteer')")
工作流程
-
启动应用程序并记下端口。
[!WARNING] 检查点——需要用户确认。 在启动本地服务器后,你必须暂停并询问用户确认,然后再运行快照脚本或启动浏览器子代理。向用户报告 URL 和端口,以便他们验证应用程序正在运行并正确渲染。在用户确认之前,请不要进行快照步骤。
-
运行快照脚本:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173 \ --output .stitch/home.html \ --wait 2000 -
多个页面——每条路由运行一次:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173 --output .stitch/home.html --wait 2000 npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173/pricing --output .stitch/pricing.html --wait 2000 npx tsx <SKILL_DIR>/scripts/snapshot.ts \ --url http://localhost:5173/dashboard --output .stitch/dashboard.html --wait 2000 --html-class dark -
清理开发服务器: 如果专门为快照提取启动了本地开发服务器,请确保在提取完成后停止服务器进程或终止后台任务。
脚本标志
| 标志 | 默认值 | 描述 |
|---|---|---|
--url | (必需) | 要捕获的 URL |
--output | (必需) | 输出文件路径 |
--wait | 1000 | 网络空闲后的额外等待时间(毫秒)。对于懒加载应用程序,请增加此值。 |
--viewport | 1280x800 | 视口大小,格式为 WIDTHxHEIGHT |
--html-class | — | <html> 元素的类(例如,dark) |
--remove-fixed | false | 移除固定/粘性元素(Cookie 横幅、聊天窗口小部件) |
--full-height | false | 将视口调整为完整的滚动高度 |
--title | — | 覆盖页面标题(设置为路由路径,例如 /dashboard 或 /settings/profile) |
--auth-script | — | 一个 JS/TS 模块的路径,该模块导出默认的 async (page) => void 函数用于身份验证 |
--inline-canvas | false | 将 <canvas> 元素(ECharts、Chart.js、D3)转换为 base64 <img> 标签 |
自动完成的内容
- 从
document.styleSheets捕获所有 CSSOM 规则(保留动态的 Vite/Tailwind 开发样式和 CSS-in-JS) - 将所有
<link rel="stylesheet">内联为<style>块 - 将
<img>的src和srcset转换为 base64 数据 URI(跳过外部字体) - 将同源和相对路径的图标字体文件(
@font-face)内联为 base64 数据 URI,以便连字永远不会渲染为 ASCII 文本 - 内联
<source srcset>URL 为 base64 - 移除失败/无效的
srcset条目,以便浏览器回退到内联的src - 移除
<script>标签、Vite HMR 开发样式块(createHotContext、import.meta.hot)和开发覆盖层 - 在内联之前解析相对路径的 CSS
url()
框架说明
| 框架 | 说明 |
|---|---|
| React + Vite | 开箱即用。--wait 1000。 |
| Next.js | --wait 3000 用于 SSR 水合。URL:http://localhost:3000。/_next/image 的 <img srcset> 会自动内联为 base64。 |
| Angular (@angular/cli / v17+) | 使用 ng serve 开箱即用(默认 URL:http://localhost:4200)。--wait 2000 用于 Angular Material / PrimeNG 动画水合和懒加载路由。 |
| Vue / Nuxt | 开箱即用。 |
| Svelte / SvelteKit | 开箱即用。 |
| Storybook | 使用 Story URL:--url http://localhost:6006/?path=/story/... |
| SSR (Webpack) | 可能需要更长的 --wait。 |
故障排除
| 问题 | 解决方案 |
|---|---|
| 图片缺失 | 增加 --wait |
| 服务器停止后图片显示为损坏 | 验证 srcset 已内联——检查日志中的 "Inlined N images"。如果 srcset URL 失败,它们会被自动移除,以便使用 src(已内联)。 |
| 图标显示为文本 / Serif 无样式字体 | 确保 snapshot.ts 从 document.styleSheets(步骤 0)捕获 CSSOM,并且同源图标字体(@font-face)已内联为 base64 数据 URI。 |
Next.js /_next/image 未内联 | 确保快照运行时开发服务器正在运行——脚本会从正在运行的服务器获取优化后的图片。 |
| 未应用暗模式 | --html-class dark |
| 输出中出现 Cookie 横幅 | --remove-fixed |
| 页面需要登录 | 使用 --auth-script ./auth.ts(请参阅下面的身份验证门控页面) |
| 图表/图形显示为空白框 | 使用 --inline-canvas 将 <canvas> 序列化为 base64 <img> |
Cannot find module 'puppeteer' | npm install -g puppeteer |
身份验证门控页面
对于具有登录保护的应用程序(Vue Router beforeEach、React ProtectedRoute 等),创建一个在 Puppeteer 会话中运行的小型身份验证脚本:
// auth-myapp.ts
import type { Page } from 'puppeteer';
export default async function authenticate(page: Page) {
// 示例 1:填写并提交登录表单
await page.type('#username', 'admin');
await page.type('#password', 'password123');
await page.click('#login-button');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
// 示例 2:直接注入 Cookie/localStorage
// await page.evaluate(() => {
// localStorage.setItem('token', 'mock-jwt-token');
// });
// 示例 3:通过模块注入调用应用程序自己的登录 API(Vue/Vite)
// await page.evaluate(() => {
// return new Promise((resolve) => {
// const script = document.createElement('script');
// script.type = 'module';
// script.textContent = `
// import { useUserStore } from '/src/store/modules/user.ts';
// import { fetchLogin } from '/src/api/auth.ts';
// const res = await fetchLogin({ userName: 'Admin', password: '123456' });
// useUserStore().setToken(res.token, res.refreshToken);
// window.dispatchEvent(new CustomEvent('auth-done'));
// `;
// document.head.appendChild(script);
// window.addEventListener('auth-done', () => resolve(true), { once: true });
// });
// });
}
然后使用它:
npx tsx <SKILL_DIR>/scripts/snapshot.ts \
--url http://localhost:5173/#/dashboard \
--output .stitch/dashboard.html \
--auth-script ./auth-myapp.ts \
--inline-canvas \
--wait 5000
脚本首先导航到 --url(可能会重定向到登录页面),运行你的身份验证函数,然后使用经过身份验证的会话重新导航到原始的 --url。
策略 B:浏览器子代理捕获
当你需要在捕获之前与页面交互(点击按钮、填写表单、导航标签页)时使用。浏览器子代理为你提供完全控制,但对于大型页面,输出可能会被截断。
工作流程
-
在本地启动应用程序。
-
使用浏览器子代理导航。
-
根据需要进行交互(点击、滚动、填写表单)。
-
提取 DOM:
document.documentElement.outerHTML[!WARNING] 大型页面可能会被截断。要处理此问题:
- 在提取之前移除
<style>标签:document.querySelectorAll('style').forEach(el => el.remove()) - 静态重新添加样式(Tailwind CDN 链接、源 CSS)
- 在提取之前移除
-
保存到文件。
附录:静态回退(MockPage.jsx)
[!NOTE] 此方法是最后手段,适用于应用程序无法在本地运行时(依赖损坏、缺少后端、无法绕过的身份验证墙)。它需要手动将 React 组件扁平化为一个 JSX 文件。尽可能优先使用策略 A。
何时使用
- 应用程序完全无法在本地运行
- 页面需要身份验证且无法模拟/绕过
- 你需要一个无法通过导航到达的特定 UI 状态(错误屏幕、空状态)
快速参考
npx tsx <SKILL_DIR>/scripts/extract_inline_html.ts \
--index-css src/css/App.css \
--extra-css index.html \
--outdir .stitch \
--page src/MockPage.jsx:Page.html:"Page Title"
关键标志:--no-tailwind(非 Tailwind 应用程序)、--html-class dark(暗模式)、--css-files(额外的 CSS 文件)。
自动检测:Tailwind 配置会被自动检测。@apply 指令会自动使用 <style type="text/tailwindcss">。
MockPage.jsx 规则
- 包含完整布局——页眉、侧边栏、页脚(先读取
App.js) - 扁平化所有条件语句——选择一个状态,移除所有三元运算符和
&&守卫 - 硬编码所有数据——将
{variable}替换为具体值,展开.map()循环 - 保留 Logo——使用带有本地路径的
<img>(后处理会将其内联) - 移除浮动元素——Cookie 横幅、聊天窗口小部件、反馈按钮
后处理
内联本地图片:
npx tsx <SKILL_DIR>/scripts/post_process.ts \
.stitch/Page.html --base-dir <app-directory>