This commit is contained in:
1 parent
c1541b80df
commit
b4c7908029
97 files changed
+12403
-154
No files matched your search
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* 通过 Chrome DevTools Protocol 测量页面在移动端视口下的横向溢出。
|
||||
*
|
||||
* 用法:node scripts/measure-overflow.mjs <url> <width> <height>
|
||||
*
|
||||
* 之所以单独写这个脚本:横向溢出是纯视觉问题,单看截图难以定位到
|
||||
* 具体是哪个元素撑破了布局。这里直接问浏览器要每个元素的 boundingRect。
|
||||
*/
|
||||
|
||||
const [, , targetUrl, widthArg = "390", heightArg = "900"] = process.argv;
|
||||
const width = Number(widthArg);
|
||||
const height = Number(heightArg);
|
||||
|
||||
const wsUrl = process.env.CDP_WS;
|
||||
if (!wsUrl) {
|
||||
console.error("缺少 CDP_WS 环境变量(WebSocket 调试地址)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const socket = new WebSocket(wsUrl);
|
||||
let messageId = 0;
|
||||
const pending = new Map();
|
||||
|
||||
/** 发送 CDP 命令并等待响应 */
|
||||
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}`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
/** 在页面上下文中执行表达式并返回其值 */
|
||||
async function evaluate(expression) {
|
||||
const result = await send("Runtime.evaluate", {
|
||||
expression,
|
||||
returnByValue: true,
|
||||
awaitPromise: true,
|
||||
});
|
||||
|
||||
if (result.exceptionDetails) {
|
||||
throw new Error(
|
||||
`页面执行异常: ${result.exceptionDetails.text} ${
|
||||
result.exceptionDetails.exception?.description ?? ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result.result.value;
|
||||
}
|
||||
|
||||
socket.addEventListener("message", (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.id && pending.has(message.id)) {
|
||||
const { resolve, reject } = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
if (message.error) {
|
||||
reject(new Error(message.error.message));
|
||||
} else {
|
||||
resolve(message.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("error", (error) => {
|
||||
console.error("WebSocket 错误:", error.message ?? error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
socket.addEventListener("open", async () => {
|
||||
try {
|
||||
await send("Page.enable");
|
||||
await send("Runtime.enable");
|
||||
await send("Emulation.setDeviceMetricsOverride", {
|
||||
width,
|
||||
height,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: true,
|
||||
});
|
||||
|
||||
await send("Page.navigate", { url: targetUrl });
|
||||
// 等待网络空闲 + 首屏渲染完成
|
||||
await new Promise((resolve) => setTimeout(resolve, 4000));
|
||||
|
||||
const report = await evaluate(`(() => {
|
||||
const doc = document.documentElement;
|
||||
const clientWidth = doc.clientWidth;
|
||||
const offenders = [];
|
||||
|
||||
for (const el of document.querySelectorAll("body *")) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 && rect.height === 0) continue;
|
||||
|
||||
const overflowRight = rect.right - clientWidth;
|
||||
const overflowLeft = -rect.left;
|
||||
|
||||
if (overflowRight > 1 || overflowLeft > 1) {
|
||||
offenders.push({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
cls: String(el.className || "").slice(0, 90),
|
||||
text: (el.textContent || "").trim().slice(0, 34),
|
||||
left: Math.round(rect.left),
|
||||
right: Math.round(rect.right),
|
||||
width: Math.round(rect.width),
|
||||
overflowRight: Math.round(overflowRight),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 只保留"最外层"的溢出元素,避免父子的重复报告
|
||||
const minimal = offenders.filter((candidate) => {
|
||||
return !offenders.some(
|
||||
(other) =>
|
||||
other !== candidate &&
|
||||
other.left <= candidate.left &&
|
||||
other.right >= candidate.right &&
|
||||
other.width > candidate.width,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
clientWidth,
|
||||
scrollWidth: doc.scrollWidth,
|
||||
documentOverflow: doc.scrollWidth - clientWidth,
|
||||
culpritCount: minimal.length,
|
||||
culprits: minimal.slice(0, 12),
|
||||
};
|
||||
})()`);
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
socket.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("测量失败:", error.message);
|
||||
socket.close();
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user