前端真实问题场景130条 - 进阶问题篇 (31-60)

3 阅读23分钟

前端真实问题场景130条 - 进阶问题篇 (31-60)

本文是《前端真实问题场景130条》系列的第2篇,涵盖第31-60条问题。

💡 每篇文章包含:真实用户问题 + 错误思路 + 正确分析 + 最佳解决方案 + 延伸建议


31. 页面无障碍访问

用户问题(真实口语): "有视障用户反馈我们网站用屏幕阅读器无法使用,怎么优化无障碍访问?"

错误思路:

  • 认为无障碍访问不重要
  • 只加aria标签就不管了
  • 认为这是浏览器问题

正确分析: 无障碍访问问题:

  1. 未使用语义化HTML标签
  2. 图片缺少alt属性
  3. 表单元素缺少label
  4. 颜色对比度不足
  5. 键盘导航不支持

最佳解决方案:

  1. 使用语义化HTML:
<nav aria-label="主导航">
  <ul>
    <li><a href="/home">首页</a></li>
  </ul>
</nav>

<main>
  <article>
    <h1>文章标题</h1>
  </article>
</main>
  1. 图片添加alt描述:
<img src="chart.png" alt="2024年销售数据柱状图">
  1. 表单关联label:
<label for="email">邮箱</label>
<input type="email" id="email" name="email">
  1. 支持键盘导航:
const handleKeyDown = (e) => {
  if (e.key === 'Enter') {
" handleSubmit();
  }
};
  1. 使用ARIA属性:
<button aria-label="关闭对话框" aria-expanded="false">
  ×
</button>

延伸建议:

  • 使用axe DevTools进行无障碍测试
  • 遵循WCAG 2.1标准
  • 定期邀请视障用户测试

32. 页面缓存策略

用户问题(真实口语): "用户刷新页面还是显示旧内容,怎么让用户看到最新数据?"

错误思路:

  • 让用户强制刷新(Ctrl+F5)
  • 在URL加随机参数
  • 禁用所有缓存

正确分析: 缓存策略问题:

  1. 未配置合理的缓存策略
  2. 静态资源缓存时间过长
  3. HTML文件未设置不缓存
  4. 未使用版本号或hash

最佳解决方案:

  1. 配置webpack文件名hash:
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].js'
}
  1. 配置服务器缓存头:
# HTML文件不缓存
location ~* \.html$ {
  add_header Cache-Control "no-cache, no-store, must-revalidate";
}

# 静态资源长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}
  1. 使用Service Worker缓存策略:
workbox.routing.registerRoute(
  /\.(?:png|jpg|jpeg|svg|gif)$/,
  new workbox.strategies.CacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60
      })
    ]
  })
);

延伸建议:

  • 使用ETag进行缓存验证
  • 实现PWA离线访问
  • 提供清除缓存的功能

33. 页面动画性能

用户问题(真实口语): "页面动画好卡,滚动的时候掉帧,怎么优化动画性能?"

错误思路:

  • 认为是电脑性能问题
  • 减少动画时长
  • 使用CSS动画就不管了

正确分析: 动画性能问题:

  1. 使用了会触发布局重排的属性(width、height等)
  2. 未使用transform和opacity
  3. 动画元素过多
  4. 未使用requestAnimationFrame
  5. 未开启GPU加速

最佳解决方案:

  1. 使用transform和opacity:
/* 错误 */
.element {
  transition: left 0.3s, width 0.3s;
}

/* 正确 */
.element {
  transform: translateX(100px);
  transition: transform 0.3s;
  will-change: transform;
}
  1. 使用requestAnimationFrame:
