2025前端面试 —— 应用篇

92 阅读7分钟

一、跨域共享Token实现单点登录

在前端开发中,当你有多个不同源的网站需要共享登录状态时,可以通过以下几种方式实现单点登录(SSO):

1. 使用子域共享Cookie

如果这些网站共享一个顶级域名(如a.example.com和b.example.com),可以:

  • 将Token存储在顶级域名下的Cookie中(设置domain为.example.com
  • 这样所有子域都能访问这个Cookie
// 设置跨子域的Cookie
document.cookie = `token=${token}; domain=.example.com; path=/; secure; samesite=lax`;

2. 使用OAuth 2.0隐式授权流

  1. 用户登录第一个网站(A)后获得Token

  2. 当访问第二个网站(B)时:

    • B检测本地无Token,重定向到认证服务器
    • 认证服务器发现用户已登录(A),直接返回Token给B
    • B使用Token完成登录

3. 使用PostMessage跨域通信

在已登录的网站A:

// 当检测到用户跳转到网站B时
window.open('https://b.example.com');
window.addEventListener('load', () => {
  window.opener.postMessage({ token: 'your_token' }, 'https://b.example.com');
});

在网站B:

window.addEventListener('message', (event) => {
  if (event.origin === 'https://a.example.com') {
    const token = event.data.token;
    // 使用token登录
  }
});

4. 使用中间页面传递Token

  1. 从网站A跳转到一个中间页面(同源的)
  2. 中间页面将Token存储在localStorage或通过URL传递给网站B

5. 后端协助的Token验证

  1. 网站A登录后,将Token发送到后端存储
  2. 访问网站B时,B后端与A后端通信验证用户状态
  3. 如果已登录,B后端生成一个针对B的Token返回给前端

安全注意事项

  1. 始终使用HTTPS传输Token
  2. 考虑Token的过期时间
  3. 实现CSRF保护机制
  4. 对于敏感操作要求重新认证

二、首屏加载速度优化

问题:首页加载时间超过5秒,Lighthouse评分仅45分

解决方案

  1. 图片懒加载与优化
<!-- 使用IntersectionObserver实现懒加载 -->
<img data-src="product.jpg" class="lazyload" />

<script>
document.addEventListener("DOMContentLoaded", () => {
  const lazyImages = [].slice.call(document.querySelectorAll("img.lazyload"));
  
  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach((lazyImage) => {
      lazyImageObserver.observe(lazyImage);
    });
  }
});
</script>
  1. 关键CSS内联
<style>
  /* 提取首屏关键CSS内联 */
  .hero-section, .main-nav { /* ... */ }
</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
  1. 代码分割+预加载
// Vue路由懒加载+预加载
const routes = [
  {
    path: '/product/:id',
    component: () => import(/* webpackPrefetch: true */ './views/Product.vue')
  }
]

效果:首屏加载时间从5s降至1.8s,Lighthouse评分提升至92

三、页面卡顿优化

现象:长时间使用后页面变卡,Chrome任务管理器显示内存持续增长

1. 内存泄露

排查步骤

  1. Chrome DevTools内存快照对比

    • 页面初始状态拍快照
    • 执行疑似泄漏操作后拍快照
    • 比较两个快照的对象分配情况
  2. 发现的问题

// 错误示例:事件监听未清除
mounted() {
  window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
  // 忘记移除监听器
}
  1. 解决方案
// Vue解决方案
mounted() {
  this.resizeObserver = new ResizeObserver(this.handleResize)
  this.resizeObserver.observe(this.$el)
},
beforeDestroy() {
  this.resizeObserver.disconnect()
}

// React解决方案
useEffect(() => {
  const handleResize = () => { /*...*/ }
  window.addEventListener('resize', handleResize)
  
  return () => {
    window.removeEventListener('resize', handleResize)
  }
}, [])

进阶工具

# 使用Chrome的Performance Monitor持续监控
# 使用Heap Snapshots查找分离的DOM树

