背景
使用者在使用electron app的時候,經常會遇到白屏,伺服器異常,網路異常等情況,但是使用者卻不知道如何解決,所以需要提供一個可以給使用者自行診斷網路的功能,一來方便使用者自己快速定位問題,自行解決,二來檢查開發排查問題的時間。
例如飛書在斷開自動自行診斷網路狀態,提示使用者設定網路。
功能需求
功能需求可以參考飛書,飛書的網路診斷就是比較完整的互動例子
需求內容:
- 支援網路狀態診斷 - 檢測網路是否線上或離線
- 支援nds解析診斷- 檢測伺服器域名是否能成功解析dns
- 支援代理診斷 - 檢測電腦是否開啟了代理
- 服務穩定性分析 - 檢測伺服器域名是否可連通
功能 | 說明 | web | app |
---|---|---|---|
支援網路狀態診斷 | 檢測網路是否線上或離線 | ☑️ | ☑️ |
支援nds解析診斷 | 檢測伺服器域名是否能成功解析dns | ✖️ | ☑️ |
支援代理診斷 | 檢測電腦是否開啟了代理 | ✖️ | ☑️ |
服務穩定性分析 | 檢測伺服器域名是否可連通 | ☑️ | ☑️ |
技術選型
web技術診斷
有時候也有這樣的需求,我想診斷web內所有域名伺服器地址的可連通性。
js引擎(瀏覽器)並沒有提供ping相關的能力,並且也無法透過xhr來做這種事情,因為這會涉及到跨域,那麼身下的方案就是透過資源請求,透過的做法就是傳送一個img的請求,而最通用的就是請求伺服器地址的favicon.ico。剛好就有這這樣的一個外掛可以幫助做這樣的事情:ping.js
安裝ping.js
$ npm install ping.js
使用demo,data返回的是rtt值
var p = new Ping();
// Using callback
p.ping("https://github.com", function(err, data) {
// Also display error if err is returned.
if (err) {
console.log("error loading resource")
data = data + " " + err;
}
document.getElementById("ping-github").innerHTML = data;
});
// You may use Promise if the browser supports ES6
p.ping("https://github.com")
.then(data => {
console.log("Successful ping: " + data);
})
.catch(data => {
console.error("Ping failed: " + data);
})
很明顯這種方式對於很多場景都是不適用的,比如一個專案涉及到其他域名的請求。
app技術診斷
app是原生應用,可以呼叫到計算機底層的api,就算不借助外掛,都可以透過開啟終端,ping域名,獲取返回值,所以app做這件事情是非常容易的,這裡我們針對electron應用舉例
診斷流程
獲取域名資訊
網路資訊的獲取分為兩種方式:
1. 業務側有輸入框,使用者手動輸入
2. 獲取專案配置的域名伺服器地址
網路狀態
網路問題發生時,網路狀態往往是第一觀察要素。如何去判斷網路是否連線上了網路:
- 通用的判斷方式:navigator.onLine,但是這個方式只會在機器未連線到區域網或路由器時返回false,其他情況下均返回true。 也就是說如果使用者路由器本身網路就不通的情況下是沒有辦法判斷的。
- 進階判斷方式:發出一個請求,看請求返回的狀態碼或者訪問一個網路資源,比如圖片。發出一個網路請求的網站來源,可以是一個固定穩定的靜態域名伺服器。
DNS解析
根據域名,查詢出對應 IP 資訊。伺服器域名,唯有正確的解析出 IP 後,才能成功進行後續的網路連線流程,這裡採用node的nds模組即可。 Desktop: dns.lookup(node)
ping連通性 與 traceRoute連通性
關於如何診斷,以及ping和traceroute的原理,可以這篇部落格:https://www.cnblogs.com/seanloveslife/p/11941736.html 。
這裡我們會用一個node的外掛 node-net-ping 來完成這些操作,該外掛不僅支援ping同時也支援traceroute,並且對這些操作的異常進行了很好的封裝,簡化呼叫過程。
如果在eletron使用過程中遇到問題:可以使用該外掛node-ping,僅支援ping
下載日誌
下載日誌的思路就是把之前每一步診斷異常的日誌透過electron-log或者其他日誌外掛寫入到本地檔案,再將本地的日誌檔案壓縮,最後允許使用者儲存在指定的目錄下面。
這裡重點看下壓縮的過程:
壓縮需要用到外掛archiver
const fs = require('fs')
const archiver = require('archiver')
/**
* 壓縮日誌
* @param {string} sourceFilePath
* @param {string} outputFilePath
* @returns
*/
const compressLogFolder = (sourceFilePath, outputFilePath) => {
return new Promise((resolve, reject) => {
try {
const outputStream = fs.createwriteStream(outputFilePath);
const archive = archiver('zip', { zlib: { level: 9 }})
archive.pipe(outputStream)
const data = fs. readFileSync (sourceFilePath, 'utf8');
archive.append(data.toString(), { name: 'network.log' })
archive.finalize();
outputStream.on( 'close', (res) = resolve(res)) ;
outputStream.on( 'error', (err) = reject(err));
} catch (error) {
reject (error)
}
})
}