用Playwright扫描渲染问题

17 March 2026
IMG-THUMBNAIL
如何扫描整个站点,查找渲染问题?

保存下面的脚本,用命令node scan.js运行即可。

const { chromium, devices } = require('playwright');
const https = require('https');
const fs = require('fs');
const path = require('path');

// --- 配置区域 ---
const CONFIG = {
  // 两种模式自动切换:
  // 1. 仅填1个sitemap.xml地址 → 自动解析sitemap
  // 2. 填多个URL/非sitemap地址 → 直接使用配置的URL列表
  urls: [
    // 示例1: sitemap模式 (仅保留这一行)
    'https://www.cyeam.com/sitemap.xml',
    
    // 示例2: 手动URL模式 (保留多个URL)
    // 'https://www.cyeam.com/',
    // 'https://www.cyeam.com/tool/pixel',
    // 'https://www.cyeam.com/baby/chaizi/onegrade2nd',
    // 'https://www.cyeam.com/geek',
    // 'https://blog.cyeam.com/css/2026/03/16/overflow'
  ],
  outputDir: './scan-results',
  timeout: 30000,
  headless: true,
  // 定义要测试的设备列表
  devicesToTest: [
    { name: 'Desktop-1080p', viewport: { width: 1920, height: 1080 } },
    { name: 'Mobile-iPhone-15', preset: 'iPhone 15' }
  ],
  maxSitemapUrls: 50 // sitemap模式下最多解析的URL数量(可调整)
};

// --- 辅助函数:原生解析Sitemap ---
const fetchSitemapUrls = (sitemapUrl) => {
  return new Promise((resolve, reject) => {
    https.get(sitemapUrl, (res) => {
      let xml = '';
      res.on('data', (chunk) => xml += chunk);
      res.on('end', () => {
        const urlRegex = /<loc>(.*?)<\/loc>/g;
        const urls = [];
        let match;
        while ((match = urlRegex.exec(xml)) !== null) {
          urls.push(match[1]);
        }
        // 过滤掉非当前域名的URL,只保留同域名
        const baseDomain = new URL(sitemapUrl).hostname;
        const filteredUrls = urls.filter(url => {
          try {
            return new URL(url).hostname === baseDomain;
          } catch (e) {
            return false;
          }
        });
        // 限制最大数量
        resolve(filteredUrls.slice(0, CONFIG.maxSitemapUrls));
      });
    }).on('error', reject);
  });
};

// --- 辅助函数:初始化目录 ---
const initDirs = (deviceName) => {
  const dir = path.join(CONFIG.outputDir, deviceName);
  const shotDir = path.join(dir, 'screenshots');
  if (!fs.existsSync(shotDir)) fs.mkdirSync(shotDir, { recursive: true });
  return { dir, shotDir };
};

