1 背景
體驗是得物的業務關鍵詞之一,對於前端開發而言,提高用户體驗更是重要工作內容之一。
得物前端平台目前有巡檢系統、監控平台等多種手段保障線上頁面穩定運行,但是仍有一部分問題處於“監控死角”,而且巡檢、監控都屬於後置告警手段,為了確保頁面上線前就能得到一定的用户體驗保障,結合公司的戰略目標,我們決定開發一個H5頁面檢測服務,用來前置檢測即將上線的頁面,提前暴露該頁面可能存在的問題反饋給對應的開發/運營,我們將這個服務稱之為:“體驗卡口”。
本文從這次“體驗卡口”服務的開發實踐出發,同時介紹得物巡檢系統的架構和設計,希望能給參與穩定性建設的開發小夥伴提供一定的學習和參考價值。
2 用户體驗量化標準
當我們試圖量化影響用户體驗的問題時,需要思考以下兩個主要問題:
什麼影響了用户體驗?
我們已經通過豐富的數據支撐和實踐經驗,對影響用户體驗的因素有了深入瞭解。從過去的線上問題反饋收集和開發經驗中,我們將體驗問題大致分為兩個等級:
P0級:這些問題嚴重影響頁面加載速度或涉及到安全風險,例如頁面包含超大的圖片/媒體資源、頁面中含有個人隱私信息;
P1級:這些問題可能對用户體驗造成潛在影響,例如頁面中存在響應時間超過300ms的接口請求。
如何檢測以及量化問題?
一旦我們對體驗問題進行了定義和分級,接下來需要建立適當的機制來檢測這些問題。對於卡口服務,我們可以採取以下步驟來量化問題、轉換為可執行的檢測代碼,並通過卡口服務生成相應的檢測報告供調用方使用:
- 確定指標和標準:首先,我們需要確定用於量化體驗問題的指標和標準。例如,對於接口請求速度問題,可以使用接口響應時間作為指標,同時設定一定的標準,例如超過特定時間閾值即視為問題。
- 編寫自動化腳本:基於指標和標準,我們可以編寫自動化腳本來模擬用户在無頭瀏覽器中執行相關操作,例如加載頁面、點擊按鈕、發送請求等。這些腳本將根據設定的指標進行性能測量和問題檢測。
- 使用無頭瀏覽器執行測試:我們可以在無頭瀏覽器中運行自動化腳本,模擬用户行為並收集相應的性能數據。
- 結果分析和報告生成:通過收集的性能數據,我們可以進行結果分析,並將問題和相關數據轉化為檢測報告。該報告可以包括問題的詳細描述、問題等級、相關性能指標和數據。
提供給調用方:最後,通過卡口服務,我們可以將生成的檢測報告提供給調用方。調用方可以根據報告中的問題和數據進行相應的優化和改進,以提升用户體驗。
這樣的機制可以幫助我們自動化地檢測和量化體驗問題,並提供可執行的檢測代碼和相關報告。這樣一來,我們可以更有效地識別和解決問題,並提供準確的數據給予開發團隊進行優化。
以下是我們整理出需要具體實現的檢測case:
收集完具體的影響用户體驗的case之後,要確定具體的開發方案,由於卡口服務與得物前端平台巡檢系統有很多技術實現重合的部分,所以我們決定利用現有的巡檢架構,將“體驗卡口”集成到現有的巡檢系統中,可以節省大量的開發時間。
3 巡檢系統基礎架構
巡檢系統的程序目標一句話總結:定時從數據源獲取待檢測頁面地址列表,然後進行批量檢測並生成報告。
為了應對不同場景下的個性化需求,巡檢系統抽象出了三個巡檢器基類,各場景繼承基類實現定製需求。
3.1 巡檢器基類
1. DataProviderBase(數據提供基類):
dataSlim(): 簡化冗餘數據;
fetchData(): 獲取遠程數據,處理並返回待檢測頁面url列表;
isSkipTime(): 用來設置條件,在某些特定條件下跳過定時任務;
schedule(): 設置定時任務運行區間;
2. PageInspectorBase(頁面檢查器基類):
check(): 檢查器入口,用來打開指定的檢測頁面,並初始化各種資源的監聽;
injectRequestHeaders(): 注入頁面接口請求需要的cookie、token等;
urlCheck(): url地址檢查;
onRequest(): 監聽頁面請求;
onResponse(): 監聽頁面響應;
onPageError(): 監聽頁面錯誤;
3. DataReporterBase(數據報告基類):
buildReporter(): 根據採集到的錯誤信息生成檢測報告;
feishuNotify(): 將生成的報告通過飛書發送到指定的通知羣;
getHTMLReporterUrl(): 根據ejs模板將報告生成html靜態文件並上傳,返回在線報告地址;
我們可以形象地將這三個基類比成一家飯店的三個不同分工的部門,能更方便地去理解它:
飯店前台負責接收顧客提供的訂單,後廚根據訂單下料炒菜裝盤,服務員將做好的飯菜提供給顧客。
DataProviderBase(數據提供基類): 負責定時輪詢接收外部提供的待檢測頁面列表。這個組件類似於飯店前台,接收顧客提供的訂單。它負責從外部獲取待檢測的頁面列表,並將這些頁面傳遞給檢測器進行檢測。
PageInspectorBase(頁面檢查器基類): 逐一檢測頁面列表中的每一個URL,並檢測頁面中的潛在問題。類似於後廚根據訂單下料、炒菜和裝盤的過程,這個組件負責逐個檢測待檢測頁面列表中的URL,並對每個頁面進行問題檢測。它可以使用一系列的檢測方法和規則,以確定頁面是否存在潛在問題。
DataReporterBase(數據報告基類): 將檢測蒐集的問題進一步整理後發送報告。類似於服務員將做好的飯菜提供給顧客,這個組件負責將經過檢測的問題進行整理和彙總,並生成相應的報告。報告可以包括問題的描述、嚴重程度、相關頁面URL等信息。然後,報告可以被髮送給相關的利益相關者,例如開發或運營。
3.2 巡檢器
基於以上三個基類,根據不同巡檢場景開發不同的巡檢器(inspector),每一個巡檢器都包含了分別繼承以上三個基類的三個子類,繼承了基類的子類巡檢器通過覆寫/拓展基類方法以實現自己的個性化需求,以下是一個極簡的巡檢器例子:
// data-provider.ts
export class DataProvider extends DataProviderBase {
// 實現特定的頁面列表獲取邏輯
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 巡檢主程序
在巡檢系統中,每個頁面的檢測任務都是獨立的異步任務,並且每份檢測報告的整理和發送也是獨立的異步任務。為了方便管理和維護這些異步任務以及任務消息的存儲和傳遞,巡檢系統使用Redis結合Bull作為巡檢系統的異步任務管理工具。
Redis是一個內存數據庫,它提供高性能的數據存儲和訪問能力。
Bull是一個基於Redis的任務隊列庫,它提供了任務的調度、執行和消息傳遞的功能。
有了巡檢器和異步任務管理能力,主程序的主要工作如下:
- 定義任務:使用Bull創建兩個任務隊列,page\_queue用於存放“頁面檢測任務”,reporter\_queue用於存放“報告生成任務”。
- 生產任務:在巡檢系統中,頁面檢測任務和報告生成任務的生產者(主程序)負責將任務添加到相應的隊列中。當巡檢器(inspector)需要進行頁面檢測時,生產者將頁面檢測任務加入page\_queue;當需要生成報告時,生產者將報告生成任務加入reporter\_queue。
- 消費任務:巡檢系統中的任務消費者(主程序)負責從任務隊列中獲取任務並執行,一次檢測任務會有>=1個頁面檢測任務,交由上文介紹的頁面檢查器PageInspector執行頁面檢查,然後將檢測報告存儲到Redis中,當該次檢測任務的所有頁面都完成檢測後,reporter_queue任務被創建並交由巡檢器(inspector)的DataReporter消費。
4 卡口服務
介紹完巡檢系統,接下來我們看如何將卡口服務集成自巡檢系統中。
卡口服務的主要功能用一句話概括:接入巡檢系統的現有架構,對外暴露一個遠程接口,提供給接口調用方主動檢測頁面的能力,然後將檢測報告回傳給調用方。
對比現有巡檢系統與卡口服務的差異:
從上文的巡檢系統架構介紹以及分析上面的表格可知,卡口服務的開發工作就是基於巡檢系統的巡檢器架構去定製實現一個巡檢器。
4.1 卡口服務運行時序
開始開發卡口服務的巡檢器之前,我們先梳理一下整個卡口服務的運行時序:
其中卡口服務主要開發任務:步驟2、3、4、7。
4.2 創建任務接口
我們在上文提到,巡檢是一種後置檢測手段,所以巡檢系統的DataProviderBase(數據提供基類)主要能力是:“定時輪詢接收外部提供的待檢測頁面列表”。
對於卡口服務來説,檢測任務由檢測方主動創建,所以我們不需要過多關注DataProviderBase的實現,而是要啓動一個api服務,負責創建檢測任務,示例代碼如下:
app.post('/xxx.xxx', async (req, res) => {
const urls = req.body?.urls // 待檢測url列表
const callBack = req.body?.callBack // 調用方接收報告的回調接口地址
const transData = req.body?.transData // 調用方需要在回調中拿到的透傳數據
// 巡檢系統檢測任務創建函數
newApp.createJob(urls.map(url => ({ url,
// 在redis任務隊列中傳遞的信息
pos: { callBack, transData },
})),
jobId => { // 返回任務id給調用方
res.json({ taskId: jobId })
}
)
})
4.3 頁面檢測
PageInspectorBase(頁面檢查器基類)是卡口服務的改造重點,在這個基類的子類實現方面,我們需要去做前文提到的具體待實現的檢測case,主要有兩類檢測case:
1. 請求資源型檢測case:在子類中覆寫onResponse方法,針對不同的資源類型執行不同的檢測邏輯;
2. 運行時檢測case:在子類中覆寫onPageOpen方法,通過基類傳入的Page對象,注入js腳本,執行頁面運行時檢測;
//
頁面檢測類
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資源" })}
// ...
}
// 運行時檢測入口 在頁面打開時執行注入的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()
在設置了 global 或 sticky 標誌位的情況下(如 /foo/g 或 /foo/y),JavaScript RegExp 對象是有狀態的。它們會將上次成功匹配後的位置記錄在 lastIndex 屬性中。使用此特性,exec() 可用來對單個字符串中的多次匹配結果進行逐條的遍歷(包括捕獲到的匹配),而相比之下, String.prototype.match() 只會返回匹配到的結果。
4.4 報告與回調
檢測任務執行完畢後,reporter_queue中會被創建一個新的“報告生成任務”,主程序調用繼承了DataReporterBase的子類進行以下操作:
- 對檢測項逐一整理,將蒐集到的錯誤進行等級分類,整理出報告源數據;
- 根據報告源數據結合ejs模板生成靜態html並上傳,得到在線檢測報告地址;
- 向調用方回調檢測報告。
4.4.1 利用ChatGPT生成報告模板
大部分研發的UI設計水平有目共睹,與其死磕報告模板的樣式和交互,不如交給AI去搞定,輸入預先定義好的數據結構和要求,ChatGPT能非常快速地給到一個模板,然後只需要稍微做一點點調整就能達到可用的狀態。
Q: 我需要你寫一個ejs模板,這個模板用來展示一個統計報告,為了樣式和交互方便,模板會通過內置bootstrap的css樣式和Jquery,你可以用到他們來實現html元素的組合和設置交互,具體的數據結構如下:
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: 好的,以下是基於提供的數據結構設計的ejs模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://xxx.xxx.css" 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 %> 統計報告
</h2>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">任務信息</h5>
<p class="card-text">任務ID: <%= data.data.taskId %>
</p>
<p class="card-text">檢測開始時間: <%= data.data.startTimeStr %>
</p>
<p class="card-text">檢測結束時間: <%= data.data.endTimeStr %>
</p>
<p class="card-text">檢測時長: <%= data.data.duration %> 秒</p>
<p class="card-text">檢測頁面數: <%= data.data.pageTotal %>
</p>
<!--以下省略 -->
</body>
</html>
AI生成的在線報告模板:
4.4.2 與調用方解耦
作為服務提供者,卡口服務並不想過多關注調用者是誰,那麼要如何實現與調用方的解耦?
——回顧上文“創建任務接口”環節,這個接口設計的入參有callBack和transData字段:
- callBack:檢測任務結束時,卡口服務將調用該地址回傳檢測報告;
- transData:檢測任務結束時,需要透傳給callBack的數據;
頁面檢測任務完成後,在回調測試報告環節,卡口服務將從redis隊列任務的緩存中中取出這兩個值,使用POST請求將報告和transData發送給callBack。
卡口服務回調示例代碼
axios.post(callBack, {
data: { msg: "本次檢測檢測報告如下:xxxxx", transData: `透傳的數據如下:${transData}` }
})
在後續的規劃中,為了使卡口服務能適應更多場景的不同需求,參考後端微服務註冊中心的概念,可以實現一個簡易的註冊中心的抽象模型,進一步解耦卡口服務與其調用方之間的邏輯,同時能拓展更多功能:自定義檢測項、自定義報告模板等。
5 總結
對於卡口服務來説,學習和閲讀巡檢的源碼是一個重要的前置工作。通過深入理解巡檢系統的實現細節和底層架構設計可以更好地理解巡檢系統是如何工作的,從而更好地進行定製和擴展,這些經驗也幫助提升了自己的編碼能力和設計能力,在後續的技術項目中可以得到應用和實踐。希望閲讀完本文的開發同學都能從本篇實踐總結中有所收穫~
引用/參考鏈接
GitHub - OptimalBits/bull
RegExp.prototype.exec() - JavaScript | MDN
文:航飛
本文屬得物技術原創,來源於:得物技術官網
未經得物技術許可嚴禁轉載,否則依法追究法律責任!