const animate = () => {
  element.style.transform = `translateX(${x}px)`;
  requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
  1. 开启GPU加速:
.gpu-accelerated {
  transform: translateZ(0);
  backface-visibility: hidden;
}
  1. 使用FLIP动画技术:
const first = element.getBoundingClientRect();
// 修改元素
const last = element.getBoundingClientRect();
const delta = {
  x: first.left - last.left,
  y: first.top - last.top
};
element.style.transform = `translate(${delta.x}px, ${delta.y}px)`;

延伸建议:

  • 使用Framer Motion等动画库
  • 使用Chrome DevTools的Performance面板分析
  • 减少动画元素数量

34. 页面深色模式

用户问题(真实口语): "用户想要深色模式,怎么实现主题切换?"

错误思路:

  • 做两套页面
  • 只改背景色,不改文字颜色
  • 使用!important覆盖样式

正确分析: 深色模式实现问题:

  1. 未使用CSS变量
  2. 颜色对比度不足
  3. 未保存用户偏好
  4. 切换时页面闪烁

最佳解决方案:

  1. 使用CSS变量:
:root {
  --bg-color: #ffffff;
  --text-color: #333333;
  --primary-color: #1890ff;
}

[data-theme='dark'] {
  --bg-color: #1a1a1a;
  --text-color: #e0e0e0;
  --primary-color: #177dd;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
}
  1. 切换主题:
const toggleTheme = () => {
  const currentTheme = document.documentElement.getAttribute('data-theme');
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', newTheme);
  localStorage.setItem('theme', newTheme);
};
  1. 初始化主题:
useEffect(() => {
  const savedTheme = localStorage.getItem('theme') || 
    (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  document.documentElement.setAttribute('data-theme', savedTheme);
}, []);

延伸建议:

  • 使用styled-components或emotion的theme
  • 使用CSS-in-JS库的ThemeProvider
  • 监听系统主题变化

35. 页面富文本编辑

用户问题(真实口语): "需要一个富文本编辑器,用户可以插入图片、表格,怎么实现?"

错误思路:

  • 使用contenteditable直接实现
  • 使用textarea
  • 认为富文本很简单

正确分析: 富文本编辑器问题:

  1. contenteditable兼容性差
  2. XSS安全风险
  3. 功能实现复杂
  4. 性能问题

最佳解决方案: (使用成熟的富文本编辑器库)

  1. 使用Quill:
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

const Editor = () => {
  const [value, setValue] = useState('');
  
  return (
    <ReactQuill
      value={value}
      onChange={setValue}
      modules={{
        toolbar: [
          [{ 'header': [1, 2, false] }],
          ['bold', 'italic', 'underline'],
          ['image', 'code-block']
        ]
      }}
    />
  );
};
  1. 使用TinyMCE:
import { Editor } from '@tinymce/tinymce-react';

const Editor = () => (
  <Editor
    apiKey="your-api-key"
    initialValue="<p>初始内容</p>"
    init={{
      height: 500,
      menubar: false,
      plugins: 'image table code',
      toolbar: 'undo redo | formatselect | bold italic | image table code'
    }}
  />
);

延伸建议:

  • 使用DOMPurify过滤XSS
  • 实现图片上传功能
  • 自定义工具栏

36. 页面拖拽排序

用户问题(真实口语): "用户想要拖拽列表项排序,怎么实现?"

错误思路:

  • 使用原生drag and drop API
  • 使用鼠标事件模拟
  • 认为拖拽很简单

正确分析: 拖拽排序问题:

  1. 原生API兼容性差
  2. 移动端不支持
  3. 动画效果差
  4. 性能问题

最佳解决方案:

  1. 使用react-beautiful-dnd:
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

const List = ({ items, onReorder }) => {
  const handleDragEnd = (result) => {
    if (!result.destination) return;
    const newItems = reorder(items, result.source.index, result.destination.index);
    onReorder(newItems);
  };

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="list">
        {(provided) => (
          <div {...provided.droppableProps} ref={provided.innerRef}>
            {items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided) => (
                  <div
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    ref={provided.innerRef}
                  >
                    {item.content}
                  </div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};
  1. 使用dnd-kit(更现代):
import { DndContext, closestCenter, useSortable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';

const SortableItem = ({ id, children }) => {
  const { attributes, listeners, setNodeRef, transform } = useSortable({ id });
  const style = {
    transform: CSS.Transform.toString(transform)
  };
  
  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      {children}
    </div>
  );
};

延伸建议:

  • 添加拖拽动画
  • 支持多列表拖拽
  • 移动端触摸支持

37. 页面图表渲染

用户问题(真实口语): "需要展示数据图表,柱状图、折线图,怎么实现?"

错误思路:

  • 使用Canvas自己画
  • 使用SVG手动绘制
  • 认为图表很简单

正确分析: 图表渲染问题:

  1. 手动实现复杂
  2. 响应式适配困难
  3. 交互功能缺失
  4. 性能问题

最佳解决方案:

  1. 使用ECharts:
import * as echarts from 'echarts';

const Chart = () => {
  const chartRef = useRef(null);
  
  useEffect(() => {
    const chart = echarts.init(chartRef.current);
    chart.setOption({
      xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
      yAxis: { type: 'value' },
      series: [{ type: 'bar', data: [120, 200, 150] }]
    });
    
    return () => chart.dispose();
  }, []);
  
  return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
};
  1. 使用Recharts(React专用):
import { BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts';

const data = [
  { name: 'Mon', value: 120 },
  { name: 'Tue', value: 200 },
  { name: 'Wed', value: 150 }
];

const Chart = () => (
  <BarChart width={400} height={400} data={data}>
    <XAxis dataKey="name" />
    <YAxis />
    <Tooltip />
    <Bar dataKey="value" fill="#8884d8" />
  </BarChart>
);

延伸建议:

  • 实现图表交互(点击、缩放)
  • 响应式适配
  • 数据动态更新

38. 页面地图集成

用户问题(真实口语): "需要在页面显示地图,标记位置,怎么集成地图?"

错误思路:

  • 使用图片代替地图
  • 自己实现地图功能
  • 认为地图很简单

正确分析: 地图集成问题:

  1. 需要地图API密钥
  2. 加载性能问题
  3. 自定义标记复杂
  4. 移动端适配

最佳解决方案:

  1. 使用高德地图:
import AMapLoader from '@amap/amap-jsapi-loader';

const Map = () => {
  const mapRef = useRef(null);
  
  useEffect(() => {
    AMapLoader.load({
      key: 'your-api-key',
      version: '2.0',
      plugins: ['AMap.Marker']
    }).then((AMap) => {
      const map = new AMap.Map(mapRef.current, {
        zoom: 11,
        center: [116.397428, 39.90923]
      });
      
      const marker = new AMap.Marker({
        position: [116.397428, 39.90923]
      });
      map.add(marker);
    });
  }, []);
  
  return <div ref={mapRef} style={{ width: '100%', height: '400px' }} />;
};
  1. 使用react-leaflet(开源地图):
import { MapContainer, TileLayer, Marker } from 'react-leaflet';

const Map = () => (
  <MapContainer center={[39.90923, 116.397428]} zoom={11} style={{ height: '400px' }}>
    <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
    <Marker position={[39.90923, 116.397428]} />
  </MapContainer>
);

延伸建议:

  • 实现地图搜索功能
  • 路径规划
  • 自定义地图样式

39. 页面视频播放

用户问题(真实口语): "需要在页面播放视频,支持自定义控制条,怎么实现?"

错误思路:

  • 使用video标签就不管了
  • 使用iframe嵌入
  • 认为视频很简单

正确分析: 视频播放问题:

  1. 浏览器兼容性
  2. 自定义控制条复杂
  3. 视频加载优化
  4. 移动端适配

最佳解决方案:

  1. 使用video.js:
import videojs from 'video.js';
import 'video.js/dist/video-js.css';

const VideoPlayer = () => {
  const videoRef = useRef(null);
  const playerRef = useRef(null);
  
  useEffect(() => {
    const player = videojs(videoRef.current, {
      controls: true,
      autoplay: false,
      sources: [{
        src: 'video.mp4',
        type: 'video/mp4'
      }]
    });
    
    playerRef.current = player;
    
    return () => {
      if (playerRef.current) {
        playerRef.current.dispose();
      }
    };
  }, []);
  
  return (
    <div data-vjs-player>
      <video ref={videoRef} className="video-js" />
    </div>
  );
};
  1. 使用react-player:
import ReactPlayer from 'react-player';

const VideoPlayer = () => (
  <ReactPlayer
    url="video.mp4"
    controls
    width="100%"
    height="400px"
  />
);

延伸建议:

  • 实现视频预览
  • 支持倍速播放
  • 视频弹幕功能

40. 页面音频播放

用户问题(真实口语): "需要播放音频,显示波形,怎么实现?"

错误思路:

  • 使用audio标签就不管了
  • 认为音频很简单
  • 不显示波形

正确分析: 音频播放问题:

  1. 波形可视化复杂
  2. 音频加载优化
  3. 播放列表管理
  4. 移动端自动播放限制

最佳解决方案:

  1. 使用Howler.js:
import { Howl } from 'howler';

const sound = new Howl({
  src: ['audio.mp3'],
  html5: true
});

const play = () => {
  sound.play();
};

const pause = () => {
  sound.pause();
};
  1. 使用Web Audio API绘制波形:
const drawWaveform = async (audioUrl) => {
  const audioContext = new AudioContext();
  const response = await fetch(audioUrl);
  const arrayBuffer = await response.arrayBuffer();
  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
  
  const canvas = document.getElementById('waveform');
  const ctx = canvas.getContext('2d');
  const data = audioBuffer.getChannelData(0);
  
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  
  const step = Math.ceil(data.length / canvas.width);
  const amp = canvas.height / 2;
  
  for (let i = 0; i < canvas.width; i++) {
    let min = 1.0;
    let max = -1.0;
    for (let j = 0; j < step; j++) {
      const datum = data[(i * step) + j];
      if (datum < min) min = datum;
      if (datum > max) max = datum;
    }
    ctx.lineTo(i, (1 + min) * amp);
    ctx.lineTo(i, (1 + max) * amp);
  }
  
  ctx.stroke();
};

延伸建议:

  • 实现音频可视化效果
  • 支持音频格式转换
  • 音频剪辑功能

41. 页面二维码生成

用户问题(真实口语): "需要生成二维码,用户扫码跳转,怎么实现?"

错误思路:

  • 使用在线API生成
  • 使用图片代替
  • 认为二维码很简单

正确分析: 二维码生成问题:

  1. 需要二维码生成库
  2. 自定义样式复杂
  3. 扫码兼容性
  4. 二维码大小适配

最佳解决方案:

  1. 使用qrcode.react:
import QRCode from 'qrcode.react';

const QRCodeGenerator = ({ url }) => (
  <QRCode
    value={url}
    size={200}
    level="H"
    includeMargin={true}
  />
);
  1. 使用qrcode库:
import QRCode from 'qrcode';

const generateQRCode = async (text) => {
  const canvas = document.getElementById('qrcode');
  await QRCode.toCanvas(canvas, text, {
    width: 200,
    margin: 2,
    color: {
      dark: '#000000',
      light: '#ffffff'
    }
  });
};

延伸建议:

  • 实现扫码功能
  • 自定义二维码样式
  • 二维码logo嵌入

42. 页面Excel导入导出

用户问题(真实口语): "用户需要导入Excel数据,怎么实现?"

错误思路:

  • 让用户复制粘贴
  • 使用CSV代替
  • 认为Excel很简单

正确分析: Excel导入导出问题:

  1. Excel格式复杂
  2. 大文件处理
  3. 数据验证
  4. 样式保留

最佳解决方案:

  1. 使用xlsx库:
import * as XLSX from 'xlsx';

const importExcel = (file) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const data = new Uint8Array(e.target.result);
      const workbook = XLSX.read(data, { type: 'array' });
      const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
      const jsonData = XLSX.utils.sheet_to_json(firstSheet);
      resolve(jsonData);
    };
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
  });
};

const exportExcel = (data, filename) => {
  const worksheet = XLSX.utils.json_to_sheet(data);
  const workbook = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
  XLSX.writeFile(workbook, filename);
};

延伸建议:

  • 实现Excel模板下载
  • 数据格式验证
  • 大文件分片处理

43. 页面PDF生成

用户问题(真实口语): "需要将页面内容导出为PDF,怎么实现?"

错误思路:

  • 让用户打印页面
  • 使用截图
  • 认为PDF很简单

正确分析: PDF生成问题:

  1. 样式保留困难
  2. 中文支持
  3. 分页控制
  4. 性能问题

最佳解决方案:

  1. 使用jsPDF:
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';

const generatePDF = async (element) => {
  const canvas = await html2canvas(element);
  const imgData = canvas.toDataURL('image/png');
  
  const pdf = new jsPDF('p', 'mm', 'a4');
  const imgWidth = 210;
  const pageHeight = 297;
  const imgHeight = (canvas.height * imgWidth) / canvas.width;
  
  pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
  pdf.save('document.pdf');
};
  1. 使用react-pdf生成PDF:
import { Document, Page, Text, View } from '@react-pdf/renderer';

const MyDocument = () => (
  <Document>
    <Page size="A4">
      <View>
        <Text>Hello World</Text>
      </View>
    </Page>
  </Document>
);

延伸建议:

  • 实现PDF预览
  • 支持PDF模板
  • PDF加密功能

44. 页面剪贴板操作

用户问题(真实口语): "用户需要复制内容到剪贴板,怎么实现?"

错误思路:

  • 使用document.execCommand('copy')
  • 让用户手动复制
  • 认为剪贴板很简单

正确分析: 剪贴板操作问题:

  1. 浏览器兼容性
  2. 权限限制
  3. HTTPS要求
  4. 移动端支持

最佳解决方案:

  1. 使用Clipboard API:
const copyToClipboard = async (text) => {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch (err) {
    console.error('复制失败:', err);
    return false;
  }
};

const pasteFromClipboard = async () => {
  try {
    const text = await navigator.clipboard.readText();
    return text;
  } catch (err) {
    console.error('粘贴失败:', err);
    return null;
  }
};
  1. 使用react-copy-to-clipboard:
import { CopyToClipboard } from 'react-copy-to-clipboard';

const CopyButton = ({ text }) => (
  <CopyToClipboard text={text}>
    <button>复制</button>
  </CopyToClipboard>
);

延伸建议:

  • 实现复制成功提示
  • 支持富文本复制
  • 剪贴板权限管理

45. 页面全屏功能

用户问题(真实口语): "用户需要全屏查看内容,怎么实现?"

错误思路:

  • 使用CSS放大
  • 认为全屏很简单
  • 不考虑退出全屏

正确分析: 全屏功能问题:

  1. 浏览器兼容性
  2. 全屏状态管理
  3. 退出全屏
  4. 移动端支持

最佳解决方案:

  1. 使用Fullscreen API:
const enterFullscreen = (element) => {
  if (element.requestFullscreen) {
    element.requestFullscreen();
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen();
  }
};

const exitFullscreen = () => {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  }
};

const isFullscreen = () => {
  return !!(
    document.fullscreenElement ||
    document.webkitFullscreenElement
  );
};
  1. 使用react-fullscreen:
import { Fullscreen, useFullscreen } from 'react-fullscreen';

const FullscreenComponent = () => {
  const { enter, exit, isFull } = useFullscreen();
  
  return (
    <Fullscreen enabled={isFull}>
      <button onClick={isFull ? exit : enter}>
        {isFull ? '退出全屏' : '全屏'}
      </button>
    </Fullscreen>
  );
};

延伸建议:

  • 全屏状态监听
  • 全屏样式适配
  • ESC键退出全屏

46. 页面通知提醒

用户问题(真实口语): "需要给用户发送通知提醒,怎么实现?"

错误思路:

  • 使用alert
  • 使用弹窗
  • 认为通知很简单

正确分析: 通知提醒问题:

  1. 浏览器兼容性
  2. 权限限制
  3. 通知管理
  4. 移动端支持

最佳解决方案:

  1. 使用Notification API:
const requestNotificationPermission = async () => {
  const permission = await Notification.requestPermission();
  return permission === 'granted';
};

const showNotification = (title, options) => {
  if (Notification.permission === 'granted') {
    new Notification(title, {
      body: options.body,
      icon: options.icon,
      badge: options.badge
    });
  }
};
  1. 使用react-toastify:
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

const showToast = (message) => {
  toast.success(message, {
    position: 'top-right',
    autoClose: 3000
  });
};

延伸建议:

  • 通知点击事件
  • 通知关闭事件
  • 通知队列管理

47. 页面地理位置

用户问题(真实口语): "需要获取用户地理位置,怎么实现?"

错误思路:

  • 使用IP定位
  • 让用户手动输入
  • 认为定位很简单

正确分析: 地理位置问题:

  1. 权限限制
  2. 精度问题
  3. 超时处理
  4. 隐私保护

最佳解决方案:

  1. 使用Geolocation API:
const getCurrentPosition = () => {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error('浏览器不支持地理位置'));
      return;
    }
    
    navigator.geolocation.getCurrentPosition(
      (position) => {
        resolve({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy
        });
      },
      (error) => {
        reject(error);
      },
      {
        enableHighAccuracy: true,
        timeout: 5000,
        maximumAge: 0
      }
    );
  });
};
  1. 使用react-geolocated:
