在当今移动优先的Web开发环境中,网络连接状态管理已经成为提升用户体验的关键因素。想象一下这样的场景:用户正在地铁上使用你的应用,突然进入隧道导致网络中断,一个优雅的网络状态提示远比页面突然崩溃要友好得多。这正是useOnline自定义钩子要解决的核心问题。
作为React开发者,我们经常需要处理这类状态管理问题。传统的解决方案往往需要在各个组件中重复编写事件监听逻辑,而useOnline钩子将这些繁琐的细节封装成一个简洁的API,让开发者可以专注于业务逻辑的实现。
浏览器原生提供了检测网络状态的API,核心是navigator.onLine属性。这个布尔值属性简单明了:
true表示在线false表示离线但要注意,这个属性只能反映浏览器的"认为"的网络状态。在某些情况下,比如电脑连接到路由器但路由器本身没有互联网连接时,可能会产生误判。
浏览器还提供了两个重要的事件:
online:当网络从离线变为在线时触发offline:当网络从在线变为离线时触发这些事件会冒泡到window对象,因此我们可以这样监听:
javascript复制window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
让我们先看一个最简实现:
javascript复制import { useState, useEffect } from 'react';
function useOnline() {
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;
}
这个版本已经可以工作,但它有几个潜在问题:
navigator对象不存在下面是一个更健壮的实现:
javascript复制import { useState, useEffect } from 'react';
function useOnline() {
const [isOnline, setIsOnline] = useState(() => {
if (typeof window === 'undefined') return true; // SSR默认返回true
return navigator?.onLine ?? true; // 兼容性处理
});
useEffect(() => {
if (typeof window === 'undefined') return; // SSR不执行
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
// 添加事件监听
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// 可选:添加心跳检测增强准确性
const heartbeat = setInterval(async () => {
try {
await fetch('https://httpbin.org/get', {
method: 'HEAD',
cache: 'no-store',
mode: 'no-cors'
});
if (!isOnline) setIsOnline(true);
} catch {
if (isOnline) setIsOnline(false);
}
}, 30000);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
clearInterval(heartbeat);
};
}, [isOnline]);
return isOnline;
}
这个增强版解决了以下问题:
最常见的应用场景是根据网络状态显示不同的UI:
javascript复制function NetworkStatus() {
const isOnline = useOnline();
return (
<div className={`network-status ${isOnline ? 'online' : 'offline'}`}>
{isOnline ? '在线' : '离线'}
</div>
);
}
对于需要发送数据的应用,可以实现离线队列:
javascript复制function useOfflineQueue() {
const isOnline = useOnline();
const [queue, setQueue] = useState([]);
useEffect(() => {
if (isOnline && queue.length > 0) {
// 处理队列中的请求
processQueue(queue);
setQueue([]);
}
}, [isOnline, queue]);
const addToQueue = (item) => {
setQueue(prev => [...prev, item]);
};
return { addToQueue, queue };
}
根据网络状态调整数据加载策略:
javascript复制function useAdaptiveFetch(url) {
const isOnline = useOnline();
const [data, setData] = useState(null);
useEffect(() => {
if (!isOnline) {
// 离线时尝试从缓存加载
const cached = localStorage.getItem(url);
if (cached) setData(JSON.parse(cached));
return;
}
// 在线时获取最新数据
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
localStorage.setItem(url, JSON.stringify(data));
});
}, [url, isOnline]);
return data;
}
频繁的网络状态变化可能会影响性能,可以添加节流:
javascript复制function useOnline() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const timerRef = useRef(null);
useEffect(() => {
const handleChange = (status) => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setIsOnline(status);
}, 500); // 500ms防抖
};
const handleOnline = () => handleChange(true);
const handleOffline = () => handleChange(false);
// ...事件监听代码...
}, []);
}
使用BroadcastChannel API同步多个标签页的状态:
javascript复制function useOnline() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const channelRef = useRef(null);
useEffect(() => {
const channel = new BroadcastChannel('network_status');
channelRef.current = channel;
channel.onmessage = (e) => {
if (e.data.type === 'NETWORK_STATUS') {
setIsOnline(e.data.status);
}
};
return () => {
channel.close();
};
}, []);
const broadcastStatus = (status) => {
if (channelRef.current) {
channelRef.current.postMessage({
type: 'NETWORK_STATUS',
status
});
}
};
// ...在状态变化时调用broadcastStatus...
}
不仅检测在线/离线,还可以检测网络质量:
javascript复制function useNetworkQuality() {
const [quality, setQuality] = useState('good');
const isOnline = useOnline();
useEffect(() => {
if (!isOnline) {
setQuality('offline');
return;
}
let startTime;
const testConnection = async () => {
startTime = performance.now();
try {
await fetch('https://httpbin.org/get', {
method: 'HEAD',
cache: 'no-store'
});
const duration = performance.now() - startTime;
setQuality(
duration < 500 ? 'good' :
duration < 2000 ? 'average' : 'poor'
);
} catch {
setQuality('offline');
}
};
testConnection();
const interval = setInterval(testConnection, 30000);
return () => clearInterval(interval);
}, [isOnline]);
return { isOnline, quality };
}
Chrome开发者工具:
编程方式:
javascript复制// 在测试中模拟离线状态
Object.defineProperty(navigator, 'onLine', {
value: false,
writable: true
});
window.dispatchEvent(new Event('offline'));
使用测试库:
javascript复制import { mockNavigatorOnline } from 'test-utils';
test('should handle offline state', () => {
mockNavigatorOnline(false);
// 测试离线场景
});
使用Jest测试useOnline钩子:
javascript复制import { renderHook, act } from '@testing-library/react-hooks';
import { useOnline } from './useOnline';
describe('useOnline', () => {
beforeEach(() => {
Object.defineProperty(navigator, 'onLine', {
value: true,
writable: true
});
});
it('should return online status', () => {
const { result } = renderHook(() => useOnline());
expect(result.current).toBe(true);
});
it('should update status when going offline', () => {
const { result } = renderHook(() => useOnline());
act(() => {
navigator.onLine = false;
window.dispatchEvent(new Event('offline'));
});
expect(result.current).toBe(false);
});
});
问题:有时浏览器的在线状态与实际网络状态不一致。
解决方案:
移动设备有更多网络状态变化场景:
优化方案:
javascript复制function useMobileOnline() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// 应用回到前台时检查网络状态
checkRealNetworkStatus().then(setIsOnline);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
// ...其他网络状态监听代码...
}
确保正确清理事件监听器:
javascript复制useEffect(() => {
const controller = new AbortController();
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline, { signal: controller.signal });
window.addEventListener('offline', handleOffline, { signal: controller.signal });
return () => {
controller.abort(); // 自动移除所有相关事件监听器
};
}, []);
将网络状态纳入全局状态管理:
javascript复制import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { setOnlineStatus } from './networkSlice';
function useReduxOnline() {
const dispatch = useDispatch();
useEffect(() => {
const handleOnline = () => dispatch(setOnlineStatus(true));
const handleOffline = () => dispatch(setOnlineStatus(false));
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [dispatch]);
}
实现真正的离线体验:
javascript复制// service-worker.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
// React组件中
function useOfflineFirst() {
const isOnline = useOnline();
useEffect(() => {
if (isOnline && 'serviceWorker' in navigator) {
// 在线时更新缓存
navigator.serviceWorker.ready
.then(registration => registration.sync.register('sync-data'));
}
}, [isOnline]);
}
优化数据获取策略:
javascript复制function useOnlineQuery(queryKey, queryFn) {
const isOnline = useOnline();
return useQuery(
queryKey,
queryFn,
{
enabled: isOnline,
staleTime: isOnline ? 0 : Infinity,
onError: (error) => {
if (error.message.includes('offline')) {
// 处理离线错误
}
}
}
);
}
在实际项目中使用useOnline钩子时,我发现最实用的技巧是将其与应用的错误边界和重试机制结合。当检测到网络恢复时,可以自动重试失败的请求,这种无缝的恢复体验用户几乎察觉不到,但却能显著提升应用的健壮性。