2. 大型响应式对象

  1. 发现的问题

    • 排查发现开发人员创建了 Cesium 地图核心对象的 Vue 响应式对象,该对象是 Cesium 的核心对象,包含很多属性及对象,Vue 的深度响应式转换需要递归地遍历对象的所有属性,这会消耗较多的时间和内存,导致页面卡顿
  2. 解决方案

    • 使用 shallowRef 进行浅响应式跟踪,Vue 不会对对象内部的属性变化进行响应式处理

四、WebWorker优化长任务

1. 大数据表格处理

问题:万行数据排序导致UI冻结3秒

解决方案

  1. 主线程代码
// 创建WebWorker
const worker = new Worker('sort.worker.js')

// 发送数据到Worker
function sortBigData(data) {
  return new Promise((resolve) => {
    worker.postMessage({ type: 'SORT', data })
    
    worker.onmessage = (e) => {
      if (e.data.type === 'SORT_DONE') {
        resolve(e.data.result)
      }
    }
  })
}

// 使用示例
const bigData = [...Array(100000).keys()].map(x => ({id: x, value: Math.random()}))
sortBigData(bigData).then(sortedData => {
  this.sortedData = sortedData
})
  1. Worker线程代码 (sort.worker.js)
// 复杂的排序算法
function complexSort(arr) {
  return arr.sort((a, b) => {
    // 复杂的多条件排序逻辑
    return a.value - b.value || a.id - b.id
  })
}

self.onmessage = function(e) {
  if (e.data.type === 'SORT') {
    const sorted = complexSort(e.data.data)
    self.postMessage({
      type: 'SORT_DONE',
      result: sorted
    })
  }
}

优化效果:UI保持流畅,排序操作不再阻塞主线程

2. Canvas图像处理

问题:高分辨率图片滤镜处理导致页面卡顿

WebWorker方案

// 主线程
const canvasWorker = new Worker('canvas.worker.js')
const offscreenCanvas = document.querySelector('canvas').transferControlToOffscreen()

canvasWorker.postMessage({
  type: 'PROCESS_IMAGE',
  canvas: offscreenCanvas,
  imageData: imageData
}, [offscreenCanvas])

// Worker线程
onmessage = function(e) {
  if (e.data.type === 'PROCESS_IMAGE') {
    const { canvas, imageData } = e.data
    const ctx = canvas.getContext('2d')
    
    // 耗时的像素处理
    for(let i = 0; i < imageData.data.length; i += 4) {
      // 应用滤镜算法...
    }
    
    ctx.putImageData(imageData, 0, 0)
  }
}

3. 地图要素数据推送处理

问题:接收多个地理要素数据推送时,如轨迹展示等功能,频繁处理实时数据导致页面卡顿

WebWorker方案:使用 webWorker 进行数据处理,提示地图流畅度

注意事项

  1. Worker间通信数据会被复制(结构化克隆算法),大数据考虑Transferable Objects
  2. 避免频繁通信,批量处理数据
  3. Worker中无法访问DOM,但可通过OffscreenCanvas操作Canvas

五、WebSocket 高并发会引发哪些问题及解决方案

连接管理问题

1. 连接数限制

问题表现

  • 浏览器对同一域名有6-8个TCP连接限制
  • 超过限制后新连接被阻塞或失败

解决方案

// 连接池管理
class WSPool {
  constructor(maxConnections = 4) {
    this.pool = new Array(maxConnections).fill(null);
    this.queue = [];
  }

  acquire() {
    return new Promise(resolve => {
      const freeIndex = this.pool.indexOf(null);
      if(freeIndex !== -1) {
        const ws = new WebSocket(url);
        this.pool[freeIndex] = ws;
        resolve(ws);
      } else {
        this.queue.push(resolve);
      }
    });
  }

  release(ws) {
    const index = this.pool.indexOf(ws);
    if(index !== -1) {
      this.pool[index] = null;
      if(this.queue.length) {
        this.acquire().then(this.queue.shift());
      }
    }
  }
}

2. 心跳中断

问题表现

  • 大量连接导致心跳包未能及时发送
  • 被服务器误判为死连接

