前端真实问题场景

7 阅读20分钟

前端真实问题场景130条 - 基础问题篇 (1-30)

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

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


1. 页面白屏问题

用户问题(真实口语): "我页面打开是空白的,啥都没有,控制台也没报错,这咋整啊?"

错误思路:

  • 认为是浏览器问题,让用户换浏览器
  • 直接刷新页面或清除缓存
  • 认为是后端接口问题,让后端查日志

正确分析: 页面白屏但控制台无报错,通常是JS执行到某处中断,但错误未被捕获。可能是:

  1. 某个JS文件加载失败
  2. 代码中存在未捕获的Promise错误
  3. 构建后的代码有语法错误
  4. 路由配置问题导致组件未渲染

最佳解决方案:

  1. 打开浏览器开发者工具 → Network面板,查看是否有资源加载失败(状态码非200)
  2. 在控制台输入 window.addEventListener('error', e => console.log('捕获错误:', e)) 重新加载页面
  3. 检查路由配置,确认路由路径和组件导入是否正确
  4. 查看构建后的主JS文件是否完整加载

延伸建议:

  • 在项目中添加全局错误监控:window.addEventListener('unhandledrejection', handler)
  • 使用Sentry等错误监控工具
  • 在CI/CD流程中添加构建完整性检查

2. 样式不生效

用户问题(真实口语): "我写了CSS,页面样式怎么没变啊?我明明写了color: red;"

错误思路:

  • 认为是浏览器缓存问题,强制刷新
  • 认为是CSS语法错误,反复检查拼写
  • 直接给元素加!important

正确分析: CSS不生效的常见原因:

  1. 选择器权重不够,被其他样式覆盖
  2. 样式文件未正确引入或加载顺序问题
  3. 使用了CSS Modules但引用方式错误
  4. 父元素有样式隔离(如Shadow DOM)
  5. 浏览器缓存了旧版本

最佳解决方案:

  1. 在浏览器控制台Elements面板检查元素,查看实际应用的样式
  2. 检查Styles面板中是否有删除线样式(被覆盖)
  3. 确认CSS文件是否被正确加载(Network面板)
  4. 检查选择器权重,计算 specificity
  5. 如果使用CSS Modules,确认导入方式:import styles from './index.module.css'

延伸建议:

  • 使用BEM命名规范避免样式冲突
  • 配置webpack的css-loader开启local模式
  • 使用浏览器开发者工具的"Force state"功能测试伪类状态

3. 接口跨域问题

用户问题(真实口语): "前端调接口报错了,说什么CORS policy,这啥意思啊?"

错误思路:

  • 认为是前端代码问题,修改axios配置
  • 认为是后端接口挂了
  • 让后端把所有接口都改成JSONP

正确分析: CORS(跨域资源共享)是浏览器的安全策略:

  1. 前端域名和接口域名不一致(端口不同也算)
  2. 后端未正确配置Access-Control-Allow-Origin头
  3. 如果是带凭证的请求(withCredentials),后端不能设置为*
  4. 预检请求(OPTIONS)未正确处理

最佳解决方案:

  1. 确认前端请求的URL和后端接口域名是否一致
  2. 让后端在响应头中添加:Access-Control-Allow-Origin: 你的前端域名
  3. 如果需要携带cookie,后端需配置:Access-Control-Allow-Credentials: true
  4. 开发环境可使用webpack-dev-server的proxy配置代理

延伸建议:

  • 生产环境使用Nginx反向代理
  • 后端配置白名单而不是*
  • 了解简单请求和预检请求的区别

4. 页面闪烁(FOUC)

用户问题(真实口语): "页面刚打开的时候样式乱乱的,闪一下才正常,用户体验好差"

错误思路:

  • 认为是网络慢,让用户升级带宽
  • 认为是浏览器渲染问题,无法解决
  • 给body加opacity过渡动画掩盖

正确分析: FOUC(Flash of Unstyled Content)原因:

  1. CSS文件加载延迟,HTML先渲染了
  2. 使用了JavaScript动态插入样式
  3. 服务端渲染和客户端样式不一致( hydration问题)
  4. 字体文件加载慢导致文字闪烁

