87 lines
2.6 KiB
JavaScript
87 lines
2.6 KiB
JavaScript
/**
|
||
* 通过 CDP 在精确的视口尺寸下截图(含整页截图)。
|
||
*
|
||
* 为什么不直接用 `chrome --headless --screenshot --window-size`:
|
||
* 该模式下截图位图宽度与 CSS 视口宽度可能不一致,导致移动端截图
|
||
* 看起来"内容被截断",实际测量(document.scrollWidth)却是正常的。
|
||
* 这里用 Emulation.setDeviceMetricsOverride 明确指定视口,
|
||
* 保证截图与真实设备表现一致。
|
||
*
|
||
* 用法:CDP_WS=<ws地址> node scripts/screenshot.mjs <url> <width> <height> <输出路径> [--full]
|
||
*/
|
||
|
||
import { writeFileSync } from "node:fs";
|
||
|
||
const [, , targetUrl, widthArg, heightArg, outputPath, ...flags] = process.argv;
|
||
const width = Number(widthArg ?? 1440);
|
||
const height = Number(heightArg ?? 900);
|
||
const fullPage = flags.includes("--full");
|
||
|
||
const wsUrl = process.env.CDP_WS;
|
||
if (!wsUrl || !targetUrl || !outputPath) {
|
||
console.error(
|
||
"用法:CDP_WS=<ws地址> node scripts/screenshot.mjs <url> <width> <height> <输出路径> [--full]",
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
const socket = new WebSocket(wsUrl);
|
||
let messageId = 0;
|
||
const pending = new Map();
|
||
|
||
function send(method, params = {}) {
|
||
const id = ++messageId;
|
||
socket.send(JSON.stringify({ id, method, params }));
|
||
return new Promise((resolve, reject) => {
|
||
pending.set(id, { resolve, reject });
|
||
setTimeout(() => {
|
||
if (pending.has(id)) {
|
||
pending.delete(id);
|
||
reject(new Error(`CDP 超时: ${method}`));
|
||
}
|
||
}, 40000);
|
||
});
|
||
}
|
||
|
||
socket.addEventListener("message", (event) => {
|
||
const message = JSON.parse(event.data);
|
||
if (message.id && pending.has(message.id)) {
|
||
const { resolve } = pending.get(message.id);
|
||
pending.delete(message.id);
|
||
resolve(message.result);
|
||
}
|
||
});
|
||
|
||
socket.addEventListener("open", async () => {
|
||
await send("Page.enable");
|
||
await send("Runtime.enable");
|
||
await send("Emulation.setDeviceMetricsOverride", {
|
||
width,
|
||
height,
|
||
deviceScaleFactor: 1,
|
||
mobile: width < 768,
|
||
});
|
||
|
||
await send("Page.navigate", { url: targetUrl });
|
||
// 等待字体加载与首屏动画稳定
|
||
await new Promise((resolve) => setTimeout(resolve, 4500));
|
||
|
||
// 让所有延迟出现的元素(滚动动画)进入最终状态
|
||
await send("Runtime.evaluate", {
|
||
expression:
|
||
"window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, 0);",
|
||
});
|
||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||
|
||
const screenshot = await send("Page.captureScreenshot", {
|
||
format: "png",
|
||
captureBeyondViewport: fullPage,
|
||
optimizeForSpeed: false,
|
||
});
|
||
|
||
writeFileSync(outputPath, Buffer.from(screenshot.data, "base64"));
|
||
console.log(`已保存 ${outputPath} (${width}x${height}${fullPage ? ", 整页" : ""})`);
|
||
socket.close();
|
||
process.exit(0);
|
||
});
|