import { geolocated } from 'react-geolocated';

const LocationComponent = geolocated({
  positionOptions: {
    enableHighAccuracy: true,
  },
  watchPosition: true,
})(({ isGeolocationAvailable, isGeolocationEnabled, coords }) => {
  if (!isGeolocationAvailable) {
    return <div>浏览器不支持地理位置</div>;
  }
  if (!isGeolocationEnabled) {
    return <div>未启用地理位置</div>;
  }
  return <div>纬度: {coords.latitude}, 经度: {coords.longitude}</div>;
});

延伸建议:

  • 位置变化监听
  • 地图显示位置
  • 位置权限管理

48. 页面设备信息

用户问题(真实口语): "需要获取用户设备信息,怎么实现?"

错误思路:

  • 使用User-Agent解析
  • 让用户手动选择
  • 认为设备信息很简单

正确分析: 设备信息问题:

  1. User-Agent不可靠
  2. 隐私限制
  3. 浏览器兼容性
  4. 移动端识别

最佳解决方案:

  1. 使用navigator对象:
const getDeviceInfo = () => {
  return {
    userAgent: navigator.userAgent,
    platform: navigator.platform,
    language: navigator.language,
    isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
    isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
    isAndroid: /Android/.test(navigator.userAgent),
    screenWidth: window.screen.width,
    screenHeight: window.screen.height,
    pixelRatio: window.devicePixelRatio
  };
};
  1. 使用react-device-detect:
