静态复刻需要登录的 Web 后台
核心原则(最重要的一条)
复刻 = 保留原站构建产物 + 接管数据层,不是照着截图重写。
自己重新实现 = 主动引入偏差:丢真实栅格比例、原始 class 名、业务边界逻辑、
被注释掉但可能有业务含义的分支。而这些恰恰是"像不像"的关键。
动手写任何代码之前,先试 xxx.js.map 能不能下。
压缩代码只告诉你"有哪些字段",source map 里的原始源码才告诉你"怎么组织的"。
前置:必须向用户索取
网址、账号、密码。 三者缺一不可(少密码则无法抓真实接口数据)。
缺任何一项就用 AskUserQuestion 问,不要猜、不要用默认值糊过去。
同时确认:目标站点是否只能内网访问(决定能否用 Playwright)。
与 website-offline-mirror 的区别
两个技能都是"复刻网站",但适用场景不同,别用错:
site-clone-static(本技能) |
website-offline-mirror |
|
|---|---|---|
| 目标 | 需要登录的后台系统(SPA) | 任意公开网站 |
| 是否要账号密码 | 必须提供 | 不需要 |
| 手段 | 保留原站构建产物 + mock 服务接管接口 | 遍历链接整站下载 + 路径改写 |
| 断网要求 | 需起本地 mock 服务 | 双击即开、完全断网 |
| 适合 | 内网管理后台、ERP、输液监测、监控大屏 | 文档站、课程站、公开门户 |
判断依据:页面要登录才能进 → 用本技能;公开可访问 → 用 website-offline-mirror。
六步流程
Step 0 — 环境自检(别跳过,能省几十分钟)
装依赖(在本技能目录下执行一次即可,所有脚本共用):
cd <skill_dir>/site-clone-static
npm i # 安装 playwright-core + pngjs
playwright-core 只提供 API,不带浏览器。浏览器按以下顺序自动探测
(实现见 scripts/_chrome.js,不再硬编码任何路径):
--chrome 参数 > PLAYWRIGHT_CHROMIUM_EXECUTABLE > BROWSER_EXE
> chromium.executablePath() > 扫描 ms-playwright 目录 > 系统 Chrome/Edge
全部失败时会打印已尝试过的候选列表,再给四条解决办法。
本机已有 Chromium 就不要下载:
ls ~/AppData/Local/ms-playwright/chromium-*/chrome-win*/chrome.exe
没有才装:npx playwright install chromium
agent-browser install 不要用 —— 从 Google 服务器下 198MB,实测连续超时。
Step 1 — 勘察(决定策略)
node scripts/01_discover.js --url <网址> --out ./probe
读 probe/report.json 的 verdict:
| strategy | 含义 | 走法 |
|---|---|---|
REUSE_ORIGINAL_SOURCE |
有 source map + baseURL 多为相对路径 | Step 2~6 |
REBUILD |
无 source map 或全是绝对路径 | 才考虑重写,且先问用户是否接受 |
Step 2 — 下载构建产物
node scripts/02_fetch_build.js --url <网址> --out ./clone
自动下 entry / js / css / chunk / map / map 里引用的图片字体,
并逐个 md5 校验与线上字节一致。这是"没改过原站代码"的证据。
Step 3 — 提取原始源码(可选但强烈建议)
node scripts/06_extract_source.js --dir ./clone --out ./_src
用于二次开发,以及查清业务逻辑(哪个参数按病区存、哪些是全局)。
Step 4 — 抓真实接口数据
node scripts/03_capture_api.js --url <网址> --user <账号> --pass <密码> \
--out ./clone --routes-file routes.txt
routes.txt 每行一条 hash 路由(没有就先留空,脚本会尝试点菜单)。
登录失败会直接 exit 2,不会继续跑。常见原因:有验证码/OTP(加 --headful 1 手动过一次)、
账号密码错、要求滑块二次认证。截图留在 _capture_after_login.png 可核对。
responses.json 的 key 格式是 "<METHOD> <path><?query>" —— 带 method 和 query,
不同请求不会互相覆盖。mock/server.js 按"精确 → method+path → 跨 method"四级回退查找,
同时兼容旧版只存 pathname 的格式。
Step 5 — 装配 mock 服务
cp assets/mock-server.js ./clone/mock/server.js
改 API_PREFIX 只有一处,就是 mock/server.js 里那一行:
const API_PREFIX = (process.env.API_PREFIX || '<你的前缀>').replace(/\/+$/, '');
完整改法、常见症状对照见 references/api-prefix.md。改完跑 07_precheck.js 验证。
cd ./clone && PORT=8910 node mock/server.js
> ★ 未匹配到规则的接口现在返回 HTTP 501 + {matched:false},不再伪装成
> {success:true, data:null}。这是有意为之:伪装成成功会让页面把"缺接口"
> 渲染成"空表格",看起来像复刻成功,实际是数据层根本没接上。
> 看到 501 就去补 RULES 或重跑 03_capture_api.js。
补齐缺失资源(静默降级,只能这样发现):
node scripts/04_fill_missing_assets.js \
--url http://127.0.0.1:8910/index.html \
--online <原站基址> --dir ./clone \
--routes-file routes.txt --user <账号> --pass <密码>
Step 6 — 验证还原度
node scripts/05_verify_pixdiff.js \
--online <原站index> --mine http://127.0.0.1:8910/index.html \
--user <账号> --pass <密码> --routes-file routes.txt --out ./_diff
像素级 diff 是唯一硬标准。 判定:
| 判定 | 含义 |
|---|---|
SIZE_MISMATCH |
两张截图宽高不同 —— 不参与百分比、不判等同,必须先修 |
IDENTICAL |
差异 < 5% |
CLOSE |
差异 < 20% |
DIFF |
差异 < 50% |
FAR |
差异 ≥ 50% |
> ★ 为什么 SIZE_MISMATCH 要单独成级:早期版本只比 Math.min(宽高) 的重叠区域,
> 原站 1680×1200、复刻版 1680×400 时下半截根本没进比对,却可能算出个位数百分比
> 判成"等同"。假等同是最难发现的一类缺陷。 现在尺寸不同直接判 SIZE_MISMATCH,
> 并在退出码里体现(非 0),不给出任何"等同"结论。
pngjs 是本步的硬依赖,缺失时脚本 exit 1,不会给出"看起来跑过了"的结果。
Step 7 — 交付前自检(必做,否则用户会来报"点不进去")
node scripts/07_precheck.js --dir ./clone --port 8930 --user <账号> --pass <密码>
自动查 7 类问题:.bat 编码、目录结构、API_PREFIX 一致性、服务能否启动、
关键字体是否缺失、未匹配接口是否明确失败、登录接口是否返回可解析 JSON、
浏览器实测能否真的登录进去。
有 FAIL 项不许交付。
四个必踩的坑
0. ★ 页面能渲染 ≠ 服务在跑(最容易误判为"还原度不行")
原站的 index.html 是纯静态文件,不依赖任何服务也能完整渲染出登录页。
但接口是相对路径,服务没起时无人应答,前端路由守卫发现拿不到 token
就会把用户弹回登录页 —— 表现是"登录页正常显示但点登录没反应"。
真实案例:清理测试目录时把占用端口的 node 进程一起杀了,交付后用户报"点不进去"。
排查三条命令:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8910/index.html # 502/000 = 代理或无服务
# PowerShell 查进程(Git Bash 的 pkill 杀不掉 node)
Get-CimInstance Win32_Process -Filter "Name='node.exe'" |
Where-Object { $_.CommandLine -like '*mock*server*' } | Select-Object ProcessId
必须用 Playwright 实测登录,不能只看页面能否渲染。 07_precheck.js 会做这件事。
1. 登录接口发的是 x-www-form-urlencoded,不是 JSON
只解析 JSON 会导致 body.username 为 undefined,表现为"明明填了账号却提示请输入用户名"。
axios 深层序列化还会产生 a[b]=c / a[0][b]=c,需用 URLSearchParams + 正则还原嵌套。
assets/mock-server.js 的 parseBody() 已实现。
2. 页面里 res.data.forEach(v => v.xxx) 会因 mock 结构不对而崩
报错 Cannot read properties of undefined,表现为页面上弹一条红字,
很容易误判成"还原度不行"。mock 必须返回 {data:[{字段齐全}]}。
还有:原站返回 500 的接口会被 axios 拦截器弹错误条 —— mock 要改成正常结构。
3. 静态资源缺失是静默降级
不报错,但图标变方块、背景图消失、字体掉回系统默认。
Element UI 的 all.min.css 有 4000+ 个 .el- class,缺一个 woff 就全站图标崩。
只能靠 Step 5 的脚本探测"本地文件是否存在"。
4. .bat 里写中文会让 cmd 把命令行拆成碎片
cmd.exe 按系统 ANSI(中文是 GBK)读 .bat,UTF-8 中文变乱码后会把命令行截断。
实测症状是 PowerShell 里收到一堆垃圾参数:'-d'、'^'、'(B)',
报错完全指向不了真正的问题(看起来像 Node 脚本写错了)。
规则:.bat 保持纯 ASCII,所有中文提示交给 node mock/server.js 输出。
07_precheck.js 会扫非 ASCII 字节并判 FAIL。
> ⚠ SkillHub 平台禁止上传 .bat 文件(2026-10-08 实测 400 不允许的文件类型: assets/start-server.bat),
> 所以启动脚本要现场按下面模板生成,不要试图当资源文件打包。
交付时创建 启动.bat,内容严格照抄(逐字节纯 ASCII,不要加中文):
@echo off
REM Static Clone - mock server launcher.
REM IMPORTANT: keep this file ASCII-only. cmd.exe reads .bat using
REM the system ANSI codepage (GBK on zh-CN); UTF-8 Chinese turns
REM into mojibake and splits the command line into garbage args.
REM All Chinese messages are printed by mock\server.js instead.
cd /d "%~dp0"
where node >nul 2>nul
if errorlevel 1 goto :nonode
title Static Clone - mock server
node mock\server.js
goto :done
:nonode
echo.
echo [ERROR] Node.js not found in PATH.
echo Install Node.js, or use: python -m http.server 8910
echo NOTE: the Python fallback serves static files only; login will not work.
echo.
pause
:done
echo.
echo Server stopped. Login will not work until restarted.
pause
验收标准
- [ ]
07_precheck.js无 FAIL 项(含浏览器实测登录通过) - [ ] 关键文件(js/css/index.html)md5 与线上一致
- [ ] 逐页截图,像素 diff 报告已生成
- [ ] 大部分页面 ≤5%,差异大的已逐个解释清楚
- [ ] 控制台错误数与原站持平(原站本身可能就有错,别追求 0)
- [ ]
.bat为纯 ASCII(07_precheck.js会验) - [ ] 交付中文名说明文档,开头写清"为什么点不进去"(服务型交付的头号困惑)
- [ ] 交付后保持 mock 服务在跑,并明确告诉用户访问地址
环境注意事项
| 问题 | 应对 |
|---|---|
Git Bash 把 argv 里的 /Home/x 转成 D:/workbudy/... |
路由列表写进文件读,不走命令行参数 |
SPA 切页用 page.goto(url+'#'+r) 会丢登录态 |
用 page.evaluate(h => location.hash = h) |
| Windows 8900 端口报 EACCES | 换 8901+ |
Git Bash 的 pkill 杀不掉 node |
PowerShell Get-CimInstance Win32_Process \| Where CommandLine -like '*mock*' \| Stop-Process |
mock 的 ROOT 用 __dirname 会导致静态资源全 404 |
用 path.resolve(__dirname, '..') |
| 列表接口只抓到 1 页 | 同一路径保留最长的 body;翻页后重抓 |
交付物结构
<站点名>/
├── 启动.bat ← 按坑位 4 的模板现场生成,纯 ASCII
├── 使用说明.md 开头必须有"为什么点不进去"章节
├── index.html 原站原件
├── static/ 原站原件
└── mock/
├── server.js 复制 assets/mock-server.js
└── responses.json 真实接口响应
source map 建议移出交付目录(通常占 70% 体积),单独归档。
不要放 .bat 之外的脚本类文件(如 .sh/.ps1) —— 平台按扩展名白名单收文件,
多余类型会被 400 不允许的文件类型 拒收。
详细参考
references/troubleshooting.md—— 故障排查手册references/case-study.md—— 完整实战案例(Vue2+Element UI,15 页,13 页等同)