React中Spotify客户端Token刷新机制querySelector报错修复求助
问题描述
纯客户端React应用依赖Spotify API,出于安全考虑未使用官方refresh token逻辑,改为在token即将过期时调用handleAuthorization引导用户重新授权。调试时设置每分钟调用检查函数,出现错误:
Uncaught DOMException: Failed to execute 'querySelector' on 'Document'
相关代码如下:
import React, { useEffect, useState } from 'react'; import { Route, Routes} from 'react-router-dom'; import LandingPage from './components/LandingPage'; import UserDashboard from './components/UserDashboard'; import PlaylistPage from './components/PlaylistPage'; import ConnectionError from './components/ConnectionError'; import 'react-notifications/lib/notifications.css'; import axios from 'axios'; function App() { const [token, setToken] = useState(''); const [profile, setProfile] = useState({}); const [playlists, setPlaylists] = useState([]); const [isOnline, setIsOnline] = useState(true); useEffect(() => { const handleOnlineStatusChange = () => { setIsOnline(window.navigator.onLine); }; window.addEventListener('online', handleOnlineStatusChange); window.addEventListener('offline', handleOnlineStatusChange); return () => { window.removeEventListener('online', handleOnlineStatusChange); window.removeEventListener('offline', handleOnlineStatusChange); }; }, []); const handleAuthorization = () => { const clientId = 'my-client-id'; const redirectUri = 'http://localhost:3000'; const scopes = ['user-library-read', 'playlist-read-private', 'playlist-modify-private', 'playlist-modify-public', 'playlist-read-collaborative']; const authorizationURL = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&redirect_uri=${encodeURIComponent( redirectUri )}&scope=${scopes.join('%20')}`; // Redirect for authorization window.location.href = authorizationURL; }; const fetchData = async () => { const hash = window.location.hash; const tokenParam = new URLSearchParams(hash.substring(1)).get('access_token'); if (tokenParam) { // Token is present in the URL const newToken = tokenParam; window.localStorage.setItem('token', newToken); setToken(newToken); // Redirect to the dashboard window.location.href = '/dashboard'; } else { // Token is not present in the URL, handle token expiration or other logic here const storedToken = window.localStorage.getItem('token'); if (storedToken) { // Token is stored locally, you can set it and perform necessary actions setToken(storedToken); // Fetch user data and playlists here try { const response = await axios.get('https://api.spotify.com/v1/me', { headers: { Authorization: `Bearer ${storedToken}`, }, }); const { data } = response; setProfile(data); // Fetch playlists try { const playlistsResponse = await axios.get('https://api.spotify.com/v1/me/playlists', { headers: { Authorization: `Bearer ${storedToken}`, }, params: { limit: 50, }, }); const { data: playlistsData } = playlistsResponse; setPlaylists(playlistsData.items); console.log(playlistsData.items); } catch (error) { console.error('Error fetching playlists:', error); } } catch (error) { console.error('Error fetching user data:', error); } } } }; const checkTokenExpiration = () => { const storedToken = window.localStorage.getItem('token'); if (storedToken) { const tokenData = JSON.parse(atob(storedToken.split('.')[1])); // Decode JWT payload // Calculate the remaining time before token expiration (in seconds) const currentTime = Math.floor(Date.now() / 1000); const expirationTime = tokenData.exp; const timeToExpiration = expirationTime - currentTime; // Set a threshold (e.g., 300 seconds) for when to refresh the token const tokenRefreshThreshold = 300; if (timeToExpiration <= tokenRefreshThreshold) { // Token is about to expire, refresh it handleAuthorization(); } } }; useEffect(() => { // Check token expiration every minute const tokenExpirationCheckInterval = setInterval(() => { checkTokenExpiration(); fetchData(); }, 60000); // 60 seconds return () => { clearInterval(tokenExpirationCheckInterval); }; }, []); const logout = () => { setToken(''); setProfile({}); setPlaylists([]); window.localStorage.removeItem('token'); // Redirect the user to the home page after logging out window.location.assign('http://localhost:3000'); }; return ( <div> {isOnline ? ( <Routes> <Route path="/" element={<LandingPage handleAuthorization={handleAuthorization} />} /> <Route path="/dashboard" element={<UserDashboard profile={profile} playlists={playlists} logout={logout} />} /> <Route path="/playlist/:id" element={<PlaylistPage />} /> </Routes> ) : ( <ConnectionError /> )} </div> ); } export default App;
错误根源
- 强制页面刷新中断React渲染流程:使用
window.location.href/window.location.assign跳转,会强制刷新页面,导致React组件在未完成卸载/渲染时被中断,引发DOM查询异常。 - 重复逻辑与定时器冲突:每分钟同时调用
checkTokenExpiration和fetchData,可能出现重复跳转、请求冲突,加剧DOM操作问题。 - JWT解码无异常处理:若token格式无效,
atob或JSON.parse会抛出错误,导致后续逻辑中断。 - 数据获取时机不合理:初始加载时未主动调用
fetchData,且重复调用会引发不必要的API请求。
修复方案
1. 替换原生跳转为React路由导航
使用react-router-dom的useNavigate钩子实现无刷新跳转,避免中断React渲染流程:
// 导入useNavigate和useRef import { Route, Routes, useNavigate } from 'react-router-dom'; import { useRef } from 'react'; function App() { const navigate = useNavigate(); const tokenExpirationCheckInterval = useRef(null); // 用ref保存定时器ID // ...其他state定义 // 修改fetchData中的跳转逻辑 const fetchData = async () => { const hash = window.location.hash; const tokenParam = new URLSearchParams(hash.substring(1)).get('access_token'); if (tokenParam) { const newToken = tokenParam; window.localStorage.setItem('token', newToken); setToken(newToken); // 无刷新跳转到dashboard navigate('/dashboard'); // 清除URL中的hash,避免重复解析 window.history.replaceState({}, document.title, window.location.pathname); } else { const storedToken = window.localStorage.getItem('token'); if (storedToken) setToken(storedToken); } }; // 修改logout的跳转逻辑 const logout = () => { setToken(''); setProfile({}); setPlaylists([]); window.localStorage.removeItem('token'); navigate('/'); // 无刷新跳转到首页 };
2. 优化token检查与定时器逻辑
- 给JWT解码添加异常捕获,防止无效token崩溃
- 仅在存在token时启动定时器,跳转前清除定时器避免重复触发
const checkTokenExpiration = () => { const storedToken = window.localStorage.getItem('token'); if (!storedToken) return; try { const tokenParts = storedToken.split('.'); // 验证JWT格式是否正确 if (tokenParts.length !== 3) throw new Error('Invalid token format'); const tokenData = JSON.parse(atob(tokenParts[1])); const currentTime = Math.floor(Date.now() / 1000); const timeToExpiration = tokenData.exp - currentTime; if (timeToExpiration <= 300) { // 跳转前清除定时器,避免重复触发 clearInterval(tokenExpirationCheckInterval.current); handleAuthorization(); } } catch (error) { console.error('Token parsing failed:', error); // token无效,直接引导重新授权 handleAuthorization(); } }; // 调整定时器的启动逻辑 useEffect(() => { // 初始化时先解析token并获取数据 fetchData(); const storedToken = window.localStorage.getItem('token'); if (storedToken) { tokenExpirationCheckInterval.current = setInterval(() => { checkTokenExpiration(); }, 60000); // 每分钟检查一次token过期 } return () => { if (tokenExpirationCheckInterval.current) { clearInterval(tokenExpirationCheckInterval.current); } }; }, []);
3. 分离数据获取逻辑,避免重复调用
将用户数据和播放列表的获取逻辑移到token变化的useEffect中,仅在token更新时触发请求:
useEffect(() => { if (!token) return; const fetchUserDataAndPlaylists = async () => { try { // 获取用户信息 const userResponse = await axios.get('https://api.spotify.com/v1/me', { headers: { Authorization: `Bearer ${token}` } }); setProfile(userResponse.data); // 获取播放列表 const playlistsResponse = await axios.get('https://api.spotify.com/v1/me/playlists', { headers: { Authorization: `Bearer ${token}` }, params: { limit: 50 } }); setPlaylists(playlistsResponse.data.items); } catch (error) { console.error('Data fetch failed:', error); // 请求失败说明token无效,引导重新授权 handleAuthorization(); } }; fetchUserDataAndPlaylists(); }, [token]);
4. 优化在线状态变化逻辑
在线状态恢复时,若有token则重新获取数据,确保数据同步:
useEffect(() => { const handleOnlineStatusChange = () => { const isNowOnline = window.navigator.onLine; setIsOnline(isNowOnline); // 重新在线且有token时,刷新数据 if (isNowOnline && token) { const fetchUserDataAndPlaylists = async () => { try { const userResponse = await axios.get('https://api.spotify.com/v1/me', { headers: { Authorization: `Bearer ${token}` } }); setProfile(userResponse.data); const playlistsResponse = await axios.get('https://api.spotify.com/v1/me/playlists', { headers: { Authorization: `Bearer ${token}` }, params: { limit: 50 } }); setPlaylists(playlistsResponse.data.items); } catch (error) { console.error('Data fetch after reconnection failed:', error); handleAuthorization(); } }; fetchUserDataAndPlaylists(); } }; window.addEventListener('online', handleOnlineStatusChange); window.addEventListener('offline', handleOnlineStatusChange); return () => { window.removeEventListener('online', handleOnlineStatusChange); window.removeEventListener('offline', handleOnlineStatusChange); }; }, [token]);
内容的提问来源于stack exchange,提问作者sofia
相关产品推荐
相关产品推荐

