普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月24日首页

🗣️面试官: 那些常见的前端面试场景问题

2025年11月24日 18:30

1. 页面白屏如何排查?

第一步:快速分类(30秒) "页面白屏主要有五种原因:JavaScript执行错误、资源加载失败、CSS样式问题、接口异常和浏览器兼容性。其中JavaScript错误最常见,特别是SPA应用中的未捕获异常。"

第二步:排查方法(1分钟) "我的排查步骤是:首先查看Console面板的错误信息,这能快速定位JS异常;然后检查Network面板确认资源加载状态;接着用Elements面板验证DOM和样式;移动端问题会用vConsole或真机调试。生产环境结合Sentry等监控系统分析。"

第三步:预防措施(30秒) "预防方面建立错误边界、资源容错机制、统一接口异常处理、兼容性检测,同时搭建监控告警体系。"

一、基础检测流程

1. 控制台检查(Console)

// 主动捕获全局错误(放在入口文件最前面)
window.addEventListener('error', function(event) {
  console.error('全局捕获:', event.error);
  // 可上报到监控系统
});

// 检查console是否有以下类型错误:
// - SyntaxError (语法错误)
// - TypeError (类型错误)
// - ReferenceError (引用错误)
// - 404资源加载失败

2. 网络请求检查(Network)

  • 关键指标

    • HTML文档状态码(200/304/404/500)
    • JS/CSS资源加载状态
    • 接口请求是否阻塞渲染
  • 检测示例

// 检查关键资源是否加载完成
const resourceCheck = () => {
  const entries = performance.getEntriesByType('resource');
  const criticalResources = entries.filter(entry => 
    entry.initiatorType === 'script' || 
    entry.initiatorType === 'css'
  );
  
  criticalResources.forEach(res => {
    if(res.responseStatus >= 400) {
      console.error(`资源加载失败: ${res.name}`, res);
    }
  });
};
window.addEventListener('load', resourceCheck);

二、深度检测方法

1. DOM渲染检测

// 检测DOM树是否正常构建
function checkDOMReady() {
  return new Promise((resolve) => {
    const check = () => {
      if(document.body && document.body.children.length > 0) {
        resolve(true);
      } else {
        setTimeout(check, 50);
      }
    };
    check();
  });
}

// 使用示例
checkDOMReady().then((isReady) => {
  if(!isReady) {
    console.error('DOM渲染超时');
    // 上报白屏信息
  }
});

2. 框架特定检测

Vue应用检测:
// 在main.js中添加
new Vue({
  render: h => h(App),
  errorCaptured(err, vm, info) {
    console.error('Vue组件错误:', err, info);
    // 可上报错误
    return false; // 阻止错误继续向上传播
  }
}).$mount('#app');

// 检查根组件挂载
if(!document.querySelector('#app').__vue__) {
  console.error('Vue根实例挂载失败');
}
React应用检测:
// Error Boundary组件
class ErrorBoundary extends React.Component {
  componentDidCatch(error, info) {
    console.error('React组件错误:', error, info);
    // 上报错误
  }
  
  render() {
    return this.props.children; 
  }
}

// 检查React根组件
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <ErrorBoundary>
    <App />
  </ErrorBoundary>
);

三、性能相关检测

1. 长任务检测

// 检测阻塞渲染的长时间任务
const observer = new PerformanceObserver((list) => {
  for(const entry of list.getEntries()) {
    if(entry.duration > 50) { // 超过50ms的任务
      console.warn('长任务影响渲染:', entry);
    }
  }
});
observer.observe({entryTypes: ["longtask"]});

2. 关键渲染路径监控

// 使用Performance API监控关键时间点
const perfData = window.performance.timing;
const metrics = {
  domReady: perfData.domComplete - perfData.domLoading,
  loadTime: perfData.loadEventEnd - perfData.navigationStart
};

if(metrics.domReady > 3000) {
  console.error('DOM解析时间过长:', metrics);
}

四、自动化检测方案

1. Puppeteer检测脚本

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  // 监听控制台错误
  page.on('console', msg => {
    if(msg.type() === 'error') {
      console.log('页面错误:', msg.text());
    }
  });
  
  // 设置超时检测
  await Promise.race([
    page.goto('https://your-site.com'),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('页面加载超时')), 5000)
    )
  ]);
  
  // 检查可见内容
  const content = await page.evaluate(() => {
    return {
      bodyText: document.body.innerText,
      childCount: document.body.children.length
    };
  });
  
  if(content.childCount === 0 || content.bodyText.length < 10) {
    console.error('检测到白屏现象');
  }
  
  await browser.close();
})();

2. 真实用户监控(RUM)

