提取靜態 HTML
從任何網頁應用程式中提取一個自包含的靜態 HTML 檔案。
選擇策略
在繼續之前,你必須要求使用者選擇要使用的策略。清楚呈現選項,推薦策略 A 作為預設首選,並為每個選項提供簡短的優缺點摘要,以協助使用者做出明智的決定。
| 策略 A (Puppeteer) | 策略 B (瀏覽器子代理程式) | |
|---|---|---|
| 適用時機 | 應用程式在本機運行,無需驗證 | 需要先與頁面互動(點擊、填寫表單) |
| 保真度 | 最高 — 計算樣式已解析 | 高 — 渲染後的 DOM |
| 設定 | 零 — 無需 Mock | 零 — 無需 Mock |
| 框架 | 任何 | 任何 |
| 輸出 | 寫入檔案 — 無大小限制 | 在代理人上下文中可能截斷 |
[!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> 元素的 class(例如 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 捕捉 CSSOM(步驟 0),並且相同來源的圖標字型(@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。
何時使用
- 應用程式完全無法在本機運行
- 頁面需要驗證,且沒有 Mock/繞過方法
- 你需要一個無法透過導航達到的特定 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()迴圈 - 保留標誌 — 使用帶有本地路徑的
<img>(後處理會將它們內嵌) - 移除浮動元素 — Cookie 橫幅、聊天小工具、意見回饋按鈕
後處理
內嵌本地圖片:
npx tsx <SKILL_DIR>/scripts/post_process.ts \
.stitch/Page.html --base-dir <app-directory>