最佳解决方案:

  1. 将关键CSS内联到HTML head中(Critical CSS)
  2. 使用rel="preload"预加载重要CSS:<link rel="preload" href="style.css" as="style">
  3. 避免使用JS插入关键样式
  4. 字体加载使用font-display: swap;

延伸建议:

  • 使用工具提取关键CSS(如critical、Penthouse)
  • 配置HTTP/2服务器推送CSS文件
  • 使用React.lazy和Suspense优化加载策略

5. 内存泄漏导致页面卡顿

用户问题(真实口语): "页面开久了就卡得不行,刷新一下就好了,是不是内存泄漏啊?"

错误思路:

  • 认为是用户电脑配置低
  • 认为是页面内容太多,无法优化
  • 直接建议用户定期刷新页面

正确分析: 内存泄漏常见原因:

  1. 事件监听器未移除:addEventListener后未removeEventListener
  2. 定时器未清理:setInterval/setTimeout未清除
  3. 闭包引用导致变量无法释放
  4. DOM引用未清理:移除DOM元素但JS仍持有引用
  5. 全局变量不断累积数据

最佳解决方案:

  1. 在组件卸载时清理:
useEffect(() => {
  const handler = () => { /*...*/ };
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);
  1. 使用Chrome DevTools的Memory面板进行堆快照分析
  2. 检查是否有不断增长的数组或对象
  3. 使用WeakMap/WeakSet存储临时数据

延伸建议:

  • 使用eslint-plugin-react-hooks检测依赖项
  • 在SPA路由切换时确保清理操作
  • 使用Performance Monitor监控内存使用趋势

6. 移动端点击延迟

用户问题(真实口语): "手机端点击按钮要顿一下才有反应,感觉好慢啊"

错误思路:

  • 认为是网络请求慢
  • 认为是手机性能问题
  • 给所有点击事件加setTimeout 0

正确分析: 移动端300ms点击延迟原因:

  1. 浏览器等待双击缩放(double-tap to zoom)
  2. 使用click事件而非touch事件
  3. 事件处理函数执行耗时过长

最佳解决方案:

  1. 添加viewport meta标签禁用缩放:<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  2. 使用FastClick库或CSS touch-action: manipulation;
  3. 使用touchstart/touchend代替click(注意处理滚动冲突)
  4. 使用现代框架的点击组件(如React的onClick已优化)

延伸建议:

  • 使用Passive Event Listeners提升滚动性能:{ passive: true }
  • 避免在事件处理函数中执行耗时操作
  • 使用Web Workers处理复杂计算

7. 图片加载慢

用户问题(真实口语): "页面图片加载好慢啊,一张图要转半天圈圈"

错误思路:

  • 认为是用户网络问题
  • 直接把所有图片改成base64内联
  • 使用loading="lazy"就不管了

正确分析: 图片加载慢的原因:

  1. 图片文件过大,未压缩
  2. 一次性加载所有图片
  3. 使用了不合适的图片格式
  4. 未使用CDN加速
  5. 没有响应式图片适配

最佳解决方案:

  1. 图片压缩:使用tinypng、imagemin等工具
  2. 使用现代格式:WebP(兼容性90%+)、AVIF
  3. 实现懒加载:Intersection Observer API
  4. 使用srcset提供多尺寸图片:<img srcset="small.jpg 480w, large.jpg 800w" sizes="(max-width: 600px) 480px, 800px">
  5. 使用CDN加速图片加载

延伸建议:

  • 使用图片占位符(LQIP - Low Quality Image Placeholders)
  • 使用blurhash生成模糊预览图
  • 配置Service Worker缓存图片资源

8. 表单提交重复点击

用户问题(真实口语): "用户连点提交按钮,后台收到好几条重复数据,咋防止啊?"

错误思路:

  • 后端做幂等性处理就完事了
  • 使用disabled属性,但网络慢时用户无法重试
  • 使用防抖函数,但可能导致用户无法提交

正确分析: 重复提交原因:

  1. 网络延迟,用户多次点击
  2. 按钮点击后无反馈,用户以为没生效
  3. 表单提交后页面未跳转或清空

最佳解决方案:

  1. 点击后立即禁用按钮并显示加载状态:
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async () => {
  if (submitting) return;
  setSubmitting(true);
  try {
    await api.submit();
  } finally {
    setSubmitting(false);
  }
};
  1. 使用Token机制防止重复提交:后端生成唯一token,前端提交时携带
  2. 提供视觉反馈:loading动画、进度条

延伸建议:

  • 使用axios的cancelToken取消重复请求
  • 后端接口支持幂等性设计(使用唯一业务ID)
  • 提交成功后重置表单并给出成功提示

9. 路由刷新404

用户问题(真实口语): "我用的React Router,直接访问某个页面或刷新就404了,本地没问题啊"

错误思路:

  • 认为是路由配置错误,反复检查路由代码
  • 认为是后端问题,让后端排查
  • 把所有路由改成hash模式

正确分析: SPA路由刷新404原因:

  1. 开发环境使用webpack-dev-server已配置historyApiFallback
  2. 生产环境服务器未配置fallback,直接访问子路径时服务器找不到对应文件
  3. 浏览器向服务器请求了前端路由路径,但服务器只有index.html

最佳解决方案:

  1. Nginx配置:
location / {
  try_files $uri $uri/ /index.html;
}
  1. Apache配置:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
  1. 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

正确分析: 状态不同步原因:

  1. 直接修改state对象属性:state.obj.key = value
  2. 数组直接push/pop,未返回新数组
  3. 异步操作后未正确处理状态更新
  4. 闭包导致获取到旧的状态值

最佳解决方案:

  1. 使用展开语法创建新对象:
// 错误
state.user.name = 'newName';

// 正确
setState({ ...state, user: { ...state.user, name: 'newName' } });
  1. 数组更新:
// 错误
state.list.push(newItem);

// 正确
setState({ list: [...state.list, newItem] });
  1. 使用函数式更新获取最新状态:setState(prev => ({ ...prev, count: prev.count + 1 }));

延伸建议:

  • 使用Immer库简化不可变数据操作
  • 使用useReducer管理复杂状态逻辑
  • 使用useCallback和useMemo优化性能

11. 打包体积过大

用户问题(真实口语): "我项目打包后有5MB,加载好慢,怎么优化啊?"

错误思路:

  • 认为项目大就没办法
  • 把所有图片都删掉
  • 使用Gzip压缩就不管了

正确分析: 打包体积大的原因:

  1. 引入了不必要的依赖(如lodash全部引入)
  2. 未对代码进行分割
  3. 第三方库未按需加载
  4. 源码未压缩混淆
  5. 重复打包相同依赖

最佳解决方案:

  1. 分析打包体积:webpack-bundle-analyzer
  2. 按需引入:
// 错误
import _ from 'lodash';

// 正确
import debounce from 'lodash/debounce';
  1. 代码分割:
const LazyComponent = React.lazy(() => import('./Component'));
  1. 配置externals将大库通过CDN引入
  2. 使用Tree Shaking:确保package.json有"sideEffects": false

延伸建议:

  • 使用动态import()实现路由级代码分割
  • 配置splitChunks将公共库分离
  • 使用Brotli压缩算法(比Gzip更小)

12. 接口请求慢

用户问题(真实口语): "接口要2秒才返回,用户体验好差,怎么优化?"

错误思路:

  • 认为是后端接口问题,让后端优化
  • 增加loading动画掩盖
  • 把超时时间设长一点

正确分析: 接口请求慢的原因:

  1. 请求链路长:DNS解析、TCP握手、SSL握手
  2. 接口返回数据量过大
  3. 并发请求限制
  4. 未使用HTTP/2
  5. 前端请求策略不合理

最佳解决方案:

  1. 使用CDN加速域名解析
  2. 接口返回必要字段,避免select *
  3. 合并请求:GraphQL或批量接口
  4. 使用HTTP/2多路复用
  5. 实现请求缓存:
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问题:

  1. 搜索引擎爬虫无法执行JavaScript
  2. 页面内容动态渲染,爬虫抓取不到
  3. 缺少meta标签和结构化数据
  4. 页面加载速度慢影响排名

最佳解决方案:

  1. 使用SSR(服务端渲染):Next.js、Nuxt.js
  2. 预渲染:prerender-spa-plugin
  3. 配置meta标签:
<meta name="description" content="页面描述">
<meta property="og:title" content="分享标题">
  1. 添加结构化数据:
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "文章标题"
}
</script>