import { isMobile, isBrowser, isTablet } from 'react-device-detect';

const DeviceInfo = () => (
  <div>
    {isMobile && <div>移动设备</div>}
    {isBrowser && <div>浏览器</div>}
    {isTablet && <div>平板</div>}
  </div>
);

延伸建议:

  • 设备能力检测
  • 响应式适配
  • 设备特定功能

49. 页面网络状态

用户问题(真实口语): "需要检测网络状态,离线时提示用户,怎么实现?"

错误思路:

  • 定期请求接口检测
  • 让用户自己判断
  • 认为网络检测很简单

正确分析: 网络状态问题:

  1. 离线检测
  2. 网络变化监听
  3. 网络类型识别
  4. 兼容性问题

最佳解决方案:

  1. 使用Navigator Online API:
const useNetworkStatus = () => {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  return isOnline;
};
  1. 使用react-network-info:
import { useNetworkStatus } from 'react-network-info';

const NetworkStatus = () => {
  const { isOnline, effectiveType } = useNetworkStatus();
  
  return (
    <div>
      {isOnline ? '在线' : '离线'}
      {effectiveType && <div>网络类型: {effectiveType}</div>}
    </div>
  );
};

延伸建议:

  • 离线数据缓存
  • 网络恢复自动重试
  • 网络速度检测