// 使用浏览器的MutationObserver监控DOM变化
const observer = new MutationObserver((mutations) => {
  if(!document.querySelector('#app')?.innerHTML) {
    // 上报白屏事件
    beacon.send('white-screen', {
      url: location.href,
      ua: navigator.userAgent
    });
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

五、常见白屏场景示例

  1. 资源加载失败

    <!-- 错误的资源路径 -->
    <script src="/wrong-path/app.js"></script>
    
  2. 语法错误

    // 缺少括号导致整个脚本不执行
    function test() {
      console.log('hello'
    }
    
  3. 框架初始化失败

    // Vue示例 - 挂载元素不存在
    new Vue({el: '#not-exist'});
    
  4. CSS阻塞

    <!-- 不正确的CSS引入阻塞渲染 -->
    <link rel="stylesheet" href="nonexist.css">
    
  5. 第三方库冲突

    // 两个库都修改了Array原型
    libraryA.modifyPrototype();
    libraryB.modifyPrototype(); // 冲突导致错误 ```
    

2.前端埋点

一、页面生命周期埋点

1. 页面加载阶段

// 记录页面开始加载时间
const pageStartTime = Date.now();

// 监听页面加载完成
window.addEventListener('load', () => {
  const loadTime = Date.now() - pageStartTime;
  track('page_load', {
    load_time: loadTime,
    referrer: document.referrer,
    resource_status: checkResources()
  });
});

// 检查关键资源加载状态
function checkResources() {
  return performance.getEntriesByType('resource').map(res => ({
    name: res.name,
    type: res.initiatorType,
    duration: res.duration.toFixed(2)
  }));
}

2. 用户交互阶段

// 点击事件埋点(支持事件委托)
document.body.addEventListener('click', (e) => {
  const target = e.target.closest('[data-track]');
  if(target) {
    track('element_click', {
      element_id: target.id,
      track_type: target.dataset.track,
      position: `${e.clientX},${e.clientY}`
    });
  }
});

// 滚动深度记录
let maxScroll = 0;
window.addEventListener('scroll', _.throttle(() => {
  const currentScroll = window.scrollY / document.body.scrollHeight;
  if(currentScroll > maxScroll) {
    maxScroll = currentScroll;
    track('scroll_depth', { depth: Math.round(maxScroll * 100) });
  }
}, 1000));

3. 页面停留时长计算

let activeStart = Date.now();
let inactiveTime = 0;

// 用户活跃状态检测
document.addEventListener('mousemove', resetActiveTimer);
document.addEventListener('keydown', resetActiveTimer);

function resetActiveTimer() {
  if(inactiveTime > 0) {
    track('user_inactive', { duration: inactiveTime });
    inactiveTime = 0;
  }
  activeStart = Date.now();
}

// 每10秒检测一次活跃状态
setInterval(() => {
  if(Date.now() - activeStart > 15000) { // 15秒无操作视为不活跃
    inactiveTime += 10000;
  } else {
    track('user_active', { duration: 10000 });
  }
}, 10000);

二、特殊场景处理

1. 页面隐藏/显示

// 页面可见性变化监听
document.addEventListener('visibilitychange', () => {
  if(document.hidden) {
    track('page_hide', { 
      stay_time: Date.now() - pageStartTime,
      scroll_depth: maxScroll
    });
  } else {
    track('page_show');
  }
});

2. 页面关闭前上报

// 确保页面关闭前数据上报
window.addEventListener('beforeunload', () => {
  const totalStay = Date.now() - pageStartTime;
  navigator.sendBeacon('/api/track', JSON.stringify({
    event: 'page_close',
    active_time: totalStay - inactiveTime,
    scroll_depth: maxScroll
  }));
});

三、数据上报优化方案

1. 批量上报机制

let eventQueue = [];
const MAX_QUEUE = 5;
const FLUSH_INTERVAL = 3000;

function addToQueue(event) {
  eventQueue.push(event);
  if(eventQueue.length >= MAX_QUEUE) {
    flushQueue();
  }
}

function flushQueue() {
  if(eventQueue.length === 0) return;
  
  const batchData = { batch: eventQueue };
  navigator.sendBeacon('/api/batch', JSON.stringify(batchData));
  eventQueue = [];
}

// 定时刷新队列
setInterval(flushQueue, FLUSH_INTERVAL);

2. 关键指标计算

// 计算FMP(首次有效绘制)
new PerformanceObserver((entryList) => {
  const [entry] = entryList.getEntriesByName('first-contentful-paint');
  track('fmp', { value: entry.startTime.toFixed(2) });
}).observe({type: 'paint', buffered: true});

// 计算LCP(最大内容绘制)
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  track('lcp', { value: lastEntry.startTime.toFixed(2) });
}).observe({type: 'largest-contentful-paint', buffered: true});

四、面试回答精简版

"我们实现全链路埋点主要分三个阶段:

  1. 加载阶段

    • performance API采集DNS/TTFB等指标
    • 监听load事件记录完整加载时间
    • 检查关键资源状态(如图片/脚本)
  2. 交互阶段

    • 事件委托监听全局点击(带data-track属性)
    • 节流处理滚动事件计算最大深度
    • 通过mousemove/keydown检测活跃状态
  3. 离开阶段

    • visibilitychange处理页面切换
    • beforeunload+sendBeacon确保关闭前上报
    • 计算总停留时长和有效活跃时间

3.那为什么大家都使用请求 GIF 图片的方式上报埋点数据呢?

  • 防止跨域问题:前端监控的请求常常会遇到跨域问题,这可能会影响监控的准确性和可用性。然而,图片的src属性并不会跨域,因此使用GIF图片作为埋点可以正常发起请求,从而有效避免跨域问题。

  • 防止阻塞页面加载:在创建资源节点后,通常只有当对象注入到浏览器的DOM树后,浏览器才会实际发送资源请求。但反复操作DOM会引发性能问题,且载入js/css资源会阻塞页面渲染,从而影响用户体验。与此不同,构造图片打点不需要插入DOM,只要在js中new出Image对象就能发起请求,这样就不会有阻塞问题。即使在没有js的浏览器环境中,也能通过img标签正常打点,这是其他类型的资源请求所做不到的。

  • 体积小,节约流量:相比其他图片格式(如BMP和PNG),GIF图片具有更小的体积。例如,最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF只需要43个字节。因此,使用GIF作为埋点可以显著节约流量,提高数据传输效率。

  • 浏览器支持性好:所有浏览器都支持Image对象,即使不支持XMLHttpRequest对象也一样。这意味着使用GIF进行埋点上报可以在各种浏览器环境中稳定运行。

  • 记录错误的过程很少出错:某些情况下,如Ajax通信过程中页面跳转,请求可能会被取消。但使用图片进行埋点上报则不会遇到这个问题,特别是在记录离开页面打点行为的时候会很有用。

❌
❌