JavaScript檢測(cè)網(wǎng)絡(luò)連接狀態(tài)以及網(wǎng)絡(luò)測(cè)速的具體方案
引言
在實(shí)際開(kāi)發(fā)項(xiàng)目中,有時(shí)候需要拿到用戶(hù)網(wǎng)絡(luò)狀態(tài),然后根據(jù)用戶(hù)的網(wǎng)絡(luò)情況進(jìn)行優(yōu)化。常見(jiàn)的網(wǎng)絡(luò)狀態(tài)有離線(xiàn) offline、WiFi、2G/3G、4G、5G 等,但在實(shí)際開(kāi)發(fā)中,我們一般會(huì)劃分成斷網(wǎng)、弱網(wǎng)、強(qiáng)網(wǎng)三個(gè)等級(jí),并針對(duì)等級(jí)進(jìn)行優(yōu)化。比如在視頻播放時(shí),可以根據(jù)不同網(wǎng)絡(luò)情況給用戶(hù)分別默認(rèn)播放不同清晰度的視頻(高清、標(biāo)清、流暢等)。
那么 JavaScript 中如何檢測(cè)用戶(hù)網(wǎng)絡(luò)狀態(tài)呢?
1、網(wǎng)絡(luò)狀態(tài):在線(xiàn) online Or 離線(xiàn) offline?
HTML5 規(guī)范提供了 navigator.onLine 來(lái)拿到瀏覽器的在線(xiàn)狀態(tài)。該屬性返回一個(gè)布爾值,true 表示在線(xiàn),false 表示離線(xiàn)。同時(shí)提供了 online 和 offline 事件,通過(guò)監(jiān)聽(tīng)這兩個(gè)事件,可以動(dòng)態(tài)監(jiān)聽(tīng)到網(wǎng)絡(luò)連接狀態(tài)變化。
// 監(jiān)聽(tīng)網(wǎng)絡(luò)連接狀態(tài)
window.addEventListener('online', () => {
console.log('網(wǎng)絡(luò)已連接');
// 觸發(fā)數(shù)據(jù)同步...
});
window.addEventListener('offline', () => {
console.log('網(wǎng)絡(luò)已斷開(kāi)');
// 進(jìn)入離線(xiàn)模式...
});
// 實(shí)時(shí)獲取當(dāng)前狀態(tài)
console.log('當(dāng)前網(wǎng)絡(luò)狀態(tài):', navigator.onLine ? '在線(xiàn)' : '離線(xiàn)');
但是 navigator.onLine 有一定的局限性,比如在某些情況下,網(wǎng)絡(luò)是連接的,它也返回 true 了,但實(shí)際上無(wú)法訪(fǎng)問(wèn)你的后端 API:
- WiFi連接了但無(wú)互聯(lián)網(wǎng)訪(fǎng)問(wèn)(比如公共圖書(shū)館需要登錄認(rèn)證的公共 WiFi)。
- 網(wǎng)絡(luò)延遲極高(理論上在線(xiàn),但請(qǐng)求超時(shí))。
- DNS解析失敗。
我們需要更加準(zhǔn)確的判斷用戶(hù)此刻是否網(wǎng)絡(luò)能正常訪(fǎng)問(wèn)服務(wù)端,這就需要做心跳檢測(cè)。
2、心跳檢測(cè)
顧名思義,心跳檢測(cè)就像人的心跳一樣,每隔一段時(shí)間跳動(dòng)一下,告訴外界"我還活著"。它是一種用于判斷連接對(duì)端(如服務(wù)器或客戶(hù)端)是否存活的機(jī)制。它常常用于 WebSocket 連接,用來(lái)解決非正常關(guān)閉造成的"死連接"問(wèn)題。我們這里可以借用它檢測(cè)客戶(hù)端“是否存活”的能力,來(lái)判斷客戶(hù)端的網(wǎng)絡(luò)是否正常訪(fǎng)問(wèn)后端 API。
心跳檢測(cè)客戶(hù)端代碼:
// client.js
class NetworkMonitor {
constructor(options = {}) {
this.options = {
pingUrl: '/api/ping', // 心跳檢測(cè)接口
interval: 30000, // 30秒檢測(cè)一次
timeout: 5000, // 5秒超時(shí)
onStatusChange: null, // 狀態(tài)變化回調(diào)
...options
};
this.isOnline = navigator.onLine; // 當(dāng)前網(wǎng)絡(luò)狀態(tài)
this.checking = false; // 是否正在檢測(cè)
this.timer = null; // 定時(shí)器
this.consecutiveFailures = 0; // 連續(xù)失敗次數(shù)
this.maxFailures = 2; // 最大允許失敗次數(shù)
this.init();
}
init() {
// 監(jiān)聽(tīng)瀏覽器原生事件
window.addEventListener('online', () => this.handleBrowserOnline());
window.addEventListener('offline', () => this.handleBrowserOffline());
// 啟動(dòng)心跳檢測(cè)
this.startHeartbeat();
}
startHeartbeat() {
this.stopHeartbeat();
this.timer = setInterval(() => this.checkNetwork(), this.options.interval);
// 立即執(zhí)行一次
this.checkNetwork();
}
stopHeartbeat() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
async checkNetwork() {
if (this.checking) return;
this.checking = true;
const startTime = Date.now();
try {
// 使用fetch發(fā)起一個(gè)輕量級(jí)請(qǐng)求
const response = await fetch(this.options.pingUrl, {
method: 'HEAD', // 只獲取頭部,減少數(shù)據(jù)傳輸
mode: 'no-cors', // 如果跨域且無(wú)需讀取響應(yīng)
cache: 'no-cache',
headers: {
'X-Network-Check': 'ping'
}
});
// 請(qǐng)求成功
this.handleSuccess();
} catch (error) {
// 請(qǐng)求失?。ǔ瑫r(shí)、網(wǎng)絡(luò)錯(cuò)誤等)
this.handleFailure();
} finally {
this.checking = false;
}
}
handleSuccess() {
// 重置失敗計(jì)數(shù)
this.consecutiveFailures = 0;
// 如果之前是離線(xiàn),現(xiàn)在變?yōu)樵诰€(xiàn)
if (!this.isOnline) {
this.setOnlineStatus(true);
}
}
handleFailure() {
this.consecutiveFailures++;
// 連續(xù)失敗達(dá)到閾值,判定為離線(xiàn)
if (this.consecutiveFailures >= this.maxFailures && this.isOnline) {
this.setOnlineStatus(false);
}
}
handleBrowserOnline() {
// 瀏覽器說(shuō)在線(xiàn)了,但我們?nèi)孕栩?yàn)證真正的API可達(dá)性
console.log('瀏覽器報(bào)告網(wǎng)絡(luò)已連接,開(kāi)始驗(yàn)證...');
this.checkNetwork();
}
handleBrowserOffline() {
// 瀏覽器明確說(shuō)離線(xiàn),直接更新?tīng)顟B(tài)
this.setOnlineStatus(false);
}
setOnlineStatus(status) {
if (this.isOnline !== status) {
this.isOnline = status;
console.log(`網(wǎng)絡(luò)狀態(tài)變更為: ${status ? '在線(xiàn)' : '離線(xiàn)'}`);
// 觸發(fā)回調(diào)
if (this.options.onStatusChange) {
this.options.onStatusChange(status);
}
// 觸發(fā)自定義事件,方便其他地方監(jiān)聽(tīng)
window.dispatchEvent(new CustomEvent('network-status-change', {
detail: { isOnline: status }
}));
}
}
// 手動(dòng)觸發(fā)一次檢測(cè)
async checkNow() {
return this.checkNetwork();
}
// 銷(xiāo)毀
destroy() {
this.stopHeartbeat();
window.removeEventListener('online', this.handleBrowserOnline);
window.removeEventListener('offline', this.handleBrowserOffline);
}
}
// 使用示例
const monitor = new NetworkMonitor({
pingUrl: 'https://your-api.com/api/ping',
interval: 15000, // 15秒檢測(cè)一次
timeout: 3000,
onStatusChange: (isOnline) => {
if (isOnline) {
// 網(wǎng)絡(luò)恢復(fù),觸發(fā)數(shù)據(jù)同步
syncWithServer();
showToast('網(wǎng)絡(luò)已恢復(fù),正在同步數(shù)據(jù)...');
} else {
// 網(wǎng)絡(luò)斷開(kāi),進(jìn)入離線(xiàn)模式
enableOfflineMode();
showToast('網(wǎng)絡(luò)已斷開(kāi),進(jìn)入離線(xiàn)模式');
}
}
});
心跳檢測(cè)服務(wù)端代碼:
// server.js
const express = require('express');
const app = express();
const port = 3000;
// 心跳檢測(cè)接口
app.head('/api/ping', (req, res) => {
// 可選:記錄心跳日志
console.log(`[${new Date().toISOString()}] 收到心跳檢測(cè)請(qǐng)求 from ${req.ip}`);
// 可選:檢查自定義頭
if (req.headers['x-network-check'] === 'ping') {
console.log('網(wǎng)絡(luò)健康檢查請(qǐng)求');
}
// 只需返回 200 狀態(tài)碼即可
res.status(200).end();
});
// 如果需要 GET 也支持(某些場(chǎng)景可能需要)
app.get('/api/ping', (req, res) => {
// 可以返回一些輕量級(jí)數(shù)據(jù)
res.json({
status: 'ok',
timestamp: Date.now(),
message: 'success'
});
});
// 啟動(dòng)服務(wù)器
app.listen(port, () => {
console.log(`網(wǎng)絡(luò)監(jiān)控服務(wù)運(yùn)行在 http://localhost:${port}`);
});
3、網(wǎng)速檢測(cè)
3.1 navigator.connection
HTML5 規(guī)范提供了 navigator.connection 可以獲取網(wǎng)絡(luò)類(lèi)型 effectiveType 和網(wǎng)絡(luò)下行速度 downlink。
console.log("網(wǎng)絡(luò)狀態(tài):" + navigator.connection.effectiveType); // 比如:4g/3g/2g
console.log("網(wǎng)絡(luò)下行速度:" + navigator.connection.downlink + "MB/S"); // 比如:10MB/S
但 navigator.connection 存在兩個(gè)問(wèn)題,第一個(gè)是兼容性很差,像 Edge、Firefox、Safari 這種主流瀏覽器都不支持。