解决方案

// 自适应心跳
let heartbeatInterval = 30000; // 初始30秒

function setupHeartbeat(ws) {
  const adjustInterval = () => {
    const latency = measureLatency(); // 测量实际网络延迟
    heartbeatInterval = Math.min(
      Math.max(latency * 3, 5000), 
      60000
    );
  };

  const ping = () => {
    if(ws.bufferedAmount > 0) {
      // 网络拥塞时跳过本次心跳
      adjustInterval();
      return;
    }
    ws.send(JSON.stringify({type: 'ping'}));
    adjustInterval();
  };

  return setInterval(ping, heartbeatInterval);
}

消息处理问题

1. 消息风暴

问题表现

  • 短时间内收到大量消息导致UI冻结
  • 主线程被阻塞无法响应用户操作

解决方案

// 基于requestAnimationFrame的节流处理
class MessageScheduler {
  constructor() {
    this.queue = [];
    this.isProcessing = false;
  }

  add(message) {
    this.queue.push(message);
    if(!this.isProcessing) {
      this.isProcessing = true;
      requestAnimationFrame(this.process.bind(this));
    }
  }

  process() {
    const startTime = performance.now();
    let processed = 0;
    
    while(this.queue.length && performance.now() - startTime < 8) {
      // 每帧最多处理8ms
      const msg = this.queue.shift();
      handleMessage(msg);
      processed++;
    }

    if(this.queue.length) {
      requestAnimationFrame(this.process.bind(this));
    } else {
      this.isProcessing = false;
    }
    
    if(processed > 50) {
      adjustThrottleRate(); // 动态调整处理速率
    }
  }
}

2. 乱序问题

问题表现

  • 消息到达顺序与发送顺序不一致
  • 导致UI状态错乱

解决方案

// 基于序列号的排序缓冲区
class MessageSequencer {
  constructor() {
    this.nextSeq = 0;
    this.buffer = new Map();
    this.timer = null;
  }

  receive(msg) {
    if(msg.seq === this.nextSeq) {
      this.deliver(msg);
    } else {
      this.buffer.set(msg.seq, msg);
      this.scheduleDeliver();
    }
  }

  deliver(msg) {
    handleMessage(msg);
    this.nextSeq++;
    
    // 检查缓冲区是否有后续消息
    while(this.buffer.has(this.nextSeq)) {
      const nextMsg = this.buffer.get(this.nextSeq);
      this.buffer.delete(this.nextSeq);
      this.deliver(nextMsg);
    }
  }

  scheduleDeliver() {
    if(!this.timer) {
      this.timer = setTimeout(() => {
        this.timer = null;
        this.flushBuffer();
      }, 100); // 最大等待100ms
    }
  }

  flushBuffer() {
    // 强制处理已缓冲的消息(可能产生跳跃)
    const sorted = Array.from(this.buffer.entries())
      .sort((a,b) => a[0] - b[0]);
    
    sorted.forEach(([seq, msg]) => {
      if(seq >= this.nextSeq) {
        this.deliver(msg);
      }
    });
  }
}

资源消耗问题

1. 内存泄漏

问题表现

  • 未清理的消息缓存导致内存持续增长
  • 长时间运行后标签页崩溃

解决方案

// 自动内存管理
class MessageCache {
  constructor(maxSize = 1000) {
    this.cache = new Map();
    this.accessTimes = new Map();
    this.maxSize = maxSize;
    setInterval(this.cleanup.bind(this), 60000);
  }

  add(id, message) {
    if(this.cache.size >= this.maxSize) {
      this.evictOldest();
    }
    this.cache.set(id, message);
    this.accessTimes.set(id, Date.now());
  }

  get(id) {
    const message = this.cache.get(id);
    if(message) {
      this.accessTimes.set(id, Date.now());
    }
    return message;
  }

  evictOldest() {
    const oldest = Array.from(this.accessTimes.entries())
      .sort((a,b) => a[1] - b[1])[0];
    if(oldest) {
      this.cache.delete(oldest[0]);
      this.accessTimes.delete(oldest[0]);
    }
  }