50. 页面性能优化总结

用户问题(真实口语): "页面性能优化有哪些方面需要注意?"

错误思路:

  • 只关注加载速度
  • 只优化代码
  • 认为性能优化很简单

正确分析: 性能优化是多方面的:

  1. 加载性能
  2. 渲染性能
  3. 交互性能
  4. 内存性能
  5. 网络性能

最佳解决方案:

  1. 加载优化:
  • 代码分割
  • 懒加载
  • 资源压缩
  • CDN加速
  • HTTP/2
  1. 渲染优化:
  • 虚拟滚动
  • 避免重排重绘
  • 使用transform和opacity
  • GPU加速
  • 减少DOM操作
  1. 交互优化:
  • 防抖节流
  • 事件委托
  • Web Workers
  • requestAnimationFrame
  1. 内存优化:
  • 及时清理引用
  • 避免内存泄漏
  • 使用WeakMap/WeakSet
  • 对象池
  1. 网络优化:
  • 请求合并
  • 数据压缩
  • 缓存策略
  • 预加载

延伸建议:

  • 使用Lighthouse定期检测
  • 建立性能监控体系
  • 持续优化迭代
  • 关注Core Web Vitals指标

总结

以上50条前端真实问题场景涵盖了前端开发的各个方面,包括:

  1. 基础问题:白屏、样式、跨域、闪烁等
  2. 性能问题:内存泄漏、打包体积、接口请求、长列表等
  3. 兼容性问题:移动端适配、浏览器兼容、IE兼容等
  4. 功能实现:表单、路由、权限、国际化等
  5. 高级功能:WebSocket、富文本、拖拽、图表等

