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

1. 页面白屏如何排查?

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

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

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

一、基础检测流程

1. 控制台检查(Console)

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

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

2. 网络请求检查(Network)

  • 关键指标

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

ini 复制代码
// 检查关键资源是否加载完成
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渲染检测

scss 复制代码
// 检测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应用检测:
javascript 复制代码
// 在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应用检测:
javascript 复制代码
// 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. 长任务检测

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

2. 关键渲染路径监控

ini 复制代码
// 使用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检测脚本

javascript 复制代码
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)

javascript 复制代码
// 使用浏览器的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. 资源加载失败

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

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

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

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

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

2.前端埋点

一、页面生命周期埋点

1. 页面加载阶段

javascript 复制代码
// 记录页面开始加载时间
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. 用户交互阶段

javascript 复制代码
// 点击事件埋点(支持事件委托)
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. 页面停留时长计算

javascript 复制代码
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. 页面隐藏/显示

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

2. 页面关闭前上报

javascript 复制代码
// 确保页面关闭前数据上报
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. 批量上报机制

ini 复制代码
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. 关键指标计算

php 复制代码
// 计算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通信过程中页面跳转,请求可能会被取消。但使用图片进行埋点上报则不会遇到这个问题,特别是在记录离开页面打点行为的时候会很有用。

相关推荐
文刀竹肃7 分钟前
DVWA -SQL Injection-通关教程-完结
前端·数据库·sql·安全·网络安全·oracle
LYFlied12 分钟前
【每日算法】LeetCode 84. 柱状图中最大的矩形
前端·算法·leetcode·面试·职场和发展
Bigger14 分钟前
Tauri(21)——窗口缩放后的”失焦惊魂”,游戏控制权丢失了
前端·macos·app
Bigger33 分钟前
Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?
前端·macos·app
bug总结34 分钟前
前端开发中为什么要使用 URL().origin 提取接口根地址
开发语言·前端·javascript·vue.js·html
zwjapple1 小时前
全栈开发面试高频算法题
算法·面试·职场和发展
程序员爱钓鱼1 小时前
Node.js 编程实战:数据库连接池与性能优化
javascript·后端·node.js
程序员爱钓鱼1 小时前
Node.js 编程实战:Redis缓存与消息队列实践
后端·面试·node.js
Gomiko2 小时前
JavaScript DOM 原生部分(二):元素内容修改
开发语言·javascript·ecmascript
一招定胜负2 小时前
网络爬虫(第三部)
前端·javascript·爬虫