延伸建议:

  • 使用Google Search Console监控收录情况
  • 生成sitemap.xml并提交搜索引擎
  • 优化页面加载速度(Core Web Vitals)

14. 移动端适配问题

用户问题(真实口语): "我页面在手机上显示不全,要左右滑动才能看全"

错误思路:

  • 认为是手机屏幕太小
  • 直接设置固定宽度320px
  • 使用缩放viewport让用户自己放大

正确分析: 移动端适配问题:

  1. 未设置viewport meta标签
  2. 使用了固定宽度布局
  3. 字体大小未使用相对单位
  4. 触摸目标过小

最佳解决方案:

  1. 设置viewport:<meta name="viewport" content="width=device-width, initial-scale=1.0">
  2. 使用响应式布局:Flexbox/Grid
  3. 使用相对单位:rem、vw/vh
  4. 媒体查询适配不同屏幕:
@media (max-width: 768px) {
  .container { padding: 10px; }
}
  1. 触摸目标至少44x44px

延伸建议:

  • 使用postcss-px-to-viewport自动转换单位
  • 使用Flexible.js或viewport-units-buggyfill兼容旧浏览器
  • 在真实设备上测试,不只是浏览器模拟器

15. 浏览器兼容性问题

用户问题(真实口语): "用户说在IE11上页面打不开,一片空白,怎么兼容啊?"