51. React Hooks依赖问题

用户问题(真实口语): "我用useEffect的时候,eslint总是警告我缺少依赖,但我加了依赖就死循环了,咋办?"

错误思路:

  • 直接禁用eslint规则
  • 把依赖数组留空
  • 使用useRef绕过检查

正确分析: Hooks依赖问题原因:

  1. 依赖项中包含对象或数组,每次渲染都是新引用
  2. 函数作为依赖项,每次渲染都是新函数
  3. 在effect中setState导致依赖变化
  4. 不理解依赖数组的作用

最佳解决方案:

  1. 使用useCallback缓存函数:
const handleSubmit = useCallback(() => {
  console.log('提交');
}, []); // 真实依赖放这里

useEffect(() => {
  handleSubmit();
}, [handleSubmit]);
  1. 使用useMemo缓存对象和数组:
const config = useMemo(() => ({
  url: '/api',
  method: 'POST'
}), []); // 依赖变化时才重新创建
  1. 拆分多个useEffect,每个处理不同逻辑
  2. 使用函数式更新避免依赖:
setCount(prev => prev + 1); // 不需要count作为依赖

延伸建议:

  • 使用eslint-plugin-react-hooks的exhaustive-deps规则
  • 理解JavaScript的闭包和引用相等性
  • 使用useReducer管理复杂状态逻辑

52. Vue响应式数据丢失

用户问题(真实口语): "Vue里我直接给对象加新属性,页面为啥不更新啊?"

错误思路:

  • 使用this.$forceUpdate强制更新
  • 重新赋值整个对象
  • 认为是Vue的bug

正确分析: Vue2响应式限制:

  1. 对象新增属性不是响应式的
  2. 数组通过索引直接修改不是响应式的
  3. 修改数组length不是响应式的
  4. 不理解Vue.set的作用

最佳解决方案:

  1. 使用Vue.set添加新属性:
// Vue2
this.$set(this.obj, 'newProp', value);
// 或
Vue.set(this.obj, 'newProp', value);
  1. 使用数组的响应式方法:
// 正确
this.arr.push(newItem);
this.arr.splice(index, 1, newItem);

// 错误
this.arr[index] = newItem; // 不触发更新
  1. Vue3使用Proxy,没有这个问题,但需要注意:
// Vue3直接赋值即可
this.obj.newProp = value; // 自动响应式

延伸建议:

  • 理解Vue2的Object.defineProperty原理
  • 升级到Vue3避免这类问题
  • 使用immer或immutability-helper管理不可变数据

53. 前端路由守卫死循环

用户问题(真实口语): "我在路由守卫里判断没登录就跳转到登录页,结果一直跳转,浏览器卡死了"

错误思路:

  • 减少守卫判断条件
  • 使用setTimeout延迟跳转
  • 认为是路由框架的bug

正确分析: 路由守卫死循环原因:

  1. 登录页也需要守卫判断,导致循环重定向
  2. 跳转目标和当前路由相同,但参数不同
  3. 守卫逻辑错误,总是触发跳转
  4. 未排除白名单路由

最佳解决方案:

  1. 设置白名单路由:
const whiteList = ['/login', '/register', '/404'];

router.beforeEach((to, from, next) => {
  const isLogin = getToken();
  
  if (isLogin) {
    if (to.path === '/login') {
      next('/dashboard'); // 已登录跳转到首页
    } else {
      next(); // 正常访问
    }
  } else {
    if (whiteList.includes(to.path)) {
      next(); // 白名单直接放行
    } else {
      next('/login'); // 未登录跳转到登录页
    }
  }
});
  1. 使用replace而不是push避免历史记录堆积
  2. 确保登录页不在守卫逻辑中重复触发

延伸建议:

  • 使用addRoutes动态添加路由,避免权限路由硬编码
  • 路由守卫逻辑尽量简单,分散到多个守卫中
  • 添加loading状态,避免用户重复点击

54. 前端微应用样式隔离失败

