前端真实问题场景130条 - 基础问题篇 (1-30)
本文是《前端真实问题场景130条》系列的第1篇,涵盖第1-30条问题。
💡 每篇文章包含:真实用户问题 + 错误思路 + 正确分析 + 最佳解决方案 + 延伸建议
1. 页面白屏问题
用户问题(真实口语): "我页面打开是空白的,啥都没有,控制台也没报错,这咋整啊?"
错误思路:
- 认为是浏览器问题,让用户换浏览器
- 直接刷新页面或清除缓存
- 认为是后端接口问题,让后端查日志
正确分析: 页面白屏但控制台无报错,通常是JS执行到某处中断,但错误未被捕获。可能是:
- 某个JS文件加载失败
- 代码中存在未捕获的Promise错误
- 构建后的代码有语法错误
- 路由配置问题导致组件未渲染
最佳解决方案:
- 打开浏览器开发者工具 → Network面板,查看是否有资源加载失败(状态码非200)
- 在控制台输入
window.addEventListener('error', e => console.log('捕获错误:', e))重新加载页面 - 检查路由配置,确认路由路径和组件导入是否正确
- 查看构建后的主JS文件是否完整加载
延伸建议:
- 在项目中添加全局错误监控:
window.addEventListener('unhandledrejection', handler) - 使用Sentry等错误监控工具
- 在CI/CD流程中添加构建完整性检查
2. 样式不生效
用户问题(真实口语): "我写了CSS,页面样式怎么没变啊?我明明写了color: red;"
错误思路:
- 认为是浏览器缓存问题,强制刷新
- 认为是CSS语法错误,反复检查拼写
- 直接给元素加!important
正确分析: CSS不生效的常见原因:
- 选择器权重不够,被其他样式覆盖
- 样式文件未正确引入或加载顺序问题
- 使用了CSS Modules但引用方式错误
- 父元素有样式隔离(如Shadow DOM)
- 浏览器缓存了旧版本
最佳解决方案:
- 在浏览器控制台Elements面板检查元素,查看实际应用的样式
- 检查Styles面板中是否有删除线样式(被覆盖)
- 确认CSS文件是否被正确加载(Network面板)
- 检查选择器权重,计算 specificity
- 如果使用CSS Modules,确认导入方式:
import styles from './index.module.css'
延伸建议:
- 使用BEM命名规范避免样式冲突
- 配置webpack的css-loader开启local模式
- 使用浏览器开发者工具的"Force state"功能测试伪类状态
3. 接口跨域问题
用户问题(真实口语): "前端调接口报错了,说什么CORS policy,这啥意思啊?"
错误思路:
- 认为是前端代码问题,修改axios配置
- 认为是后端接口挂了
- 让后端把所有接口都改成JSONP
正确分析: CORS(跨域资源共享)是浏览器的安全策略:
- 前端域名和接口域名不一致(端口不同也算)
- 后端未正确配置Access-Control-Allow-Origin头
- 如果是带凭证的请求(withCredentials),后端不能设置为*
- 预检请求(OPTIONS)未正确处理
最佳解决方案:
- 确认前端请求的URL和后端接口域名是否一致
- 让后端在响应头中添加:
Access-Control-Allow-Origin: 你的前端域名 - 如果需要携带cookie,后端需配置:
Access-Control-Allow-Credentials: true - 开发环境可使用webpack-dev-server的proxy配置代理
延伸建议:
- 生产环境使用Nginx反向代理
- 后端配置白名单而不是*
- 了解简单请求和预检请求的区别
4. 页面闪烁(FOUC)
用户问题(真实口语): "页面刚打开的时候样式乱乱的,闪一下才正常,用户体验好差"
错误思路:
- 认为是网络慢,让用户升级带宽
- 认为是浏览器渲染问题,无法解决
- 给body加opacity过渡动画掩盖
正确分析: FOUC(Flash of Unstyled Content)原因:
- CSS文件加载延迟,HTML先渲染了
- 使用了JavaScript动态插入样式
- 服务端渲染和客户端样式不一致( hydration问题)
- 字体文件加载慢导致文字闪烁
最佳解决方案:
- 将关键CSS内联到HTML head中(Critical CSS)
- 使用rel="preload"预加载重要CSS:
<link rel="preload" href="style.css" as="style"> - 避免使用JS插入关键样式
- 字体加载使用font-display: swap;
延伸建议:
- 使用工具提取关键CSS(如critical、Penthouse)
- 配置HTTP/2服务器推送CSS文件
- 使用React.lazy和Suspense优化加载策略
5. 内存泄漏导致页面卡顿
用户问题(真实口语): "页面开久了就卡得不行,刷新一下就好了,是不是内存泄漏啊?"
错误思路:
- 认为是用户电脑配置低
- 认为是页面内容太多,无法优化
- 直接建议用户定期刷新页面
正确分析: 内存泄漏常见原因:
- 事件监听器未移除:
addEventListener后未removeEventListener - 定时器未清理:
setInterval/setTimeout未清除 - 闭包引用导致变量无法释放
- DOM引用未清理:移除DOM元素但JS仍持有引用
- 全局变量不断累积数据
最佳解决方案:
- 在组件卸载时清理:
useEffect(() => {
const handler = () => { /*...*/ };
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
- 使用Chrome DevTools的Memory面板进行堆快照分析
- 检查是否有不断增长的数组或对象
- 使用WeakMap/WeakSet存储临时数据
延伸建议:
- 使用eslint-plugin-react-hooks检测依赖项
- 在SPA路由切换时确保清理操作
- 使用Performance Monitor监控内存使用趋势
6. 移动端点击延迟
用户问题(真实口语): "手机端点击按钮要顿一下才有反应,感觉好慢啊"
错误思路:
- 认为是网络请求慢
- 认为是手机性能问题
- 给所有点击事件加setTimeout 0
正确分析: 移动端300ms点击延迟原因:
- 浏览器等待双击缩放(double-tap to zoom)
- 使用click事件而非touch事件
- 事件处理函数执行耗时过长
最佳解决方案:
- 添加viewport meta标签禁用缩放:
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> - 使用FastClick库或CSS touch-action: manipulation;
- 使用touchstart/touchend代替click(注意处理滚动冲突)
- 使用现代框架的点击组件(如React的onClick已优化)
延伸建议:
- 使用Passive Event Listeners提升滚动性能:
{ passive: true } - 避免在事件处理函数中执行耗时操作
- 使用Web Workers处理复杂计算
7. 图片加载慢
用户问题(真实口语): "页面图片加载好慢啊,一张图要转半天圈圈"
错误思路:
- 认为是用户网络问题
- 直接把所有图片改成base64内联
- 使用loading="lazy"就不管了
正确分析: 图片加载慢的原因:
- 图片文件过大,未压缩
- 一次性加载所有图片
- 使用了不合适的图片格式
- 未使用CDN加速
- 没有响应式图片适配
最佳解决方案:
- 图片压缩:使用tinypng、imagemin等工具
- 使用现代格式:WebP(兼容性90%+)、AVIF
- 实现懒加载:Intersection Observer API
- 使用srcset提供多尺寸图片:
<img srcset="small.jpg 480w, large.jpg 800w" sizes="(max-width: 600px) 480px, 800px"> - 使用CDN加速图片加载
延伸建议:
- 使用图片占位符(LQIP - Low Quality Image Placeholders)
- 使用blurhash生成模糊预览图
- 配置Service Worker缓存图片资源
8. 表单提交重复点击
用户问题(真实口语): "用户连点提交按钮,后台收到好几条重复数据,咋防止啊?"
错误思路:
- 后端做幂等性处理就完事了
- 使用disabled属性,但网络慢时用户无法重试
- 使用防抖函数,但可能导致用户无法提交
正确分析: 重复提交原因:
- 网络延迟,用户多次点击
- 按钮点击后无反馈,用户以为没生效
- 表单提交后页面未跳转或清空
最佳解决方案:
- 点击后立即禁用按钮并显示加载状态:
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async () => {
if (submitting) return;
setSubmitting(true);
try {
await api.submit();
} finally {
setSubmitting(false);
}
};
- 使用Token机制防止重复提交:后端生成唯一token,前端提交时携带
- 提供视觉反馈:loading动画、进度条
延伸建议:
- 使用axios的cancelToken取消重复请求
- 后端接口支持幂等性设计(使用唯一业务ID)
- 提交成功后重置表单并给出成功提示
9. 路由刷新404
用户问题(真实口语): "我用的React Router,直接访问某个页面或刷新就404了,本地没问题啊"
错误思路:
- 认为是路由配置错误,反复检查路由代码
- 认为是后端问题,让后端排查
- 把所有路由改成hash模式
正确分析: SPA路由刷新404原因:
- 开发环境使用webpack-dev-server已配置historyApiFallback
- 生产环境服务器未配置fallback,直接访问子路径时服务器找不到对应文件
- 浏览器向服务器请求了前端路由路径,但服务器只有index.html
最佳解决方案:
- Nginx配置:
location / {
try_files $uri $uri/ /index.html;
}
- Apache配置:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
- Node.js (Express):
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
延伸建议:
- 了解BrowserRouter和HashRouter的区别
- 配置404页面处理不存在的路由
- 使用静态网站托管服务时查看其SPA配置选项
10. 组件状态不同步
用户问题(真实口语): "我setState了,但页面没更新,打印出来数据是对的啊"
错误思路:
- 认为是React的bug
- 使用forceUpdate强制更新
- 把状态提升到全局store
正确分析: 状态不同步原因:
- 直接修改state对象属性:
state.obj.key = value - 数组直接push/pop,未返回新数组
- 异步操作后未正确处理状态更新
- 闭包导致获取到旧的状态值
最佳解决方案:
- 使用展开语法创建新对象:
// 错误
state.user.name = 'newName';
// 正确
setState({ ...state, user: { ...state.user, name: 'newName' } });
- 数组更新:
// 错误
state.list.push(newItem);
// 正确
setState({ list: [...state.list, newItem] });
- 使用函数式更新获取最新状态:
setState(prev => ({ ...prev, count: prev.count + 1 }));
延伸建议:
- 使用Immer库简化不可变数据操作
- 使用useReducer管理复杂状态逻辑
- 使用useCallback和useMemo优化性能
11. 打包体积过大
用户问题(真实口语): "我项目打包后有5MB,加载好慢,怎么优化啊?"
错误思路:
- 认为项目大就没办法
- 把所有图片都删掉
- 使用Gzip压缩就不管了
正确分析: 打包体积大的原因:
- 引入了不必要的依赖(如lodash全部引入)
- 未对代码进行分割
- 第三方库未按需加载
- 源码未压缩混淆
- 重复打包相同依赖
最佳解决方案:
- 分析打包体积:
webpack-bundle-analyzer - 按需引入:
// 错误
import _ from 'lodash';
// 正确
import debounce from 'lodash/debounce';
- 代码分割:
const LazyComponent = React.lazy(() => import('./Component'));
- 配置externals将大库通过CDN引入
- 使用Tree Shaking:确保package.json有"sideEffects": false
延伸建议:
- 使用动态import()实现路由级代码分割
- 配置splitChunks将公共库分离
- 使用Brotli压缩算法(比Gzip更小)
12. 接口请求慢
用户问题(真实口语): "接口要2秒才返回,用户体验好差,怎么优化?"
错误思路:
- 认为是后端接口问题,让后端优化
- 增加loading动画掩盖
- 把超时时间设长一点
正确分析: 接口请求慢的原因:
- 请求链路长:DNS解析、TCP握手、SSL握手
- 接口返回数据量过大
- 并发请求限制
- 未使用HTTP/2
- 前端请求策略不合理
最佳解决方案:
- 使用CDN加速域名解析
- 接口返回必要字段,避免select *
- 合并请求:GraphQL或批量接口
- 使用HTTP/2多路复用
- 实现请求缓存:
const cache = new Map();
const fetchWithCache = async (url) => {
if (cache.has(url)) return cache.get(url);
const data = await fetch(url);
cache.set(url, data);
return data;
};
延伸建议:
- 使用Service Worker实现离线缓存
- 实现接口预加载:预测用户行为提前请求
- 使用QUIC协议(HTTP/3)减少握手时间
13. SEO优化问题
用户问题(真实口语): "我们网站搜索引擎搜不到,SEO怎么搞啊?"
错误思路:
- 认为是搜索引擎的问题
- 直接买搜索引擎广告
- 在页面堆砌大量关键词
正确分析: SPA应用SEO问题:
- 搜索引擎爬虫无法执行JavaScript
- 页面内容动态渲染,爬虫抓取不到
- 缺少meta标签和结构化数据
- 页面加载速度慢影响排名
最佳解决方案:
- 使用SSR(服务端渲染):Next.js、Nuxt.js
- 预渲染:prerender-spa-plugin
- 配置meta标签:
<meta name="description" content="页面描述">
<meta property="og:title" content="分享标题">
- 添加结构化数据:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "文章标题"
}
</script>
延伸建议:
- 使用Google Search Console监控收录情况
- 生成sitemap.xml并提交搜索引擎
- 优化页面加载速度(Core Web Vitals)
14. 移动端适配问题
用户问题(真实口语): "我页面在手机上显示不全,要左右滑动才能看全"
错误思路:
- 认为是手机屏幕太小
- 直接设置固定宽度320px
- 使用缩放viewport让用户自己放大
正确分析: 移动端适配问题:
- 未设置viewport meta标签
- 使用了固定宽度布局
- 字体大小未使用相对单位
- 触摸目标过小
最佳解决方案:
- 设置viewport:
<meta name="viewport" content="width=device-width, initial-scale=1.0"> - 使用响应式布局:Flexbox/Grid
- 使用相对单位:rem、vw/vh
- 媒体查询适配不同屏幕:
@media (max-width: 768px) {
.container { padding: 10px; }
}
- 触摸目标至少44x44px
延伸建议:
- 使用postcss-px-to-viewport自动转换单位
- 使用Flexible.js或viewport-units-buggyfill兼容旧浏览器
- 在真实设备上测试,不只是浏览器模拟器
15. 浏览器兼容性问题
用户问题(真实口语): "用户说在IE11上页面打不开,一片空白,怎么兼容啊?"
错误思路:
- 让用户升级浏览器
- 认为现代框架不支持IE,放弃兼容
- 引入babel-polyfill就不管了
正确分析: IE兼容性问题:
- 不支持ES6+语法(箭头函数、async/await等)
- 不支持CSS新特性(flexbox、grid等)
- 缺少部分Web API(Promise、fetch等)
- 事件模型差异
最佳解决方案:
- 配置babel转译:
{
"presets": [
["@babel/preset-env", {
"targets": { "ie": "11" },
"useBuiltIns": "usage",
"corejs": 3
}]
]
}
- 使用polyfill:core-js、regenerator-runtime
- CSS前缀:autoprefixer
- 条件注释针对IE特殊处理:
<!--[if IE]>
<script src="ie-polyfill.js"></script>
<![endif]-->
延伸建议:
- 使用@babel/preset-env的browserslist配置
- 使用Modernizr检测特性支持
- 考虑渐进增强策略,非核心功能在IE上降级
16. 页面滚动穿透
用户问题(真实口语): "弹出层打开的时候,背后页面还能滚动,体验好差"
错误思路:
- 认为是移动端特性,无法解决
- 直接给body加overflow: hidden
- 记录滚动位置,关闭时恢复
正确分析: 滚动穿透原因:
- 弹出层未阻止事件冒泡
- body滚动未锁定
- iOS上-webkit-overflow-scrolling: touch导致
- 多个滚动容器嵌套
最佳解决方案:
- 打开弹层时:
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
- 关闭弹层时恢复
- 使用body-scroll-lock库处理iOS兼容
- 弹层内容使用独立滚动容器:
.modal-content {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
延伸建议:
- 使用React组件库(如antd-mobile)已处理此问题
- 考虑使用position: sticky替代固定定位
- 在Android上测试不同浏览器的滚动行为
17. 本地存储容量超限
用户问题(真实口语): "用户反馈网站存不了数据了,控制台报QuotaExceededError"
错误思路:
- 认为是浏览器bug
- 让用户清理浏览器缓存
- 直接使用IndexedDB存大量数据
正确分析: 本地存储限制:
- localStorage容量限制约5-10MB
- 存储大量JSON数据导致超限
- 未清理过期数据
- 同源下所有页面共享配额
最佳解决方案:
- 数据压缩:使用LZ-string等库压缩
- 分片存储:将大数据拆分成小块
- 使用IndexedDB代替localStorage:
const db = await openDB('my-db', 1, {
upgrade(db) {
db.createObjectStore('store');
},
});
await db.put('store', data, 'key');
- 定期清理过期数据:
const cleanup = () => {
Object.keys(localStorage).forEach(key => {
const item = JSON.parse(localStorage.getItem(key));
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
}
});
};
延伸建议:
- 使用localForage库统一API
- 评估数据是否真的需要本地存储
- 对敏感数据使用sessionStorage
18. WebSocket断线重连
用户问题(真实口语): "WebSocket经常断开,断开之后就不自动连,用户要手动刷新才行"
错误思路:
- 认为是网络不稳定,无法解决
- 设置一个很大的心跳间隔
- 断开后就显示错误页面
正确分析: WebSocket断线原因:
- 网络切换(WiFi/4G切换)
- 服务器超时断开
- 浏览器休眠后断开
- 未实现重连机制
最佳解决方案:
- 实现自动重连:
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectInterval = 1000;
this.maxReconnectInterval = 30000;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onclose = () => {
setTimeout(() => this.connect(), this.reconnectInterval);
this.reconnectInterval = Math.min(
this.reconnectInterval * 2,
this.maxReconnectInterval
);
};
this.ws.onopen = () => {
this.reconnectInterval = 1000;
};
}
}
- 心跳检测:
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
延伸建议:
- 使用reconnecting-websocket库
- 实现指数退避算法避免频繁重连
- 记录重连次数,过多时提示用户检查网络
19. 国际化切换闪屏
用户问题(真实口语): "切换语言的时候,页面会闪一下,先显示英文再显示中文"
错误思路:
- 认为是正常的加载过程
- 切换时给整个页面加loading
- 把语言包都提前加载好
正确分析: 语言切换闪屏原因:
- 语言包异步加载,加载完成前显示默认语言
- 未保存用户选择,刷新后恢复默认
- 组件重新渲染导致闪烁
最佳解决方案:
- 将语言包与主bundle一起打包(体积小的话)
- 使用Suspense和lazy加载语言包:
const messages = {
en: () => import('./locales/en.json'),
zh: () => import('./locales/zh.json'),
};
const App = () => {
const [locale, setLocale] = useState('en');
const messages = useMemo(() => messages[locale](), [locale]);
return (
<Suspense fallback={<div>Loading...</div>}>
<IntlProvider locale={locale} messages={messages}>
{/* app */}
</IntlProvider>
</Suspense>
);
};
- 本地存储用户选择:
const savedLocale = localStorage.getItem('locale') || 'en';
延伸建议:
- 使用react-intl或i18next管理国际化
- 根据浏览器语言自动设置默认语言
- 语言包拆分,按页面加载
20. 长列表性能问题
用户问题(真实口语): "列表有上万条数据,页面卡得不行了,滚动都费劲"
错误思路:
- 让后端做分页
- 使用loading更多,一次少加载点
- 认为数据量大就没办法
正确分析: 长列表性能问题:
- 一次性渲染太多DOM节点
- 每个节点数据量大
- 滚动时频繁重绘
- 内存占用过高
最佳解决方案:
- 虚拟滚动:react-window、react-virtualized
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={400}
itemCount={10000}
itemSize={35}
width={300}
>
{Row}
</List>
);
- 无限滚动:Intersection Observer实现
- 分页加载:每页加载固定数量
- 使用Object Pool重用DOM节点
延伸建议:
- 使用React.memo优化列表项
- 图片懒加载
- 使用Web Worker处理列表数据排序和过滤
21. 权限控制闪烁
用户问题(真实口语): "页面刚打开时,没权限的按钮先显示出来了,然后才隐藏,闪一下"
错误思路:
- 认为是网络延迟,无法避免
- 给整个页面加loading
- 把权限数据存localStorage
正确分析: 权限闪烁原因:
- 权限数据异步获取,获取完成前按默认状态显示
- 未在获取权限前隐藏相关元素
- 权限数据存储在客户端,刷新后需要重新获取
最佳解决方案:
- 在权限数据加载完成前不渲染相关元素:
const [permissions, setPermissions] = useState(null);
if (!permissions) return <Loading />;
return (
<div>
{permissions.includes('edit') && <Button>Edit</Button>}
</div>
);
- 使用Suspense:
const permissionsResource = fetchPermissions();
const Permissions = () => {
const permissions = permissionsResource.read();
return permissions.includes('edit') ? <Button>Edit</Button> : null;
};
- 服务端返回HTML时注入权限数据
延伸建议:
- 使用React的useContext管理权限状态
- 封装Permission组件统一处理
- 权限数据缓存,但需设置过期时间
22. 表单验证体验差
用户问题(真实口语): "表单提交后才告诉用户哪里填错了,体验不好,能不能边填边验证?"
错误思路:
- 认为实时验证影响性能
- 只在提交时验证
- 用户输入就立即验证,导致频繁报错
正确分析: 表单验证体验问题:
- 验证时机不合理,输入过程中频繁报错
- 错误提示不明显
- 未对输入进行防抖处理
- 异步验证(如用户名唯一性)处理不当
最佳解决方案:
- 失焦时验证:onBlur触发
- 输入防抖:延迟300ms后验证
const [value, setValue] = useState('');
const [error, setError] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
if (value.length < 6) {
setError('至少6个字符');
} else {
setError('');
}
}, 300);
return () => clearTimeout(timer);
}, [value]);
- 使用表单库:Formik、react-hook-form
- 异步验证:
const validateUsername = async (value) => {
const exists = await checkUsername(value);
return exists ? '用户名已存在' : undefined;
};
延伸建议:
- 使用yup或joi定义验证规则
- 提供实时密码强度提示
- 验证通过时显示成功状态
23. 弹窗层级问题
用户问题(真实口语): "弹窗里再打开弹窗,后面的弹窗被遮住了,z-index设了9999都不管用"
错误思路:
- 不断增加z-index值
- 认为是浏览器bug
- 把所有弹窗z-index都设成一样
正确分析: 弹窗层级问题:
- 父元素创建了新的层叠上下文(stacking context)
- z-index只在同一层叠上下文内有效
- 多个弹窗不在同一DOM层级
最佳解决方案:
- 使用React Portal将弹窗渲染到body下:
import { createPortal } from 'react-dom';
const Modal = ({ children }) => {
return createPortal(
<div className="modal">{children}</div>,
document.body
);
};
- 统一管理z-index:
const zIndex = {
modal: 1000,
drawer: 900,
tooltip: 800,
};
- 使用z-index自动递增:
let zIndexCounter = 1000;
const getNextZIndex = () => ++zIndexCounter;
延伸建议:
- 使用Modal组件库(如antd、Material-UI)
- 避免在弹窗内再嵌套弹窗,使用步骤条替代
- 使用CSS的isolation属性隔离层叠上下文
24. 页面水印被删除
用户问题(真实口语): "我们页面加了水印,但用户用开发者工具直接删掉了,怎么防止?"
错误思路:
- 认为前端无法防止
- 使用MutationObserver监控
- 把水印做成图片背景
正确分析: 水印被删除原因:
- 水印是DOM元素,可被用户删除
- 前端验证不可靠
- 用户可禁用JavaScript
最佳解决方案:
- 使用Canvas绘制水印:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '20px Arial';
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillText('Watermark', 50, 50);
document.body.style.backgroundImage = `url(${canvas.toDataURL()})`;
- 使用CSS的user-select: none;
- 服务端生成带水印的图片或PDF
- 使用WebGL渲染关键内容
延伸建议:
- 前端水印只是威慑,重要信息需要后端验证
- 使用Shadow DOM增加删除难度
- 定期检测水印是否存在,不存在时重新生成
25. 日期格式化不一致
用户问题(真实口语): "日期显示有的格式是2024-01-01,有的是2024/01/01,还有的是01-01-2024,好乱"
错误思路:
- 认为不同页面可以不同格式
- 每个地方手动格式化
- 使用toLocaleDateString简单处理
正确分析: 日期格式混乱原因:
- 未统一日期格式化工具
- 不同开发者使用不同方式
- 未考虑国际化
- 后端返回格式不统一
最佳解决方案:
- 使用统一日期库:dayjs、date-fns
import dayjs from 'dayjs';
const formatDate = (date) => dayjs(date).format('YYYY-MM-DD');
- 封装日期格式化组件:
const DateDisplay = ({ value, format = 'YYYY-MM-DD' }) => (
<span>{dayjs(value).format(format)}</span>
);
- 根据用户地区自动格式化:
const format = new Intl.DateTimeFormat(navigator.language).format;
延伸建议:
- 使用dayjs的locale支持多语言
- 后端统一返回ISO 8601格式(YYYY-MM-DDTHH:mm:ss.sssZ)
- 封装useDateFormat Hook
26. 页面标题动态修改
用户问题(真实口语): "我们SPA应用,浏览器标签页的标题不会变,一直都是网站名称"
错误思路:
- 认为是SPA的局限
- 在index.html写死标题
- 每个页面手动document.title = 'xxx'
正确分析: 页面标题不变化原因:
- SPA路由切换时不触发页面重新加载
- 未在路由变化时更新document.title
- 搜索引擎无法获取动态标题
最佳解决方案:
- 使用React Helmet管理文档头:
import { Helmet } from 'react-helmet';
const Page = () => (
<>
<Helmet>
<title>页面标题 - 网站名称</title>
<meta name="description" content="页面描述" />
</Helmet>
{/* page content */}
</>
);
- 在路由配置中定义标题:
const routes = [
{
path: '/home',
component: Home,
title: '首页'
}
];
// 路由守卫中设置
document.title = route.title;
- 服务端渲染时注入标题
延伸建议:
- 使用react-helmet-async支持SSR
- 根据路由动态生成标题模板
- 在浏览器历史记录中显示正确标题
27. 页面 favicon 不显示
用户问题(真实口语): "网站图标不显示,刷新好几次才出来,怎么回事?"
错误思路:
- 认为是浏览器缓存问题
- 把favicon.ico放根目录就不管了
- 使用base64编码内联
正确分析: favicon不显示原因:
- 浏览器缓存了旧的favicon
- 未正确配置link标签
- favicon文件路径错误或文件损坏
- 使用了相对路径,在不同页面访问时路径不一致
最佳解决方案:
- 在HTML head中正确配置:
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
- 使用绝对路径,避免相对路径问题
- 添加版本号强制更新:
<link rel="icon" href="/favicon.ico?v=2">
- 使用在线favicon生成工具确保文件格式正确
- 配置服务器返回正确的Content-Type:image/x-icon
延伸建议:
- 使用SVG格式的favicon,支持高分辨率
- 为不同设备提供不同尺寸的图标
- 使用favicon.io等工具生成完整的图标集
28. 文件上传进度显示
用户问题(真实口语): "上传大文件的时候,用户不知道进度,以为卡死了,怎么显示上传进度?"
错误思路:
- 认为是后端问题,让后端返回进度
- 只显示loading动画
- 使用setTimeout模拟进度
正确分析: 文件上传进度问题:
- 未使用XMLHttpRequest的upload.onprogress
- 使用了fetch API但未配置进度回调
- 未给用户视觉反馈
- 上传失败后未提示用户
最佳解决方案:
- 使用axios的onUploadProgress:
const uploadFile = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
await axios.post('/upload', formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percentCompleted);
}
});
};
- 显示进度条:
const [progress, setProgress] = useState(0);
const handleUpload = async (file) => {
await uploadFile(file, (percent) => {
setProgress(percent);
});
};
- 使用XMLHttpRequest:
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
const progress = (e.loaded / e.total) * 100;
updateProgress(progress);
};
延伸建议:
- 大文件分片上传,显示分片进度
- 支持断点续传
- 上传失败后提供重试功能
29. 页面打印样式问题
用户问题(真实口语): "用户打印页面的时候,样式全乱了,背景色也没了,怎么优化打印效果?"
错误思路:
- 认为是浏览器打印功能问题
- 让用户截图打印
- 直接打印网页,不做任何处理
正确分析: 打印样式问题:
- 未使用@media print媒体查询
- 打印时默认不打印背景色
- 页面布局不适合打印(A4纸张)
- 打印了不需要的元素(导航、按钮等)
最佳解决方案:
- 使用打印媒体查询:
@media print {
/* 隐藏不需要的元素 */
.no-print {
display: none !important;
}
/* 设置打印背景色 */
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* 调整页面布局 */
.container {
width: 100%;
max-width: none;
}
/* 分页控制 */
.page-break {
page-break-after: always;
}
}
- 添加打印按钮:
const handlePrint = () => {
window.print();
};
- 使用专门的打印样式表:
<link rel="stylesheet" href="print.css" media="print">
延伸建议:
- 使用print.js库提供更多打印选项
- 生成PDF供用户下载打印
- 提供打印预览功能
30. 页面性能监控
用户问题(真实口语): "用户反馈页面慢,但我们本地测试很快,怎么监控真实用户的性能?"
错误思路:
- 只在开发环境测试性能
- 让用户截图反馈
- 使用console.time手动计时
正确分析: 性能监控缺失原因:
- 未使用性能监控工具
- 只关注首屏加载,忽略交互性能
- 未收集真实用户数据
- 性能数据未可视化
最佳解决方案:
- 使用Web Vitals监控核心指标:
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
- 使用Performance API:
const perfData = performance.getEntriesByType('navigation')[0];
const pageLoadTime = perfData.loadEventEnd - perfData.fetchStart;
- 集成监控平台:Sentry、DataDog、New Relic
- 自定义性能埋点:
const measurePerformance = (name, fn) => {
const start = performance.now();
const result = fn();
const end = performance.now();
console.log(`${name} took ${end - start}ms`);
return result;
};
延伸建议:
- 使用Lighthouse CI自动化性能测试
- 设置性能告警阈值
- 定期分析性能报告并优化
📚 系列文章
🔗 相关链接
- GitHub仓库: awesome-frontend-problems
- 完整文档: frontend-problems-50.md
觉得有用的话,别忘了点赞 👍、收藏 ⭐、关注 👀 支持一下~