  cleanup() {
    const cutoff = Date.now() - 3600000; // 1小时未访问
    for(const [id, time] of this.accessTimes) {
      if(time < cutoff) {
        this.cache.delete(id);
        this.accessTimes.delete(id);
      }
    }
  }
}

2. CPU过载

问题表现

  • 消息处理消耗大量CPU资源
  • 移动设备发热严重

解决方案

// Web Worker分流处理
const worker = new Worker('message-processor.js');

// 主线程
websocket.onmessage = (event) => {
  worker.postMessage({
    type: 'raw',
    data: event.data
  });
};

worker.onmessage = (event) => {
  switch(event.data.type) {
    case 'render':
      updateUI(event.data.payload);
      break;
    case 'alert':
      showAlert(event.data.message);
      break;
  }
};

// message-processor.js
self.onmessage = async (event) => {
  if(event.data.type === 'raw') {
    const msg = await parseMessage(event.data.data);
    const result = await processComplexLogic(msg);
    self.postMessage({
      type: 'render',
      payload: result
    });
  }
};

用户体验问题

1. UI闪烁

问题表现

  • 高频更新导致界面不断重绘
  • 用户无法聚焦阅读内容

解决方案

// 视觉稳定技术
class StableRenderer {
  constructor() {
    this.pendingUpdates = new Map();
    this.rafId = null;
  }

  scheduleUpdate(key, html) {
    this.pendingUpdates.set(key, html);
    if(!this.rafId) {
      this.rafId = requestAnimationFrame(() => {
        this.applyUpdates();
        this.rafId = null;
      });
    }
  }

  applyUpdates() {
    // 批量DOM操作
    const fragment = document.createDocumentFragment();
    
    this.pendingUpdates.forEach((html, key) => {
      const element = document.getElementById(key);
      if(element) {
        element.innerHTML = html;
        fragment.appendChild(element.cloneNode(true));
      }
    });
    
    // 使用requestPostAnimationFrame减少闪烁
    requestPostAnimationFrame(() => {
      this.pendingUpdates.forEach((html, key) => {
        const element = document.getElementById(key);
        if(element) {
          element.parentNode.replaceChild(
            fragment.querySelector(`#${key}`), 
            element
          );
        }
      });
      this.pendingUpdates.clear();
    });
  }
}

2. 操作冲突

问题表现

  • 用户输入被实时更新覆盖
  • 表单提交时显示旧数据

解决方案

// 编辑锁机制
class EditLock {
  constructor() {
    this.lockedElements = new WeakMap();
  }

  acquire(element) {
    if(!this.lockedElements.has(element)) {
      const lock = {
        version: 0,
        isLocked: false
      };
      this.lockedElements.set(element, lock);
      return lock;
    }
    return this.lockedElements.get(element);
  }

  checkConflict(element, serverVersion) {
    const lock = this.acquire(element);
    if(lock.isLocked && lock.version !== serverVersion) {
      showConflictDialog({
        local: lock.version,
        remote: serverVersion
      });
      return true;
    }
    return false;
  }
}

// 使用示例
inputElement.addEventListener('focus', () => {
  const lock = editLock.acquire(inputElement);
  lock.isLocked = true;
  lock.version++;
});

websocket.onmessage = (event) => {
  if(editLock.checkConflict(inputElement, event.data.version)) {
    return; // 忽略冲突更新
  }
  // 正常处理消息
};

监控与调优

1. 性能指标采集

const metrics = {
  messageRate: new RollingWindow(60), // 60秒滑动窗口
  processingTime: new Histogram([5, 10, 25, 50, 100])
};

websocket.onmessage = (event) => {
  const start = performance.now();
  
  metrics.messageRate.add(1);
  processMessage(event.data);
  
  const duration = performance.now() - start;
  metrics.processingTime.record(duration);
  
  if(duration > 50) {
    reportLongTask(event.data, duration);
  }
};

