卡口服務(wù)——基于前端巡檢系統(tǒng)的拓展實踐
一、背景
體驗是得物的業(yè)務(wù)關(guān)鍵詞之一,對于前端開發(fā)而言,提高用戶體驗更是重要工作內(nèi)容之一。
得物前端平臺目前有巡檢系統(tǒng)、監(jiān)控平臺等多種手段保障線上頁面穩(wěn)定運行,但是仍有一部分問題處于“監(jiān)控死角”,而且巡檢、監(jiān)控都屬于后置告警手段,為了確保頁面上線前就能得到一定的用戶體驗保障,結(jié)合公司的戰(zhàn)略目標(biāo),我們決定開發(fā)一個H5頁面檢測服務(wù),用來前置檢測即將上線的頁面,提前暴露該頁面可能存在的問題反饋給對應(yīng)的開發(fā)/運營,我們將這個服務(wù)稱之為:“體驗卡口”。
本文從這次“體驗卡口”服務(wù)的開發(fā)實踐出發(fā),同時介紹得物巡檢系統(tǒng)的架構(gòu)和設(shè)計,希望能給參與穩(wěn)定性建設(shè)的開發(fā)小伙伴提供一定的學(xué)習(xí)和參考價值。
二、用戶體驗量化標(biāo)準(zhǔn)
當(dāng)我們試圖量化影響用戶體驗的問題時,需要思考以下兩個主要問題:
什么影響了用戶體驗?
我們已經(jīng)通過豐富的數(shù)據(jù)支撐和實踐經(jīng)驗,對影響用戶體驗的因素有了深入了解。從過去的線上問題反饋收集和開發(fā)經(jīng)驗中,我們將體驗問題大致分為兩個等級:
- P0級:這些問題嚴(yán)重影響頁面加載速度或涉及到安全風(fēng)險,例如頁面包含超大的圖片/媒體資源、頁面中含有個人隱私信息;
- P1級:這些問題可能對用戶體驗造成潛在影響,例如頁面中存在響應(yīng)時間超過300ms的接口請求。
如何檢測以及量化問題?
一旦我們對體驗問題進行了定義和分級,接下來需要建立適當(dāng)?shù)臋C制來檢測這些問題。對于卡口服務(wù),我們可以采取以下步驟來量化問題、轉(zhuǎn)換為可執(zhí)行的檢測代碼,并通過卡口服務(wù)生成相應(yīng)的檢測報告供調(diào)用方使用:
- 確定指標(biāo)和標(biāo)準(zhǔn):首先,我們需要確定用于量化體驗問題的指標(biāo)和標(biāo)準(zhǔn)。例如,對于接口請求速度問題,可以使用接口響應(yīng)時間作為指標(biāo),同時設(shè)定一定的標(biāo)準(zhǔn),例如超過特定時間閾值即視為問題。
- 編寫自動化腳本:基于指標(biāo)和標(biāo)準(zhǔn),我們可以編寫自動化腳本來模擬用戶在無頭瀏覽器中執(zhí)行相關(guān)操作,例如加載頁面、點擊按鈕、發(fā)送請求等。這些腳本將根據(jù)設(shè)定的指標(biāo)進行性能測量和問題檢測。
- 使用無頭瀏覽器執(zhí)行測試:我們可以在無頭瀏覽器中運行自動化腳本,模擬用戶行為并收集相應(yīng)的性能數(shù)據(jù)。
- 結(jié)果分析和報告生成:通過收集的性能數(shù)據(jù),我們可以進行結(jié)果分析,并將問題和相關(guān)數(shù)據(jù)轉(zhuǎn)化為檢測報告。該報告可以包括問題的詳細(xì)描述、問題等級、相關(guān)性能指標(biāo)和數(shù)據(jù)。
- 提供給調(diào)用方:最后,通過卡口服務(wù),我們可以將生成的檢測報告提供給調(diào)用方。調(diào)用方可以根據(jù)報告中的問題和數(shù)據(jù)進行相應(yīng)的優(yōu)化和改進,以提升用戶體驗。
這樣的機制可以幫助我們自動化地檢測和量化體驗問題,并提供可執(zhí)行的檢測代碼和相關(guān)報告。這樣一來,我們可以更有效地識別和解決問題,并提供準(zhǔn)確的數(shù)據(jù)給予開發(fā)團隊進行優(yōu)化。
以下是我們整理出需要具體實現(xiàn)的檢測case:
收集完具體的影響用戶體驗的case之后,要確定具體的開發(fā)方案,由于卡口服務(wù)與得物前端平臺巡檢系統(tǒng)有很多技術(shù)實現(xiàn)重合的部分,所以我們決定利用現(xiàn)有的巡檢架構(gòu),將“體驗卡口”集成到現(xiàn)有的巡檢系統(tǒng)中,可以節(jié)省大量的開發(fā)時間。
3、巡檢系統(tǒng)基礎(chǔ)架構(gòu)
巡檢系統(tǒng)的程序目標(biāo)一句話總結(jié):定時從數(shù)據(jù)源獲取待檢測頁面地址列表,然后進行批量檢測并生成報告。
為了應(yīng)對不同場景下的個性化需求,巡檢系統(tǒng)抽象出了三個巡檢器基類,各場景繼承基類實現(xiàn)定制需求。
3.1 巡檢器基類
- DataProviderBase(數(shù)據(jù)提供基類):
- dataSlim(): 簡化冗余數(shù)據(jù);
- fetchData():獲取遠(yuǎn)程數(shù)據(jù),處理并返回待檢測頁面url列表;
- isSkipTime():用來設(shè)置條件,在某些特定條件下跳過定時任務(wù);
- schedule():設(shè)置定時任務(wù)運行區(qū)間;
- PageInspectorBase(頁面檢查器基類):
- check():檢查器入口,用來打開指定的檢測頁面,并初始化各種資源的監(jiān)聽;
- injectRequestHeaders():注入頁面接口請求需要的cookie、token等;
- urlCheck():url地址檢查;
- onRequest():監(jiān)聽頁面請求;
- onResponse():監(jiān)聽頁面響應(yīng);
- onPageError():監(jiān)聽頁面錯誤;
- DataReporterBase(數(shù)據(jù)報告基類):
- buildReporter(): 根據(jù)采集到的錯誤信息生成檢測報告;
- feishuNotify():將生成的報告通過飛書發(fā)送到指定的通知群;
- getHTMLReporterUrl():根據(jù)ejs模板將報告生成html靜態(tài)文件并上傳,返回在線報告地址;
我們可以形象地將這三個基類比成一家飯店的三個不同分工的部門,能更方便地去理解它:
飯店前臺負(fù)責(zé)接收顧客提供的訂單,后廚根據(jù)訂單下料炒菜裝盤,服務(wù)員將做好的飯菜提供給顧客。
DataProviderBase(數(shù)據(jù)提供基類):負(fù)責(zé)定時輪詢接收外部提供的待檢測頁面列表。這個組件類似于飯店前臺,接收顧客提供的訂單。它負(fù)責(zé)從外部獲取待檢測的頁面列表,并將這些頁面?zhèn)鬟f給檢測器進行檢測。
PageInspectorBase(頁面檢查器基類):逐一檢測頁面列表中的每一個URL,并檢測頁面中的潛在問題。類似于后廚根據(jù)訂單下料、炒菜和裝盤的過程,這個組件負(fù)責(zé)逐個檢測待檢測頁面列表中的URL,并對每個頁面進行問題檢測。它可以使用一系列的檢測方法和規(guī)則,以確定頁面是否存在潛在問題。
DataReporterBase(數(shù)據(jù)報告基類):將檢測搜集的問題進一步整理后發(fā)送報告。類似于服務(wù)員將做好的飯菜提供給顧客,這個組件負(fù)責(zé)將經(jīng)過檢測的問題進行整理和匯總,并生成相應(yīng)的報告。報告可以包括問題的描述、嚴(yán)重程度、相關(guān)頁面URL等信息。然后,報告可以被發(fā)送給相關(guān)的利益相關(guān)者,例如開發(fā)或運營。
3.2 巡檢器
基于以上三個基類,根據(jù)不同巡檢場景開發(fā)不同的巡檢器(inspector),每一個巡檢器都包含了分別繼承以上三個基類的三個子類,繼承了基類的子類巡檢器通過覆寫/拓展基類方法以實現(xiàn)自己的個性化需求,以下是一個極簡的巡檢器例子:
// data-provider.ts
export class DataProvider extends DataProviderBase {
// 實現(xiàn)特定的頁面列表獲取邏輯
async fetchData(args) {
return await axios.get('https://xxx.xxx').then(res => res.data.urlList)
}
// 每隔15分鐘獲取一次待檢測列表
async schedule() {
return [{cron: '*/15 * * * *',args: {}}]
}
}
// page-inspector.ts
export class PageInspector extends PageInspectorBase {
async onPageOpen(page, reporter: PageReporter, data) {
const pageTitle = await page.evaluate('window.document.title')
console.log('這里可以獲取到頁面title', pageTitle)
}
}
// data-reporter.ts
export class DataReporter extends DataReporterBase {
async beforeFeishuNotify(data: InspectorReportBase) {
console.log('在飛書通知前做點什么', data)
return data
}
}
3.3 巡檢主程序
在巡檢系統(tǒng)中,每個頁面的檢測任務(wù)都是獨立的異步任務(wù),并且每份檢測報告的整理和發(fā)送也是獨立的異步任務(wù)。為了方便管理和維護這些異步任務(wù)以及任務(wù)消息的存儲和傳遞,巡檢系統(tǒng)使用Redis結(jié)合Bull作為巡檢系統(tǒng)的異步任務(wù)管理工具。
Redis是一個內(nèi)存數(shù)據(jù)庫,它提供高性能的數(shù)據(jù)存儲和訪問能力。
Bull是一個基于Redis的任務(wù)隊列庫,它提供了任務(wù)的調(diào)度、執(zhí)行和消息傳遞的功能。
有了巡檢器和異步任務(wù)管理能力,主程序的主要工作如下:
- 定義任務(wù):使用Bull創(chuàng)建兩個任務(wù)隊列,page_queue用于存放“頁面檢測任務(wù)”,reporter_queue用于存放“報告生成任務(wù)”。
- 生產(chǎn)任務(wù):在巡檢系統(tǒng)中,頁面檢測任務(wù)和報告生成任務(wù)的生產(chǎn)者(主程序)負(fù)責(zé)將任務(wù)添加到相應(yīng)的隊列中。當(dāng)巡檢器(inspector)需要進行頁面檢測時,生產(chǎn)者將頁面檢測任務(wù)加入page_queue;當(dāng)需要生成報告時,生產(chǎn)者將報告生成任務(wù)加入reporter_queue。
- 消費任務(wù):巡檢系統(tǒng)中的任務(wù)消費者(主程序)負(fù)責(zé)從任務(wù)隊列中獲取任務(wù)并執(zhí)行,一次檢測任務(wù)會有>=1個頁面檢測任務(wù),交由上文介紹的頁面檢查器PageInspector執(zhí)行頁面檢查,然后將檢測報告存儲到Redis中,當(dāng)該次檢測任務(wù)的所有頁面都完成檢測后,reporter_queue任務(wù)被創(chuàng)建并交由巡檢器(inspector)的DataReporter消費。
四、卡口服務(wù)
介紹完巡檢系統(tǒng),接下來我們看如何將卡口服務(wù)集成自巡檢系統(tǒng)中。
卡口服務(wù)的主要功能用一句話概括:接入巡檢系統(tǒng)的現(xiàn)有架構(gòu),對外暴露一個遠(yuǎn)程接口,提供給接口調(diào)用方主動檢測頁面的能力,然后將檢測報告回傳給調(diào)用方。
對比現(xiàn)有巡檢系統(tǒng)與卡口服務(wù)的差異:
檢測發(fā)起方 | 檢測case | 檢測報告重點 | 檢測回調(diào) | 在線報告 | |
巡檢系統(tǒng) | 系統(tǒng)自行定時觸發(fā) | 通用檢測 | 重點關(guān)注異常 | 無 | 有 |
卡口服務(wù) | 接口調(diào)用觸發(fā) | 基于通用檢測拓展 | 給出詳細(xì)報告 | 根據(jù)需求觸發(fā) | 有 |
從上文的巡檢系統(tǒng)架構(gòu)介紹以及分析上面的表格可知,卡口服務(wù)的開發(fā)工作就是基于巡檢系統(tǒng)的巡檢器架構(gòu)去定制實現(xiàn)一個巡檢器。
4.1 卡口服務(wù)運行時序
開始開發(fā)卡口服務(wù)的巡檢器之前,我們先梳理一下整個卡口服務(wù)的運行時序:
其中卡口服務(wù)主要開發(fā)任務(wù):步驟2、3、4、7。
4.2 創(chuàng)建任務(wù)接口
我們在上文提到,巡檢是一種后置檢測手段,所以巡檢系統(tǒng)的DataProviderBase(數(shù)據(jù)提供基類)主要能力是:“定時輪詢接收外部提供的待檢測頁面列表”。
對于卡口服務(wù)來說,檢測任務(wù)由檢測方主動創(chuàng)建,所以我們不需要過多關(guān)注DataProviderBase的實現(xiàn),而是要啟動一個api服務(wù),負(fù)責(zé)創(chuàng)建檢測任務(wù),示例代碼如下:
app.post('/xxx.xxx', async (req, res) => {
const urls = req.body?.urls // 待檢測url列表
const callBack = req.body?.callBack // 調(diào)用方接收報告的回調(diào)接口地址
const transData = req.body?.transData // 調(diào)用方需要在回調(diào)中拿到的透傳數(shù)據(jù)
// 巡檢系統(tǒng)檢測任務(wù)創(chuàng)建函數(shù)
newApp.createJob(urls.map(url => ({ url,
// 在redis任務(wù)隊列中傳遞的信息
pos: { callBack, transData },
})),
jobId => { // 返回任務(wù)id給調(diào)用方
res.json({ taskId: jobId })
}
)
})
4.3 頁面檢測
PageInspectorBase(頁面檢查器基類)是卡口服務(wù)的改造重點,在這個基類的子類實現(xiàn)方面,我們需要去做前文提到的具體待實現(xiàn)的檢測case,主要有兩類檢測case:
- 請求資源型檢測case:在子類中覆寫onResponse方法,針對不同的資源類型執(zhí)行不同的檢測邏輯;
- 運行時檢測case:在子類中覆寫onPageOpen方法,通過基類傳入的Page對象,注入js腳本,執(zhí)行頁面運行時檢測;
// 頁面檢測類
class PageInspector extends PageInspectorBase {
// ...
// 針對不同資源類型檢測方法配置Map
checkResponseMethodsMap = new Map([['image', this.checkImageResponse]])
// 請求資源型檢測入口 針對請求資源進行檢測
async onResponse(response: Response, reporter: PageReporter, data: IJobItem) {
const resourceType = response.request().resourceType()
const checkMethod = this.checkResponseMethodsMap.get(resourceType)
await checkMethod(response, reporter, data)
}
// 檢測圖片資源
async checkImageResponse(response: Response, reporter: PageReporter, data: IJobItem) {
// ...
if (imageCdnList.includes(url)) {reporter.add({ errorType: "圖片類型錯誤.非cdn資源" })}
// ...
}
// 運行時檢測入口 在頁面打開時執(zhí)行注入的js腳本進行運行時檢測
async onPageOpen(page, reporter: PageReporter, data) {
// ...
const htmlText = await page.evaluate('window.document.documentElement.innerHTML')
const phoneRegex = /\b((?:\+?86)?1(?:3\d{3}|5[^4\D]\d{2}|8\d{3}|7(?:[35678]\d{2}|4(?:0\d|1[0-2]|9\d))|9[189]\d{2}|66\d{2})\d{6})\b/g;
let phoneMatch: RegExpExecArray
let collectMessage = []
while ((phoneMatch = phoneRegex.exec(html)) !== null) {
const phone = phoneMatch[1];collectMessage.push(`手機號碼:${phone}`);
}
collectMessage.forEach(val => {reporter.add({ errorMessage: `敏感信息:${val}`})})
// ...
}
// ...
}
RegExp.prototype.exec()
在設(shè)置了 global 或 sticky 標(biāo)志位的情況下(如 /foo/g 或 /foo/y),JavaScript RegExp 對象是有狀態(tài)的。它們會將上次成功匹配后的位置記錄在 lastIndex 屬性中。使用此特性,exec() 可用來對單個字符串中的多次匹配結(jié)果進行逐條的遍歷(包括捕獲到的匹配),而相比之下, String.prototype.match() 只會返回匹配到的結(jié)果。
4.4 報告與回調(diào)
檢測任務(wù)執(zhí)行完畢后,reporter_queue中會被創(chuàng)建一個新的“報告生成任務(wù)”,主程序調(diào)用繼承了DataReporterBase的子類進行以下操作:
- 對檢測項逐一整理,將搜集到的錯誤進行等級分類,整理出報告源數(shù)據(jù);
- 根據(jù)報告源數(shù)據(jù)結(jié)合ejs模板生成靜態(tài)html并上傳,得到在線檢測報告地址;
- 向調(diào)用方回調(diào)檢測報告。
4.4.1 利用ChatGPT生成報告模板
大部分研發(fā)的UI設(shè)計水平有目共睹,與其死磕報告模板的樣式和交互,不如交給AI去搞定,輸入預(yù)先定義好的數(shù)據(jù)結(jié)構(gòu)和要求,ChatGPT能非??焖俚亟o到一個模板,然后只需要稍微做一點點調(diào)整就能達(dá)到可用的狀態(tài)。
Q: 我需要你寫一個ejs模板,這個模板用來展示一個統(tǒng)計報告,為了樣式和交互方便,模板會通過內(nèi)置bootstrap的css樣式和Jquery,你可以用到他們來實現(xiàn)html元素的組合和設(shè)置交互,具體的數(shù)據(jù)結(jié)構(gòu)如下:
interface CheckResult {
name: string;
status: 0 | 1;
message: string;
severityLevel: 0 | 1;
}
interface DetectionResult {
url: string;
status: 0 | 1;
checkResults: CheckResult[];
}
interface InspectorReport {
taskId: string;
reporterUrl?: string;
detectionResults: DetectionResult[];
startTimeStr: string;
endTimeStr: string;
duration: number;
pageTotal: number;
taskStat: {
completed: number,
waiting: number,
active: number,
delayed: number,
paused: number,
failed: number,
}
}
export interface InspectorReportBase {
manifest: IManifest,
data: InspectorReport;
}
export interface IManifest {
name: string
desc?: string
notifyTo?: string[]
}
A: 好的,以下是基于提供的數(shù)據(jù)結(jié)構(gòu)設(shè)計的ejs模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet">
<script src="https://xxx.xxx.js"></script>
<title>
<%=data.manifest.desc%>
</title>
</head>
<body>
<div class="container">
<h2 class="text-center">
<%= data.manifest.desc %> 統(tǒng)計報告
</h2>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">任務(wù)信息</h5>
<p class="card-text">任務(wù)ID: <%= data.data.taskId %>
</p>
<p class="card-text">檢測開始時間: <%= data.data.startTimeStr %>
</p>
<p class="card-text">檢測結(jié)束時間: <%= data.data.endTimeStr %>
</p>
<p class="card-text">檢測時長: <%= data.data.duration %> 秒</p>
<p class="card-text">檢測頁面數(shù): <%= data.data.pageTotal %>
</p>
<!--以下省略 -->
</body>
</html>
AI生成的在線報告模板:
4.4.2 與調(diào)用方解耦
作為服務(wù)提供者,卡口服務(wù)并不想過多關(guān)注調(diào)用者是誰,那么要如何實現(xiàn)與調(diào)用方的解耦?
——回顧上文“創(chuàng)建任務(wù)接口”環(huán)節(jié),這個接口設(shè)計的入?yún)⒂衏allBack和transData字段:
- callBack:檢測任務(wù)結(jié)束時,卡口服務(wù)將調(diào)用該地址回傳檢測報告;
- transData:檢測任務(wù)結(jié)束時,需要透傳給callBack的數(shù)據(jù);
頁面檢測任務(wù)完成后,在回調(diào)測試報告環(huán)節(jié),卡口服務(wù)將從redis隊列任務(wù)的緩存中中取出這兩個值,使用POST請求將報告和transData發(fā)送給callBack。
// 卡口服務(wù)回調(diào)示例代碼
axios.post(callBack, {
data: { msg: "本次檢測檢測報告如下:xxxxx", transData: `透傳的數(shù)據(jù)如下:${transData}` }
})
在后續(xù)的規(guī)劃中,為了使卡口服務(wù)能適應(yīng)更多場景的不同需求,參考后端微服務(wù)注冊中心的概念,可以實現(xiàn)一個簡易的注冊中心的抽象模型,進一步解耦卡口服務(wù)與其調(diào)用方之間的邏輯,同時能拓展更多功能:自定義檢測項、自定義報告模板等。
5、總結(jié)
對于卡口服務(wù)來說,學(xué)習(xí)和閱讀巡檢的源碼是一個重要的前置工作。通過深入理解巡檢系統(tǒng)的實現(xiàn)細(xì)節(jié)和底層架構(gòu)設(shè)計可以更好地理解巡檢系統(tǒng)是如何工作的,從而更好地進行定制和擴展,這些經(jīng)驗也幫助提升了自己的編碼能力和設(shè)計能力,在后續(xù)的技術(shù)項目中可以得到應(yīng)用和實踐。希望閱讀完本文的開發(fā)同學(xué)都能從本篇實踐總結(jié)中有所收獲~
引用/參考鏈接
GitHub - OptimalBits/bull
RegExp.prototype.exec() - JavaScript | MDN