第二個(gè)是數(shù)據(jù)不準(zhǔn)確,與一些測(cè)速器的結(jié)果有一定的偏差?;谶@兩個(gè)問(wèn)題,實(shí)際項(xiàng)目中不會(huì)采用 navigator.connection 方案。
3.2 圖片測(cè)速法
圖片測(cè)速法就是通過(guò)客戶(hù)端加載圖片,拿到圖片大小和加載時(shí)間,進(jìn)而算出用戶(hù)的網(wǎng)速,其具體步驟如下:
- 加載兩張圖片,一張極小的(比如 1x1像素),一張稍大的(比如 2x2像素)。
- 記錄圖片請(qǐng)求開(kāi)始時(shí)間,以及圖片
onLoad完成的時(shí)間。 - 計(jì)算速度:
圖片大小 / 加載時(shí)間 = 網(wǎng)速。 - 取平均值,把兩個(gè)數(shù)據(jù)取個(gè)平均值,目的是減少誤差。
小結(jié)
- 瀏覽器可以通過(guò)
navigator.onLine接口拿到瀏覽器的在線(xiàn)狀態(tài),并通過(guò)監(jiān)聽(tīng)online和offline事件監(jiān)聽(tīng)網(wǎng)絡(luò)變化。 - 由于
navigator.onLine存在一定的局限性,比如無(wú)法識(shí)別到 WiFi 連接了但無(wú)互聯(lián)網(wǎng)訪(fǎng)問(wèn)、網(wǎng)絡(luò)延遲極高、DNS 解析失敗等情況,我們還需要通過(guò)心跳檢測(cè)方案來(lái)判斷客戶(hù)端是否能正常訪(fǎng)問(wèn)后端 API。 navigator.connection雖然能拿到用戶(hù)的網(wǎng)絡(luò)下行速度downlink,但connection接口存在兼容性和數(shù)據(jù)不準(zhǔn)的問(wèn)題,所以我們需要用圖片測(cè)速法來(lái)更加準(zhǔn)確地拿到用戶(hù)網(wǎng)速情況。
以上就是JavaScript檢測(cè)網(wǎng)絡(luò)連接狀態(tài)以及網(wǎng)絡(luò)測(cè)速的具體方案的詳細(xì)內(nèi)容,更多關(guān)于JavaScript檢測(cè)網(wǎng)絡(luò)連接狀態(tài)及測(cè)速的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript實(shí)例--教你實(shí)現(xiàn)撲克牌洗牌功能
我們一般都會(huì)按照順序把隨機(jī)摸過(guò)來(lái)的牌從小到大的順序在手上理整齊(記得小時(shí)候打牌兩副牌手都抓不過(guò)來(lái)),這篇隨筆就是想通過(guò)實(shí)現(xiàn)這個(gè)功能來(lái)熟悉下js中排序數(shù)組等相關(guān)知識(shí)。2014-05-05
同步異步動(dòng)態(tài)引入js文件的幾種方法總結(jié)
下面小編就為大家?guī)?lái)一篇同步異步動(dòng)態(tài)引入js文件的幾種方法總結(jié)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09
js尾調(diào)用優(yōu)化的實(shí)現(xiàn)
這篇文章主要介紹了js尾調(diào)用優(yōu)化的實(shí)現(xiàn),尾調(diào)用(Tail Call)是函數(shù)式編程的一個(gè)重要概念,本文介紹它的含義和用法。感興趣的可以了解一下2019-05-05