setInterval(() => {
  const rate = metrics.messageRate.sum();
  const p95 = metrics.processingTime.percentile(95);
  
  if(rate > 1000 || p95 > 30) {
    adjustThrottleLevel();
  }
}, 10000);

2. 动态策略调整

// 根据网络条件调整策略
function adaptStrategy() {
  const connection = navigator.connection;
  
  if(connection) {
    switch(connection.effectiveType) {
      case '4g':
        setBatchSize(50);
        setThrottleDelay(10);
        break;
      case '3g':
        setBatchSize(20);
        setThrottleDelay(30);
        break;
      case '2g':
        setBatchSize(5);
        setThrottleDelay(100);
        useTextCompression();
        break;
    }
    
    if(connection.saveData) {
      enableDataSaverMode();
    }
  }
}

// 监听网络变化
navigator.connection.addEventListener('change', adaptStrategy);

降级方案

1. WebSocket不可用时的降级

function connectWithFallback() {
  const ws = new WebSocket(url);
  
  ws.onerror = () => {
    if(!this.hasRetried) {
      this.hasRetried = true;
      setTimeout(() => connectWithFallback(), 1000);
    } else {
      startLongPolling();
    }
  };
}

function startLongPolling() {
  let abortController = new AbortController();
  
  async function poll() {
    try {
      const response = await fetch('/poll', {
        signal: abortController.signal
      });
      const messages = await response.json();
      processMessages(messages);
      poll();
    } catch(e) {
      if(e.name !== 'AbortError') {
        setTimeout(poll, 5000);
      }
    }
  }
  
  poll();
  
  // 尝试恢复WebSocket
  setInterval(() => {
    if(navigator.onLine) {
      abortController.abort();
      abortController = new AbortController();
      connectWithFallback();
    }
  }, 30000);
}

六、自动响应的动态数组

现有如下代码:

let b = 1 
const a = [b ,2, 3] 

a.join('.') // 输出 [1, 2, 3] 

b = 2 

a.join('.') // 依然输出 [1, 2, 3] 

b的值更改为2之后,若希望动态输出为[2, 2, 3],且a的相关调用不能更改(如a.join('.')该调用式不可修改),请输出解决反感

1、思路:

  1. 动态获取最新数组:通过 getLatestArray 函数实时生成依赖 b 的最新数组。
  2. Proxy 自动转发:代理对象 a 拦截所有属性访问(如 a.joina[0]),自动转发到最新数组的对应属性 / 方法。
  3. 保持方法上下文:通过 bind(latestArr) 确保数组方法(如 mappush)内部的 this 指向最新数组,避免上下文错乱。

2、解决方案:

let b = 1;

// 定义生成最新数组的函数(动态依赖b)
const getLatestArray = () => [b, 2, 3];

// 创建代理对象,自动关联到最新数组
const a = new Proxy({}, {
  // 拦截属性访问(如 a[0]、a.length)
  get(target, prop) {
    const latestArr = getLatestArray();
    
    // 如果访问的是方法(如 join、map、push)
    if (typeof latestArr[prop] === 'function') {
      // 绑定最新数组作为上下文,确保方法内的this指向正确
      return latestArr[prop].bind(latestArr);
    }
    
    // 否则直接返回最新数组的属性(如索引、length)
    return latestArr[prop];
  },
  
  // 拦截属性设置(可选,如需支持修改数组)
  set(target, prop, value) {
    const latestArr = getLatestArray();
    latestArr[prop] = value;
    return true;
  }
});

// 测试各种数组方法和属性访问
console.log(a.join('.')); // 1.2.3(初始值)
console.log(a.map(x => x * 2)); // [2, 4, 6]
console.log(a.length); // 3
console.log(a[0]); // 1

// 修改b的值
b = 5;

// 自动更新
console.log(a.join('.')); // 5.2.3
console.log(a.map(x => x * 2)); // [10, 4, 6]
console.log(a.length); // 3
console.log(a[0]); // 5

// 支持修改数组(如新增元素)
a.push(4);
console.log(a.join('.')); // 5.2.3.4(修改后的值)