用户问题(真实口语): "微前端子应用的样式把主应用污染了,样式全乱了,怎么隔离?"

错误思路:

  • 使用!important覆盖样式
  • 手动给所有样式加前缀
  • 认为是微前端框架的bug

正确分析: 样式隔离失败原因:

  1. CSS全局污染,样式作用域冲突
  2. 使用了全局标签选择器(div, p等)
  3. CSS Modules配置错误
  4. Shadow DOM使用不当

最佳解决方案:

  1. 使用CSS Modules:
/* Button.module.css */
.button {
  background: blue;
  color: white;
}
import styles from './Button.module.css';

const Button = () => <button className={styles.button}>点击</button>;
  1. 使用styled-components:
const Button = styled.button`
  background: blue;
  color: white;
`;
  1. qiankun框架使用strictStyleIsolation:
start({
  sandbox: {
    strictStyleIsolation: true // 启用Shadow DOM隔离
  }
});
  1. 使用BEM命名规范:
.app-header__button--primary { /* 模块-元素--修饰符 */
  background: blue;
}

延伸建议:

  • 主应用和子应用使用不同的CSS前缀
  • 避免使用全局标签选择器,使用class
  • 使用postcss-prefix-selector自动添加前缀

55. 前端缓存数据过期策略

用户问题(真实口语): "我把接口数据存localStorage了,但数据过期了页面还显示旧的,怎么自动清理?"

错误思路:

  • 存一个固定时间,到期就清理
  • 每次打开页面都清空缓存
  • 认为缓存不需要过期

正确分析: 缓存过期问题原因:

  1. 未设置过期时间戳
  2. 未定期检查清理
  3. 缓存数据结构不合理
  4. 未区分不同数据的过期策略

最佳解决方案:

  1. 封装带过期时间的缓存:
const setCache = (key, data, ttl = 3600000) => {
  const cacheData = {
    data,
    expire: Date.now() + ttl // 过期时间
  };
  localStorage.setItem(key, JSON.stringify(cacheData));
};

const getCache = (key) => {
  const cacheData = localStorage.getItem(key);
  if (!cacheData) return null;
  
  const { data, expire } = JSON.parse(cacheData);
  if (Date.now() > expire) {
    localStorage.removeItem(key); // 过期清理
    return null;
  }
  return data;
};
  1. 使用sessionStorage存储会话级缓存
  2. 使用IndexedDB存储大量结构化缓存数据
  3. 结合Service Worker实现更复杂的缓存策略

延伸建议:

  • 使用localforage库统一API
  • 实现LRU缓存淘汰策略
  • 缓存版本控制,接口升级时清理旧缓存

56. 前端请求重试机制

用户问题(真实口语): "用户网络不好,接口经常失败,怎么自动重试?"

错误思路:

  • 让用户手动刷新页面
  • 在catch里无限重试
  • 使用setInterval定时重试

正确分析: 请求重试问题原因:

  1. 未区分错误类型,不应该重试的也重试了
  2. 重试次数过多,浪费资源
  3. 重试间隔太短,网络来不及恢复
  4. 未考虑幂等性,非幂等接口重试导致数据错误

最佳解决方案:

  1. 使用axios-retry:
import axiosRetry from 'axios-retry';

axiosRetry(axios, {
  retries: 3, // 重试3次
  retryDelay: (retryCount) => {
    return retryCount * 1000; // 指数退避
  },
  retryCondition: (error) => {
    // 只重试网络错误和5xx错误
    return axiosRetry.isNetworkOrIdempotentRequestError(error) || 
           error.response.status >= 500;
  }
});
  1. 手动实现重试:
const requestWithRetry = async (fn, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
};

延伸建议:

  • 实现指数退避算法
  • 重试前检查网络状态
  • 提供取消重试的选项
  • 记录重试日志,便于排查问题

57. 前端并发请求控制

用户问题(真实口语): "页面初始化要调十几个接口,浏览器卡死了,怎么控制并发?"

错误思路:

  • 让后端合并接口
  • 使用setTimeout延迟请求
  • 认为浏览器会自动处理

正确分析: 并发请求问题原因:

  1. 浏览器有并发限制(通常6-8个)
  2. 大量请求同时发起,阻塞主线程
  3. 未按优先级排序
  4. 未使用请求池

最佳解决方案:

  1. 使用p-limit控制并发:
import pLimit from 'p-limit';

const limit = pLimit(3); // 同时最多3个请求

const promises = urls.map(url => {
  return limit(() => fetch(url));
});

await Promise.all(promises);
  1. 按优先级分批请求:
// 先请求关键数据
const criticalData = await fetchCriticalData();

// 再请求非关键数据
const nonCriticalData = await fetchNonCriticalData();
  1. 使用请求队列:
