背景介紹
因為正在開發一個專案,而這個專案使用到了puppeteer
,其中有個功能是在puppeteer
開啟的chrome裡開啟多個Tab
,並進行管理。
雖然puppeteer
可以開啟多個網站,但是並不利於管理,所有我使用的是外掛的方式,通過外掛來開啟多網站,並進行管理。
但是這裡有個需求是,當網站崩潰時,我要做出一些操作。但是目前網上沒有一個好的辦法去監聽當前網站是否崩潰。
可能有同學會說:puppeteer
不是提供了一個page.on('error', fn)
的方法,來進行監聽麼?
請注意上文中提到的,使用外掛開啟多個網站,puppeteer
提供的方法只能對自己開啟的網站起作用,沒有使用puppeteer
開啟的網站,page.on('error', fn)
方法無能為力。
使用Service Workers
這個方法是由我同事Haitao提出來的思路。
在當前網站上執行一個Service Workers
,因為在執行的時候Service Workers
會再啟動一個單獨的程式,當前網站和Service Workers
是兩個單獨的程式。也就是說當網站崩潰時,並不影響Service Workers
程式。所以可通過心跳檢測
來進行判斷網站是否崩潰。
網上也有阿里的同學寫的相關文章:如何監控網頁崩潰?
但是我並沒有使用這個方式,因為當Service Workers
崩潰了,那就沒有任何辦法了,可能有同學會說:網站和Service Workers
互相發心跳檢測
。這可能是一種辦法,但是我不太喜歡這種方式。
使用Webkit的遠端除錯協議
介紹
在開始前,我們先去看下puppeteer
的原始碼,為什麼puppeteer
可以監聽到網頁的崩潰。
其程式碼在lib/Page.js
檔案裡。
首先可以看到Page是一個Class
,其繼承了EventEmitter
,EventEmitter
為page
提供了on
方法,也就是我們之前看到的:page.on('error', fn)
從這裡就可知,在Page Class
裡,有地方呼叫了this.emit('error')
來觸發error event
。搜了一下,發現其程式碼在_onTargetCrashed
方法裡。如:
觸發crash
的方法,我們找到了。那這個_onTargetCrashed
又是在哪觸發的呢?
可見,是一個叫client
的方法監聽到了Inspector.targetCrashed
事件,而這個事件觸發了_onTargetCrashed
函式,clinet
方法就不再跟了,因為跳地方較多,只需要知道,最終client
是一個websocket
的產物。而websocket
建立的程式碼在lib/Launcher.js
裡。程式碼位置
注意這兩行:
const transport = new PipeTransport((chromeProcess.stdio[3]), (chromeProcess.stdio[4]));
connection = new Connection('', transport, slowMo);
複製程式碼
chromeProcess
是nodejs
中的spawn
產物,程式碼為:
const chromeProcess = childProcess.spawn(
chromeExecutable,
chromeArguments,
{
detached: process.platform !== 'win32',
env,
stdio
}
);
複製程式碼
其中chromeArguments
是chrome
啟動的引數列表,此列表是有一個--remote-debugging-
的:
if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
複製程式碼
現在就明朗多了,Inspector.targetCrashed
這個事件,是由Webkit遠端除錯協議
也就是remote debugging protocol
提供的。
其定義在webkit的Inspector.json
裡: Source/WebCore/inspector/Inspector.json#L39-L42
關於這個event
的commit url為:github.com/WebKit/webk…
編寫解決方案程式碼
現在我們知道了,只要能監聽到Inspector.targetCrashed
事件,就可以知道網站是否關閉了。我們先在puppeteer
的啟動引數裡,增加一行啟動引數:
puppeteer.launch({
'--remote-debugging-port=9222',
// other args
});
複製程式碼
當puppeteer
啟動時,會監聽本地的9222
埠,其中路徑/json
為當前的詳情。如:
其格式為:
[
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3",
"faviconUrl": "https://s.ytimg.com/yts/img/favicon_32-vflOogEID.png",
"id": "A1CB5A9CC25A7EE8A99C6A4A1876E4D3",
"title": "張三李四 Chang and Lee 【等無此人 Waiting】 - YouTube",
"type": "page",
"url": "https://www.youtube.com/watch?v=lAcUGvpRkig&list=PL3p0C_7POnMHG-b0dzkeTVdNuM6yRE5iQ&index=10&t=0s",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3"
},
// other
{
複製程式碼
其中的type
為當前程式的詳情:
- page: 網頁
- iframe: 網頁巢狀的iframe
- background_page: 外掛頁面
- service_worker: Service Workers
這個type的作用在於,你只想監聽某一型別的崩潰。
還有一個更主要的欄位:webSocketDebuggerUrl
。我們將使用這個欄位的值,來進行獲取訊息。有一個簡單的demo:
const http = require('http');
const WebSocket = require('ws');
http.get('http://127.0.0.1:9222/json', res => {
res.addListener('data', data => {
const result = JSON.parse(data.toString());
result.forEach(info => {
const client = new WebSocket(info.webSocketDebuggerUrl);
client.on('message', data => {
if (data.indexOf('"method":"Inspector.targetCrashed"') !== -1) {
console.error('crash!');
}
});
});
})
})
複製程式碼
先看懂這段程式碼,後面的程式碼才好理解,因為程式碼過於簡單,這裡就不再介紹了。
這段程式碼有個問題是,外掛開啟網站時,會存在一定的延遲,可能會導致某些網站沒有被監聽到,而且當這段程式碼執行後,外掛再開啟網站時,也不會監聽到。針對這個問題,優化了下程式碼:
const http = require('http');
const WebSocket = require('ws');
module.exports = () => {
const wsList = {};
let crashStaus = false;
const getWsList = () => {
return new Promise((resolve) => {
http.get('http://127.0.0.1:9222/json', res => {
res.addListener('data', data => {
try {
const result = JSON.parse(data.toString());
const tempWsList = {};
result.forEach(info => {
if (typeof wsList[info.id] === 'undefined') {
tempWsList[info.id] = info.webSocketDebuggerUrl;
wsList[info.id] = info.webSocketDebuggerUrl;
}
});
if (Object.keys(tempWsList).length !== 0) {
resolve(tempWsList);
}
} catch (e) {
console.error(e);
}
});
});
});
};
setInterval(() => {
getWsList().then(list => {
Object.values(list).forEach(wsUrl => {
const client = new WebSocket(wsUrl);
client.on('message', data => {
if (data.indexOf('"method":"Inspector.targetCrashed"') !== -1) {
if (!crashStaus) {
crashStaus = true;
console.log('crash!!!');
}
}
});
})
});
}, 1000);
};
複製程式碼
其中需要說明一下這段程式碼:
if (!crashStaus) {
crashStaus = true;
console.log('crash!!!');
}
複製程式碼
因為我的需求是,任何一個程式crash
了,就關閉整個服務,重新執行。所以如果有多個程式同時crash
了。我的程式碼只走一次,不想讓他走多次。這個是針對我這裡的需求,各位同學可以根據自己的需求更改程式碼。
參考
作者資訊
- Author: Black-Hole
- Blog: bugs.cc/
- Github: github.com/BlackHole1/
- Twitter: twitter.com/Free_BlackH…
- Email: 158blackhole@gmail.com