错误思路:

  • 让用户升级浏览器
  • 认为现代框架不支持IE,放弃兼容
  • 引入babel-polyfill就不管了

正确分析: IE兼容性问题:

  1. 不支持ES6+语法(箭头函数、async/await等)
  2. 不支持CSS新特性(flexbox、grid等)
  3. 缺少部分Web API(Promise、fetch等)
  4. 事件模型差异

最佳解决方案:

  1. 配置babel转译:
{
  "presets": [
    ["@babel/preset-env", {
      "targets": { "ie": "11" },
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ]
}
  1. 使用polyfill:core-js、regenerator-runtime
  2. CSS前缀:autoprefixer
  3. 条件注释针对IE特殊处理:
<!--[if IE]>
  <script src="ie-polyfill.js"></script>
<![endif]-->

延伸建议:

  • 使用@babel/preset-env的browserslist配置
  • 使用Modernizr检测特性支持
  • 考虑渐进增强策略,非核心功能在IE上降级

16. 页面滚动穿透

用户问题(真实口语): "弹出层打开的时候,背后页面还能滚动,体验好差"

错误思路:

  • 认为是移动端特性,无法解决
  • 直接给body加overflow: hidden
  • 记录滚动位置,关闭时恢复

正确分析: 滚动穿透原因:

  1. 弹出层未阻止事件冒泡
  2. body滚动未锁定
  3. iOS上-webkit-overflow-scrolling: touch导致
  4. 多个滚动容器嵌套

最佳解决方案:

  1. 打开弹层时:
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
  1. 关闭弹层时恢复
  2. 使用body-scroll-lock库处理iOS兼容
  3. 弹层内容使用独立滚动容器:
.modal-content {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

延伸建议:

  • 使用React组件库(如antd-mobile)已处理此问题
  • 考虑使用position: sticky替代固定定位
  • 在Android上测试不同浏览器的滚动行为

17. 本地存储容量超限

用户问题(真实口语): "用户反馈网站存不了数据了,控制台报QuotaExceededError"

错误思路:

  • 认为是浏览器bug
  • 让用户清理浏览器缓存
  • 直接使用IndexedDB存大量数据

正确分析: 本地存储限制:

  1. localStorage容量限制约5-10MB
  2. 存储大量JSON数据导致超限
  3. 未清理过期数据
  4. 同源下所有页面共享配额

最佳解决方案:

  1. 数据压缩:使用LZ-string等库压缩
  2. 分片存储:将大数据拆分成小块
  3. 使用IndexedDB代替localStorage:
const db = await openDB('my-db', 1, {
  upgrade(db) {
    db.createObjectStore('store');
  },
});
await db.put('store', data, 'key');
  1. 定期清理过期数据:
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断线原因:

  1. 网络切换(WiFi/4G切换)
  2. 服务器超时断开
  3. 浏览器休眠后断开
  4. 未实现重连机制

最佳解决方案:

  1. 实现自动重连:
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;
    };
  }
}
  1. 心跳检测:
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
}, 30000);