// --- 核心逻辑 ---
(async () => {
  try {
    let urls = [];
    let isSitemapMode = false;

    // 自动判断模式:仅1个URL且是sitemap.xml → sitemap模式
    if (CONFIG.urls.length === 1 && CONFIG.urls[0].toLowerCase().includes('sitemap.xml')) {
      isSitemapMode = true;
      const sitemapUrl = CONFIG.urls[0];
      console.log(`🔍 检测到sitemap模式,正在解析: ${sitemapUrl}`);
      urls = await fetchSitemapUrls(sitemapUrl);
      console.log(`✅ 从sitemap解析出 ${urls.length} 个URL\n`);
    } else {
      // 手动URL模式
      urls = CONFIG.urls;
      console.log(`📋 使用手动配置的URL列表,共 ${urls.length} 个URL\n`);
    }

    if (urls.length === 0) {
      console.log('❌ 未找到可检测的URL,请检查配置!');
      return;
    }

    // 遍历测试设备
    for (const deviceConfig of CONFIG.devicesToTest) {
      console.log(`\n========== 正在测试设备: ${deviceConfig.name} ==========`);
      const { dir, shotDir } = initDirs(deviceConfig.name);
      const results = [];

      // 启动浏览器
      const browser = await chromium.launch({ headless: CONFIG.headless });
      
      let context;
      if (deviceConfig.preset) {
        const device = devices[deviceConfig.preset];
        context = await browser.newContext({ ...device });
      } else {
        context = await browser.newContext({ viewport: deviceConfig.viewport });
      }

      const page = await context.newPage();

      // 遍历每个URL检测
      for (const url of urls) {
        const pageResult = { url, errors: [], screenshot: null };
        const safeFilename = url.replace(/[^a-zA-Z0-9]/g, '_');

        try {
          // 监听JS错误
          page.on('pageerror', err => pageResult.errors.push(`[JS] ${err.message}`));
          
          // 访问页面
          await page.goto(url, { waitUntil: 'networkidle', timeout: CONFIG.timeout });

          // 检查1: 横向滚动条(布局溢出)
          const hasHorizontalScroll = await page.evaluate(() => {
            return document.documentElement.scrollWidth > document.documentElement.clientWidth;
          });
          if (hasHorizontalScroll) {
            pageResult.errors.push('[Layout] 检测到横向滚动条,内容溢出屏幕');
          }

          // 检查2: 裂图检测(跳过大图预览的viewer图片)
          const brokenImages = await page.evaluate(() => {
            const imgs = Array.from(document.images);
            return imgs
              .filter(img => !img.alt.includes('大图预览')) // 跳过viewer图片
              .filter(img => img.naturalWidth === 0) // 筛选裂图
              .map(img => {
                // 生成XPath定位
                const getXPath = (element) => {
                  if (element.id !== '') return `//*[@id="${element.id}"]`;
                  if (element === document.body) return '/html/body';
                  
                  let ix = 0;
                  const siblings = element.parentNode.childNodes;
                  for (let i = 0; i < siblings.length; i++) {
                    const sibling = siblings[i];
                    if (sibling === element) return `${getXPath(element.parentNode)}/${element.tagName.toLowerCase()}[${ix + 1}]`;
                    if (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++;
                  }
                  return getXPath(element.parentNode);
                };

                return {
                  src: img.src,
                  alt: img.alt,
                  html: img.outerHTML,
                  xpath: getXPath(img)
                };
              });
          });

          // 记录裂图详情
          if (brokenImages.length > 0) {
            for (const brokenImg of brokenImages) {
              const errMsg = `[Layout] 裂图详情:
                - 链接: ${brokenImg.src}
                - Alt文本: ${brokenImg.alt || '无'}
                - HTML代码: ${brokenImg.html}
                - XPath路径: ${brokenImg.xpath}`;
              pageResult.errors.push(errMsg);
            }
          }

          // 截图保存
          const screenshotPath = path.join(deviceConfig.name, 'screenshots', `${safeFilename}.png`);
          await page.screenshot({ path: path.join(CONFIG.outputDir, screenshotPath), fullPage: true });
          pageResult.screenshot = screenshotPath;

        } catch (e) {
          pageResult.errors.push(`[Critical] ${e.message}`);
        }

        results.push(pageResult);
        
        // 控制台输出结果(带错误明细)
        if (pageResult.errors.length > 0) {
          console.log(`❌ ${url}`);
          pageResult.errors.forEach(err => console.log(`   - ${err}`));
        } else {
          console.log(`✅ ${url}`);
        }
      }

      // 保存检测报告
      fs.writeFileSync(path.join(dir, 'report.json'), JSON.stringify(results, null, 2));
      await browser.close();
    }

    console.log(`\n🎉 扫描完成!结果已保存至: ${path.resolve(CONFIG.outputDir)}`);
  } catch (error) {
    console.error('💥 运行出错:', error);
  }
})();

原文链接: 用Playwright扫描渲染问题 ,转载请注明来源!

EOF