React 18如何在浏览器标签页标题动态添加动画红圈图标
React 18实现浏览器标签页带动画红圈效果

核心前提说明:
document.title仅支持纯文本内容,无法直接插入HTML元素、编写CSS实现红圈样式和动画效果。所有标签页上的自定义视觉提示,都是通过动态修改页面favicon(标签页左侧的站点小图标)实现的,动画效果通过定时切换favicon的绘制帧模拟。
实现步骤
1. 封装自定义Hook(React 18推荐方式)
把favicon修改、动画控制的逻辑封装成可复用的Hook,自动处理原始favicon保存、动画帧切换、组件卸载恢复的逻辑,不需要额外引入第三方依赖,通过Canvas动态绘制徽标即可。
import { useEffect, useRef } from 'react'; // 初始化favicon节点,提前保存原始favicon地址用于后续恢复 const initFavicon = () => { let link = document.querySelector('link[rel="icon"]'); if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } return { node: link, originalHref: link.href } } const { node: faviconNode, originalHref: originalFavicon } = initFavicon(); /** * favicon带动画红圈徽标Hook * @param {boolean} visible - 是否显示红圈 * @param {number} count - 红圈内显示的未读数,传0则显示纯红圈 */ export function useFaviconRedDot(visible = false, count = 0) { const animationTimer = useRef(null); const frameRef = useRef(0); // 绘制单帧favicon const drawFrame = (scale = 1, opacity = 1) => { const canvas = document.createElement('canvas'); canvas.width = 32; canvas.height = 32; const ctx = canvas.getContext('2d'); const baseIcon = new Image(); baseIcon.crossOrigin = 'anonymous'; baseIcon.onload = () => { // 先绘制原始站点favicon作为底图 ctx.drawImage(baseIcon, 0, 0, 32, 32); if (visible) { ctx.save(); ctx.globalAlpha = opacity; // 绘制红色圆形徽标 ctx.fillStyle = '#f43f32'; const radius = 7 * scale; ctx.beginPath(); ctx.arc(32 - radius, radius, radius, 0, Math.PI * 2); ctx.fill(); // 绘制徽标内的数字 if (count > 0) { ctx.fillStyle = '#fff'; ctx.font = 'bold 11px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const showText = count > 99 ? '99+' : String(count); ctx.fillText(showText, 32 - radius, radius); } ctx.restore(); } faviconNode.href = canvas.toDataURL('image/png'); } baseIcon.src = originalFavicon; }; useEffect(() => { // 隐藏红圈时清除动画,恢复原始favicon if (!visible) { if (animationTimer.current) clearInterval(animationTimer.current); animationTimer.current = null; faviconNode.href = originalFavicon; return; } // 开启动画:每120ms切换一帧,实现平滑呼吸效果 animationTimer.current = setInterval(() => { frameRef.current = (frameRef.current + 1) % 30; const progress = frameRef.current / 30; // 用正弦函数计算缩放、透明度参数,实现自然的呼吸动画 const currentScale = 0.85 + 0.2 * Math.sin(progress * Math.PI * 2); const currentOpacity = 0.7 + 0.3 * Math.sin(progress * Math.PI * 2); drawFrame(currentScale, currentOpacity); }, 120); // 组件卸载时清理定时器、恢复原始favicon return () => { if (animationTimer.current) clearInterval(animationTimer.current); faviconNode.href = originalFavicon; } }, [visible, count]); }
2. 在业务组件中调用
直接在全局组件或者需要触发提示的组件中引入Hook,传入显示状态和未读数即可:
import { useState } from 'react'; import { useFaviconRedDot } from './useFaviconRedDot'; function GlobalLayout() { const [unreadMessage, setUnreadMessage] = useState(0); // 未读数大于0时自动显示带动画的红圈 useFaviconRedDot(unreadMessage > 0, unreadMessage); return ( <div className="app"> <button onClick={() => setUnreadMessage(prev => prev + 1)}> 模拟收到新消息 </button> <button onClick={() => setUnreadMessage(0)}> 标记所有消息已读 </button> </div> ); } export default GlobalLayout;
注意事项
- 如果你的站点favicon是跨域资源(比如存放在CDN),需要给CDN配置跨域访问头,否则Canvas绘制时会触发跨域污染报错,无法导出图片。
- 动画帧间隔不建议低于100ms,过高的刷新频率会不必要地消耗浏览器性能,100-200ms的间隔已经能实现流畅的提示效果。
- 如果不需要显示数字,只需要纯红圈效果,调用Hook时把count参数固定传0即可。
内容的提问来源于stack exchange,提问作者Guillaume SOTTON
相关产品推荐
相关产品推荐