延伸建议:

  • 使用reconnecting-websocket库
  • 实现指数退避算法避免频繁重连
  • 记录重连次数,过多时提示用户检查网络

19. 国际化切换闪屏

用户问题(真实口语): "切换语言的时候,页面会闪一下,先显示英文再显示中文"

错误思路:

  • 认为是正常的加载过程
  • 切换时给整个页面加loading
  • 把语言包都提前加载好

正确分析: 语言切换闪屏原因:

  1. 语言包异步加载,加载完成前显示默认语言
  2. 未保存用户选择,刷新后恢复默认
  3. 组件重新渲染导致闪烁

最佳解决方案:

  1. 将语言包与主bundle一起打包(体积小的话)
  2. 使用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>
  );
};
  1. 本地存储用户选择:
const savedLocale = localStorage.getItem('locale') || 'en';

延伸建议:

  • 使用react-intl或i18next管理国际化
  • 根据浏览器语言自动设置默认语言
  • 语言包拆分,按页面加载

20. 长列表性能问题

用户问题(真实口语): "列表有上万条数据,页面卡得不行了,滚动都费劲"

错误思路:

  • 让后端做分页
  • 使用loading更多,一次少加载点
  • 认为数据量大就没办法

正确分析: 长列表性能问题:

  1. 一次性渲染太多DOM节点
  2. 每个节点数据量大
  3. 滚动时频繁重绘
  4. 内存占用过高

最佳解决方案:

  1. 虚拟滚动: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>
);
  1. 无限滚动:Intersection Observer实现
  2. 分页加载:每页加载固定数量
  3. 使用Object Pool重用DOM节点

延伸建议:

  • 使用React.memo优化列表项
  • 图片懒加载
  • 使用Web Worker处理列表数据排序和过滤

21. 权限控制闪烁

用户问题(真实口语): "页面刚打开时,没权限的按钮先显示出来了,然后才隐藏,闪一下"

错误思路:

  • 认为是网络延迟,无法避免
  • 给整个页面加loading
  • 把权限数据存localStorage

正确分析: 权限闪烁原因:

  1. 权限数据异步获取,获取完成前按默认状态显示
  2. 未在获取权限前隐藏相关元素
  3. 权限数据存储在客户端,刷新后需要重新获取

最佳解决方案:

  1. 在权限数据加载完成前不渲染相关元素:
const [permissions, setPermissions] = useState(null);

if (!permissions) return <Loading />;

return (
  <div>
    {permissions.includes('edit') && <Button>Edit</Button>}
  </div>
);
  1. 使用Suspense:
const permissionsResource = fetchPermissions();

const Permissions = () => {
  const permissions = permissionsResource.read();
  return permissions.includes('edit') ? <Button>Edit</Button> : null;
};
  1. 服务端返回HTML时注入权限数据

延伸建议:

  • 使用React的useContext管理权限状态
  • 封装Permission组件统一处理
  • 权限数据缓存,但需设置过期时间

22. 表单验证体验差

用户问题(真实口语): "表单提交后才告诉用户哪里填错了,体验不好,能不能边填边验证?"

错误思路:

  • 认为实时验证影响性能
  • 只在提交时验证
  • 用户输入就立即验证,导致频繁报错

正确分析: 表单验证体验问题:

  1. 验证时机不合理,输入过程中频繁报错
  2. 错误提示不明显
  3. 未对输入进行防抖处理
  4. 异步验证(如用户名唯一性)处理不当

