前端真实问题场景130条 - 进阶问题篇 (31-60)
本文是《前端真实问题场景130条》系列的第2篇,涵盖第31-60条问题。
💡 每篇文章包含:真实用户问题 + 错误思路 + 正确分析 + 最佳解决方案 + 延伸建议
31. 页面无障碍访问
用户问题(真实口语): "有视障用户反馈我们网站用屏幕阅读器无法使用,怎么优化无障碍访问?"
错误思路:
- 认为无障碍访问不重要
- 只加aria标签就不管了
- 认为这是浏览器问题
正确分析: 无障碍访问问题:
- 未使用语义化HTML标签
- 图片缺少alt属性
- 表单元素缺少label
- 颜色对比度不足
- 键盘导航不支持
最佳解决方案:
- 使用语义化HTML:
<nav aria-label="主导航">
<ul>
<li><a href="/home">首页</a></li>
</ul>
</nav>
<main>
<article>
<h1>文章标题</h1>
</article>
</main>
- 图片添加alt描述:
<img src="chart.png" alt="2024年销售数据柱状图">
- 表单关联label:
<label for="email">邮箱</label>
<input type="email" id="email" name="email">
- 支持键盘导航:
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
" handleSubmit();
}
};
- 使用ARIA属性:
<button aria-label="关闭对话框" aria-expanded="false">
×
</button>
延伸建议:
- 使用axe DevTools进行无障碍测试
- 遵循WCAG 2.1标准
- 定期邀请视障用户测试
32. 页面缓存策略
用户问题(真实口语): "用户刷新页面还是显示旧内容,怎么让用户看到最新数据?"
错误思路:
- 让用户强制刷新(Ctrl+F5)
- 在URL加随机参数
- 禁用所有缓存
正确分析: 缓存策略问题:
- 未配置合理的缓存策略
- 静态资源缓存时间过长
- HTML文件未设置不缓存
- 未使用版本号或hash
最佳解决方案:
- 配置webpack文件名hash:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
- 配置服务器缓存头:
# 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";
}
- 使用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动画就不管了
正确分析: 动画性能问题:
- 使用了会触发布局重排的属性(width、height等)
- 未使用transform和opacity
- 动画元素过多
- 未使用requestAnimationFrame
- 未开启GPU加速
最佳解决方案:
- 使用transform和opacity:
/* 错误 */
.element {
transition: left 0.3s, width 0.3s;
}
/* 正确 */
.element {
transform: translateX(100px);
transition: transform 0.3s;
will-change: transform;
}
- 使用requestAnimationFrame:
const animate = () => {
element.style.transform = `translateX(${x}px)`;
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
- 开启GPU加速:
.gpu-accelerated {
transform: translateZ(0);
backface-visibility: hidden;
}
- 使用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覆盖样式
正确分析: 深色模式实现问题:
- 未使用CSS变量
- 颜色对比度不足
- 未保存用户偏好
- 切换时页面闪烁
最佳解决方案:
- 使用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);
}
- 切换主题:
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);
};
- 初始化主题:
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
- 认为富文本很简单
正确分析: 富文本编辑器问题:
- contenteditable兼容性差
- XSS安全风险
- 功能实现复杂
- 性能问题
最佳解决方案: (使用成熟的富文本编辑器库)
- 使用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']
]
}}
/>
);
};
- 使用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
- 使用鼠标事件模拟
- 认为拖拽很简单
正确分析: 拖拽排序问题:
- 原生API兼容性差
- 移动端不支持
- 动画效果差
- 性能问题
最佳解决方案:
- 使用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>
);
};
- 使用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手动绘制
- 认为图表很简单
正确分析: 图表渲染问题:
- 手动实现复杂
- 响应式适配困难
- 交互功能缺失
- 性能问题
最佳解决方案:
- 使用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' }} />;
};
- 使用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. 页面地图集成
用户问题(真实口语): "需要在页面显示地图,标记位置,怎么集成地图?"
错误思路:
- 使用图片代替地图
- 自己实现地图功能
- 认为地图很简单
正确分析: 地图集成问题:
- 需要地图API密钥
- 加载性能问题
- 自定义标记复杂
- 移动端适配
最佳解决方案:
- 使用高德地图:
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' }} />;
};
- 使用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嵌入
- 认为视频很简单
正确分析: 视频播放问题:
- 浏览器兼容性
- 自定义控制条复杂
- 视频加载优化
- 移动端适配
最佳解决方案:
- 使用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>
);
};
- 使用react-player:
import ReactPlayer from 'react-player';
const VideoPlayer = () => (
<ReactPlayer
url="video.mp4"
controls
width="100%"
height="400px"
/>
);
延伸建议:
- 实现视频预览
- 支持倍速播放
- 视频弹幕功能
40. 页面音频播放
用户问题(真实口语): "需要播放音频,显示波形,怎么实现?"
错误思路:
- 使用audio标签就不管了
- 认为音频很简单
- 不显示波形
正确分析: 音频播放问题:
- 波形可视化复杂
- 音频加载优化
- 播放列表管理
- 移动端自动播放限制
最佳解决方案:
- 使用Howler.js:
import { Howl } from 'howler';
const sound = new Howl({
src: ['audio.mp3'],
html5: true
});
const play = () => {
sound.play();
};
const pause = () => {
sound.pause();
};
- 使用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生成
- 使用图片代替
- 认为二维码很简单
正确分析: 二维码生成问题:
- 需要二维码生成库
- 自定义样式复杂
- 扫码兼容性
- 二维码大小适配
最佳解决方案:
- 使用qrcode.react:
import QRCode from 'qrcode.react';
const QRCodeGenerator = ({ url }) => (
<QRCode
value={url}
size={200}
level="H"
includeMargin={true}
/>
);
- 使用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导入导出问题:
- Excel格式复杂
- 大文件处理
- 数据验证
- 样式保留
最佳解决方案:
- 使用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生成问题:
- 样式保留困难
- 中文支持
- 分页控制
- 性能问题
最佳解决方案:
- 使用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');
};
- 使用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')
- 让用户手动复制
- 认为剪贴板很简单
正确分析: 剪贴板操作问题:
- 浏览器兼容性
- 权限限制
- HTTPS要求
- 移动端支持
最佳解决方案:
- 使用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;
}
};
- 使用react-copy-to-clipboard:
import { CopyToClipboard } from 'react-copy-to-clipboard';
const CopyButton = ({ text }) => (
<CopyToClipboard text={text}>
<button>复制</button>
</CopyToClipboard>
);
延伸建议:
- 实现复制成功提示
- 支持富文本复制
- 剪贴板权限管理
45. 页面全屏功能
用户问题(真实口语): "用户需要全屏查看内容,怎么实现?"
错误思路:
- 使用CSS放大
- 认为全屏很简单
- 不考虑退出全屏
正确分析: 全屏功能问题:
- 浏览器兼容性
- 全屏状态管理
- 退出全屏
- 移动端支持
最佳解决方案:
- 使用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
);
};
- 使用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
- 使用弹窗
- 认为通知很简单
正确分析: 通知提醒问题:
- 浏览器兼容性
- 权限限制
- 通知管理
- 移动端支持
最佳解决方案:
- 使用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
});
}
};
- 使用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定位
- 让用户手动输入
- 认为定位很简单
正确分析: 地理位置问题:
- 权限限制
- 精度问题
- 超时处理
- 隐私保护
最佳解决方案:
- 使用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
}
);
});
};
- 使用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解析
- 让用户手动选择
- 认为设备信息很简单
正确分析: 设备信息问题:
- User-Agent不可靠
- 隐私限制
- 浏览器兼容性
- 移动端识别
最佳解决方案:
- 使用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
};
};
- 使用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. 页面网络状态
用户问题(真实口语): "需要检测网络状态,离线时提示用户,怎么实现?"
错误思路:
- 定期请求接口检测
- 让用户自己判断
- 认为网络检测很简单
正确分析: 网络状态问题:
- 离线检测
- 网络变化监听
- 网络类型识别
- 兼容性问题
最佳解决方案:
- 使用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;
};
- 使用react-network-info:
import { useNetworkStatus } from 'react-network-info';
const NetworkStatus = () => {
const { isOnline, effectiveType } = useNetworkStatus();
return (
<div>
{isOnline ? '在线' : '离线'}
{effectiveType && <div>网络类型: {effectiveType}</div>}
</div>
);
};
延伸建议:
- 离线数据缓存
- 网络恢复自动重试
- 网络速度检测
50. 页面性能优化总结
用户问题(真实口语): "页面性能优化有哪些方面需要注意?"
错误思路:
- 只关注加载速度
- 只优化代码
- 认为性能优化很简单
正确分析: 性能优化是多方面的:
- 加载性能
- 渲染性能
- 交互性能
- 内存性能
- 网络性能
最佳解决方案:
- 加载优化:
- 代码分割
- 懒加载
- 资源压缩
- CDN加速
- HTTP/2
- 渲染优化:
- 虚拟滚动
- 避免重排重绘
- 使用transform和opacity
- GPU加速
- 减少DOM操作
- 交互优化:
- 防抖节流
- 事件委托
- Web Workers
- requestAnimationFrame
- 内存优化:
- 及时清理引用
- 避免内存泄漏
- 使用WeakMap/WeakSet
- 对象池
- 网络优化:
- 请求合并
- 数据压缩
- 缓存策略
- 预加载
延伸建议:
- 使用Lighthouse定期检测
- 建立性能监控体系
- 持续优化迭代
- 关注Core Web Vitals指标
总结
以上50条前端真实问题场景涵盖了前端开发的各个方面,包括:
- 基础问题:白屏、样式、跨域、闪烁等
- 性能问题:内存泄漏、打包体积、接口请求、长列表等
- 兼容性问题:移动端适配、浏览器兼容、IE兼容等
- 功能实现:表单、路由、权限、国际化等
- 高级功能:WebSocket、富文本、拖拽、图表等
51. React Hooks依赖问题
用户问题(真实口语): "我用useEffect的时候,eslint总是警告我缺少依赖,但我加了依赖就死循环了,咋办?"
错误思路:
- 直接禁用eslint规则
- 把依赖数组留空
- 使用useRef绕过检查
正确分析: Hooks依赖问题原因:
- 依赖项中包含对象或数组,每次渲染都是新引用
- 函数作为依赖项,每次渲染都是新函数
- 在effect中setState导致依赖变化
- 不理解依赖数组的作用
最佳解决方案:
- 使用useCallback缓存函数:
const handleSubmit = useCallback(() => {
console.log('提交');
}, []); // 真实依赖放这里
useEffect(() => {
handleSubmit();
}, [handleSubmit]);
- 使用useMemo缓存对象和数组:
const config = useMemo(() => ({
url: '/api',
method: 'POST'
}), []); // 依赖变化时才重新创建
- 拆分多个useEffect,每个处理不同逻辑
- 使用函数式更新避免依赖:
setCount(prev => prev + 1); // 不需要count作为依赖
延伸建议:
- 使用eslint-plugin-react-hooks的exhaustive-deps规则
- 理解JavaScript的闭包和引用相等性
- 使用useReducer管理复杂状态逻辑
52. Vue响应式数据丢失
用户问题(真实口语): "Vue里我直接给对象加新属性,页面为啥不更新啊?"
错误思路:
- 使用this.$forceUpdate强制更新
- 重新赋值整个对象
- 认为是Vue的bug
正确分析: Vue2响应式限制:
- 对象新增属性不是响应式的
- 数组通过索引直接修改不是响应式的
- 修改数组length不是响应式的
- 不理解Vue.set的作用
最佳解决方案:
- 使用Vue.set添加新属性:
// Vue2
this.$set(this.obj, 'newProp', value);
// 或
Vue.set(this.obj, 'newProp', value);
- 使用数组的响应式方法:
// 正确
this.arr.push(newItem);
this.arr.splice(index, 1, newItem);
// 错误
this.arr[index] = newItem; // 不触发更新
- Vue3使用Proxy,没有这个问题,但需要注意:
// Vue3直接赋值即可
this.obj.newProp = value; // 自动响应式
延伸建议:
- 理解Vue2的Object.defineProperty原理
- 升级到Vue3避免这类问题
- 使用immer或immutability-helper管理不可变数据
53. 前端路由守卫死循环
用户问题(真实口语): "我在路由守卫里判断没登录就跳转到登录页,结果一直跳转,浏览器卡死了"
错误思路:
- 减少守卫判断条件
- 使用setTimeout延迟跳转
- 认为是路由框架的bug
正确分析: 路由守卫死循环原因:
- 登录页也需要守卫判断,导致循环重定向
- 跳转目标和当前路由相同,但参数不同
- 守卫逻辑错误,总是触发跳转
- 未排除白名单路由
最佳解决方案:
- 设置白名单路由:
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'); // 未登录跳转到登录页
}
}
});
- 使用replace而不是push避免历史记录堆积
- 确保登录页不在守卫逻辑中重复触发
延伸建议:
- 使用addRoutes动态添加路由,避免权限路由硬编码
- 路由守卫逻辑尽量简单,分散到多个守卫中
- 添加loading状态,避免用户重复点击
54. 前端微应用样式隔离失败
用户问题(真实口语): "微前端子应用的样式把主应用污染了,样式全乱了,怎么隔离?"
错误思路:
- 使用!important覆盖样式
- 手动给所有样式加前缀
- 认为是微前端框架的bug
正确分析: 样式隔离失败原因:
- CSS全局污染,样式作用域冲突
- 使用了全局标签选择器(div, p等)
- CSS Modules配置错误
- Shadow DOM使用不当
最佳解决方案:
- 使用CSS Modules:
/* Button.module.css */
.button {
background: blue;
color: white;
}
import styles from './Button.module.css';
const Button = () => <button className={styles.button}>点击</button>;
- 使用styled-components:
const Button = styled.button`
background: blue;
color: white;
`;
- qiankun框架使用strictStyleIsolation:
start({
sandbox: {
strictStyleIsolation: true // 启用Shadow DOM隔离
}
});
- 使用BEM命名规范:
.app-header__button--primary { /* 模块-元素--修饰符 */
background: blue;
}
延伸建议:
- 主应用和子应用使用不同的CSS前缀
- 避免使用全局标签选择器,使用class
- 使用postcss-prefix-selector自动添加前缀
55. 前端缓存数据过期策略
用户问题(真实口语): "我把接口数据存localStorage了,但数据过期了页面还显示旧的,怎么自动清理?"
错误思路:
- 存一个固定时间,到期就清理
- 每次打开页面都清空缓存
- 认为缓存不需要过期
正确分析: 缓存过期问题原因:
- 未设置过期时间戳
- 未定期检查清理
- 缓存数据结构不合理
- 未区分不同数据的过期策略
最佳解决方案:
- 封装带过期时间的缓存:
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;
};
- 使用sessionStorage存储会话级缓存
- 使用IndexedDB存储大量结构化缓存数据
- 结合Service Worker实现更复杂的缓存策略
延伸建议:
- 使用localforage库统一API
- 实现LRU缓存淘汰策略
- 缓存版本控制,接口升级时清理旧缓存
56. 前端请求重试机制
用户问题(真实口语): "用户网络不好,接口经常失败,怎么自动重试?"
错误思路:
- 让用户手动刷新页面
- 在catch里无限重试
- 使用setInterval定时重试
正确分析: 请求重试问题原因:
- 未区分错误类型,不应该重试的也重试了
- 重试次数过多,浪费资源
- 重试间隔太短,网络来不及恢复
- 未考虑幂等性,非幂等接口重试导致数据错误
最佳解决方案:
- 使用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;
}
});
- 手动实现重试:
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延迟请求
- 认为浏览器会自动处理
正确分析: 并发请求问题原因:
- 浏览器有并发限制(通常6-8个)
- 大量请求同时发起,阻塞主线程
- 未按优先级排序
- 未使用请求池
最佳解决方案:
- 使用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);
- 按优先级分批请求:
// 先请求关键数据
const criticalData = await fetchCriticalData();
// 再请求非关键数据
const nonCriticalData = await fetchNonCriticalData();
- 使用请求队列:
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骨架
- 认为骨架屏实现复杂
正确分析: 骨架屏问题原因:
- 未在数据加载前显示占位
- 骨架屏和实际内容结构差异大
- 未根据实际内容生成骨架
- 骨架屏样式未优化
最佳解决方案:
- 使用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>
);
};
- 使用骨架屏组件库:
const CardSkeleton = () => (
<div className="skeleton-card">
<div className="skeleton-image" />
<div className="skeleton-title" />
<div className="skeleton-text" />
</div>
);
- 使用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问题原因:
- 生产环境代码压缩,无法定位原始代码
- 未上传SourceMap到监控平台
- SourceMap文件泄露风险
- 未配置正确的source-map类型
最佳解决方案:
- 配置webpack生成SourceMap:
// webpack.config.js
module.exports = {
devtool: 'hidden-source-map', // 生成source-map但不暴露
productionSourceMap: false // 不生成.map文件到生产环境
};
- 上传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
})
]
- 使用Sentry查看原始错误:
import * as Sentry from '@sentry/browser';
Sentry.init({
dsn: 'your-dsn',
release: process.env.RELEASE_VERSION
});
延伸建议:
- 使用private-sourcemap保护SourceMap
- 只在监控平台上传SourceMap,不部署到生产
- 配置错误采样,避免上报过多错误
60. 前端性能监控指标
用户问题(真实口语): "老板问我们网站性能怎么样,除了加载时间还有哪些指标要监控?"
错误思路:
- 只监控首屏加载时间
- 使用performance.now手动计算
- 认为性能就是加载速度
正确分析: 性能指标问题原因:
- 只关注加载性能,忽略运行时性能
- 未使用标准的性能指标
- 未监控用户真实体验
- 未区分不同网络环境
最佳解决方案:
- 监控Core Web Vitals:
import { getCLS, getFID, getLCP } from 'web-vitals';
getCLS(console.log); // 累积布局偏移
getFID(console.log); // 首次输入延迟
getLCP(console.log); // 最大内容绘制
- 使用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;
- 监控运行时性能:
// 长任务监控
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收集性能数据
- 按设备、网络条件分组分析
- 设置性能预算,超过阈值告警
📚 系列文章
🔗 相关链接
- GitHub仓库: awesome-frontend-problems
- 完整文档: frontend-problems-50.md
觉得有用的话,别忘了点赞 👍、收藏 ⭐、关注 👀 支持一下~