class RequestQueue {
  constructor(maxConcurrent = 3) {
    this.maxConcurrent = maxConcurrent;
    this.queue = [];
    this.running = 0;
  }
  
  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.run();
    });
  }
  
  async run() {
    while (this.running < this.maxConcurrent && this.queue.length) {
      const { requestFn, resolve, reject } = this.queue.shift();
      this.running++;
      
      try {
        const result = await requestFn();
        resolve(result);
      } catch (error) {
        reject(error);
      } finally {
        this.running--;
        this.run();
      }
    }
  }
}

延伸建议:

  • 使用GraphQL合并多个请求
  • 实现请求优先级队列
  • 使用Service Worker代理请求

58. 前端骨架屏实现

用户问题(真实口语): "页面加载的时候一片空白,用户体验不好,怎么显示加载骨架?"

错误思路:

  • 使用loading动画
  • 让后端返回HTML骨架
  • 认为骨架屏实现复杂

正确分析: 骨架屏问题原因:

  1. 未在数据加载前显示占位
  2. 骨架屏和实际内容结构差异大
  3. 未根据实际内容生成骨架
  4. 骨架屏样式未优化

最佳解决方案:

  1. 使用react-loading-skeleton:
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';

const Article = ({ title, content, loading }) => {
  return (
    <div>
      <h1>{loading ? <Skeleton /> : title}</h1>
      <p>{loading ? <Skeleton count={5} /> : content}</p>
    </div>
  );
};
  1. 使用骨架屏组件库:
const CardSkeleton = () => (
  <div className="skeleton-card">
    <div className="skeleton-image" />
    <div className="skeleton-title" />
    <div className="skeleton-text" />
  </div>
);
  1. 使用webpack插件自动生成骨架屏:
// webpack.config.js
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');

plugins: [
  new SkeletonWebpackPlugin({
    webpackConfig: {
      entry: {
        app: path.join(__dirname, './src/skeleton.js')
      }
    }
  })
]

延伸建议:

  • 根据实际内容动态生成骨架屏
  • 使用CSS动画增强骨架屏效果
  • 骨架屏和实际内容平滑过渡

59. 前端错误上报SourceMap

用户问题(真实口语): "生产环境代码报错,但都是压缩后的代码,怎么看原始代码错误位置?"

错误思路:

  • 在开发环境复现问题
  • 关闭代码压缩
  • 认为无法定位生产环境问题

正确分析: SourceMap问题原因:

  1. 生产环境代码压缩,无法定位原始代码
  2. 未上传SourceMap到监控平台
  3. SourceMap文件泄露风险
  4. 未配置正确的source-map类型

最佳解决方案:

  1. 配置webpack生成SourceMap:
// webpack.config.js
module.exports = {
  devtool: 'hidden-source-map', // 生成source-map但不暴露
  productionSourceMap: false // 不生成.map文件到生产环境
};
  1. 上传SourceMap到监控平台:
// 使用sentry-webpack-plugin
const SentryWebpackPlugin = require('@sentry/webpack-plugin');

plugins: [
  new SentryWebpackPlugin({
    authToken: process.env.SENTRY_AUTH_TOKEN,
    org: 'your-org',
    project: 'your-project',
    include: './dist',
    ignore: ['node_modules'],
    release: process.env.RELEASE_VERSION
  })
]
  1. 使用Sentry查看原始错误:
import * as Sentry from '@sentry/browser';

Sentry.init({
  dsn: 'your-dsn',
  release: process.env.RELEASE_VERSION
});

延伸建议:

  • 使用private-sourcemap保护SourceMap
  • 只在监控平台上传SourceMap,不部署到生产
  • 配置错误采样,避免上报过多错误

60. 前端性能监控指标

用户问题(真实口语): "老板问我们网站性能怎么样,除了加载时间还有哪些指标要监控?"

错误思路:

  • 只监控首屏加载时间
  • 使用performance.now手动计算
  • 认为性能就是加载速度

正确分析: 性能指标问题原因:

  1. 只关注加载性能,忽略运行时性能
  2. 未使用标准的性能指标
  3. 未监控用户真实体验
  4. 未区分不同网络环境

最佳解决方案:

  1. 监控Core Web Vitals:
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log); // 累积布局偏移
getFID(console.log); // 首次输入延迟
getLCP(console.log); // 最大内容绘制
  1. 使用Performance API:
// 页面加载时间
const perfData = performance.getEntriesByType('navigation')[0];
const pageLoadTime = perfData.loadEventEnd - perfData.fetchStart;

// 首字节时间
const ttfb = perfData.responseStart - perfData.requestStart;

// DNS解析时间
const dnsTime = perfData.domainLookupEnd - perfData.domainLookupStart;
  1. 监控运行时性能:
// 长任务监控
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log('长任务:', entry.duration);
    }
  }
});
observer.observe({ entryTypes: ['longtask'] });

延伸建议:

  • 使用Google Analytics或Sentry收集性能数据
  • 按设备、网络条件分组分析
  • 设置性能预算,超过阈值告警


📚 系列文章

🔗 相关链接


觉得有用的话,别忘了点赞 👍、收藏 ⭐、关注 👀 支持一下~