最佳解决方案:

  1. 失焦时验证:onBlur触发
  2. 输入防抖:延迟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]);
  1. 使用表单库:Formik、react-hook-form
  2. 异步验证:
const validateUsername = async (value) => {
  const exists = await checkUsername(value);
  return exists ? '用户名已存在' : undefined;
};

延伸建议:

  • 使用yup或joi定义验证规则
  • 提供实时密码强度提示
  • 验证通过时显示成功状态

23. 弹窗层级问题

用户问题(真实口语): "弹窗里再打开弹窗,后面的弹窗被遮住了,z-index设了9999都不管用"

错误思路:

  • 不断增加z-index值
  • 认为是浏览器bug
  • 把所有弹窗z-index都设成一样

正确分析: 弹窗层级问题:

  1. 父元素创建了新的层叠上下文(stacking context)
  2. z-index只在同一层叠上下文内有效
  3. 多个弹窗不在同一DOM层级

最佳解决方案:

  1. 使用React Portal将弹窗渲染到body下:
import { createPortal } from 'react-dom';

const Modal = ({ children }) => {
  return createPortal(
    <div className="modal">{children}</div>,
    document.body
  );
};
  1. 统一管理z-index:
const zIndex = {
  modal: 1000,
  drawer: 900,
  tooltip: 800,
};
  1. 使用z-index自动递增:
let zIndexCounter = 1000;
const getNextZIndex = () => ++zIndexCounter;

延伸建议:

  • 使用Modal组件库(如antd、Material-UI)
  • 避免在弹窗内再嵌套弹窗,使用步骤条替代
  • 使用CSS的isolation属性隔离层叠上下文

24. 页面水印被删除

用户问题(真实口语): "我们页面加了水印,但用户用开发者工具直接删掉了,怎么防止?"

错误思路:

  • 认为前端无法防止
  • 使用MutationObserver监控
  • 把水印做成图片背景

正确分析: 水印被删除原因:

  1. 水印是DOM元素,可被用户删除
  2. 前端验证不可靠
  3. 用户可禁用JavaScript

最佳解决方案:

  1. 使用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()})`;
  1. 使用CSS的user-select: none;
  2. 服务端生成带水印的图片或PDF
  3. 使用WebGL渲染关键内容

延伸建议:

  • 前端水印只是威慑,重要信息需要后端验证
  • 使用Shadow DOM增加删除难度
  • 定期检测水印是否存在,不存在时重新生成

25. 日期格式化不一致

用户问题(真实口语): "日期显示有的格式是2024-01-01,有的是2024/01/01,还有的是01-01-2024,好乱"

错误思路:

  • 认为不同页面可以不同格式
  • 每个地方手动格式化
  • 使用toLocaleDateString简单处理

正确分析: 日期格式混乱原因:

  1. 未统一日期格式化工具
  2. 不同开发者使用不同方式
  3. 未考虑国际化
  4. 后端返回格式不统一

最佳解决方案:

  1. 使用统一日期库:dayjs、date-fns
import dayjs from 'dayjs';

const formatDate = (date) => dayjs(date).format('YYYY-MM-DD');
  1. 封装日期格式化组件:
const DateDisplay = ({ value, format = 'YYYY-MM-DD' }) => (
  <span>{dayjs(value).format(format)}</span>
);
  1. 根据用户地区自动格式化:
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'

正确分析: 页面标题不变化原因:

  1. SPA路由切换时不触发页面重新加载
  2. 未在路由变化时更新document.title
  3. 搜索引擎无法获取动态标题

最佳解决方案:

  1. 使用React Helmet管理文档头:
import { Helmet } from 'react-helmet';

const Page = () => (
  <>
    <Helmet>
      <title>页面标题 - 网站名称</title>
      <meta name="description" content="页面描述" />
    </Helmet>
    {/* page content */}
  </>
);
  1. 在路由配置中定义标题:
const routes = [
  {
    path: '/home',
    component: Home,
    title: '首页'
  }
];

// 路由守卫中设置
document.title = route.title;
  1. 服务端渲染时注入标题

延伸建议:

  • 使用react-helmet-async支持SSR
  • 根据路由动态生成标题模板
  • 在浏览器历史记录中显示正确标题

27. 页面 favicon 不显示

用户问题(真实口语): "网站图标不显示,刷新好几次才出来,怎么回事?"

错误思路:

  • 认为是浏览器缓存问题
  • 把favicon.ico放根目录就不管了
  • 使用base64编码内联

正确分析: favicon不显示原因:

  1. 浏览器缓存了旧的favicon
  2. 未正确配置link标签
  3. favicon文件路径错误或文件损坏
  4. 使用了相对路径,在不同页面访问时路径不一致

最佳解决方案:

  1. 在HTML head中正确配置:
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
  1. 使用绝对路径,避免相对路径问题
  2. 添加版本号强制更新:
<link rel="icon" href="/favicon.ico?v=2">
  1. 使用在线favicon生成工具确保文件格式正确
  2. 配置服务器返回正确的Content-Type:image/x-icon

延伸建议:

  • 使用SVG格式的favicon,支持高分辨率
  • 为不同设备提供不同尺寸的图标
  • 使用favicon.io等工具生成完整的图标集

28. 文件上传进度显示

用户问题(真实口语): "上传大文件的时候,用户不知道进度,以为卡死了,怎么显示上传进度?"

错误思路:

  • 认为是后端问题,让后端返回进度
  • 只显示loading动画
  • 使用setTimeout模拟进度

正确分析: 文件上传进度问题:

  1. 未使用XMLHttpRequest的upload.onprogress
  2. 使用了fetch API但未配置进度回调
  3. 未给用户视觉反馈
  4. 上传失败后未提示用户

最佳解决方案:

  1. 使用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);
    }
  });
};
  1. 显示进度条:
const [progress, setProgress] = useState(0);

const handleUpload = async (file) => {
  await uploadFile(file, (percent) => {
    setProgress(percent);
  });
};
  1. 使用XMLHttpRequest:
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
  const progress = (e.loaded / e.total) * 100;
  updateProgress(progress);
};

延伸建议:

  • 大文件分片上传,显示分片进度
  • 支持断点续传
  • 上传失败后提供重试功能

29. 页面打印样式问题

用户问题(真实口语): "用户打印页面的时候,样式全乱了,背景色也没了,怎么优化打印效果?"

错误思路:

  • 认为是浏览器打印功能问题
  • 让用户截图打印
  • 直接打印网页,不做任何处理

正确分析: 打印样式问题:

  1. 未使用@media print媒体查询
  2. 打印时默认不打印背景色
  3. 页面布局不适合打印(A4纸张)
  4. 打印了不需要的元素(导航、按钮等)

最佳解决方案:

  1. 使用打印媒体查询:
@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;
  }
}
  1. 添加打印按钮:
const handlePrint = () => {
  window.print();
};
  1. 使用专门的打印样式表:
<link rel="stylesheet" href="print.css" media="print">

延伸建议:

  • 使用print.js库提供更多打印选项
  • 生成PDF供用户下载打印
  • 提供打印预览功能

30. 页面性能监控

用户问题(真实口语): "用户反馈页面慢,但我们本地测试很快,怎么监控真实用户的性能?"

错误思路:

  • 只在开发环境测试性能
  • 让用户截图反馈
  • 使用console.time手动计时

正确分析: 性能监控缺失原因:

  1. 未使用性能监控工具
  2. 只关注首屏加载,忽略交互性能
  3. 未收集真实用户数据
  4. 性能数据未可视化

最佳解决方案:

  1. 使用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);
  1. 使用Performance API:
const perfData = performance.getEntriesByType('navigation')[0];
const pageLoadTime = perfData.loadEventEnd - perfData.fetchStart;
  1. 集成监控平台:Sentry、DataDog、New Relic
  2. 自定义性能埋点:
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自动化性能测试
  • 设置性能告警阈值
  • 定期分析性能报告并优化


📚 系列文章

🔗 相关链接


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