前端日誌管理模塊的設計與實現
給團隊封裝一個簡單統一的日誌管理工具 / 模塊,來替換掉項目中野蠻生長的
console.log()
吧!
一、問題背景 ⛰️
在項目中,我們會頻繁用到 console.log()
來輸出一些關鍵信息到控制檯中,有助於開發調試,以及問題的排查,待項目上線後,這些調試日誌又得及時清除。
同時在前端質量要求下,我們會做 “前端埋點”,用於遠程上報一些關鍵行爲信息,用於在出問題時還原用戶的操作路徑,復現 BUG,從而解決問題,而各種各樣的上報若是能在業務開發中抹平差異,也有助於研發提效。
因此,有必要在團隊中封裝日誌工具(Logger),用於統一管理日誌輸出和格式化上報,降低開發者對多平臺上報差異的心智負擔。
二、需求概述 🧾
預期日誌管理工具(Logger)需要有如下能力:
-
支持區分
info
、warn
、error
三種本地調試類型日誌 -
支持遠程上報自定義日誌
report()
-
支持設置 namespace,用於區分代碼執行的 scope
-
支持鏈式操作
-
區分生產環境和開發環境,生產環境禁止輸出日誌到控制檯
-
支持功能可擴展
三、方案設計 🖌️
在閱讀完 Axios 的源碼後,個人認爲 Axios 裏對於設計模式的應用是非常靈活,同理,一個好的日誌工具也應當遵守着一定的軟件設計模式原則。
作爲項目中用到的日誌工具,單例模式應當是更適合的選擇!
Logger 的打印輸出能力,本質上還是藉助了 window.console
對象中的方法:
Console 對象
在面向對象編程中,我們可以認爲 console
是一個已經初始化的實例,同時也是一個單例,因爲它是全局唯一。
而單例模式的最大好處就是全局唯一,對於做日誌統一管理有着天然的友好支持基礎。
四、實現細節 🔍
接下來通過具體的代碼,來逐一實現並完善我們的 Logger 日誌工具類。
4.1 ES Module 下的單例模式
在 ESM 規範下,我們可以直接通過直接導出實例方式(export default new ClassName()
),來實現單例模式。
Logger 的基礎結構就有了:
/**
* 日誌打印工具,統一管理日誌輸出&上報
*/
class Logger {
/** 命名空間(scope),用於區分所在執行文件 */
private namespace: string
constructor(namespace = 'unknown') {
this.namespace = namespace
}
}
export default new Logger()
4.2 可擴展的單例模式
參考 Axios 的設計 [1],因此我們還提供 create()
方法,爲創建新實例留一個入口方法。
/**
* 創建新的 Logger 實例
*
* @param namespace 命名空間
* @returns Logger
*/
public create(namespace = 'unknown') {
return new Logger(namespace);
}
當需要重新定義一個 logger 實例時,就可以參考如下方式:
import logger from '@/utils/logger'
const newLogger = logger.create('custom')
logger.info(newLogger === logger) // [unknown] false
4.3 定義 “打印” 類日誌方法
需要區分 info
、warn
、error
三種類型的日誌,實現如下:
定義日誌枚舉類型:
const enum LogLevel {
/** 普通日誌 */
Log,
/** 警告日誌 */
Warning,
/** 錯誤日誌 */
Error,
}
const Styles = ['color: green;', 'color: orange;', 'color: red;']
const Methods = ['info', 'warn', 'error'] as const
private _log(level: LogLevel, args: unknown[]) {
if (!__DEV__) return
console[Methods[level]](`%c${this.namespace}`, Styles[level], ...args)
}
/**
* 打印輸出信息 🐛
*
* @param args 任意參數
*/
public info(...args: unknown[]) {
this._log(LogLevel.Log, args)
return this
}
/**
* 打印輸出警告信息 ❕
*
* @param args 任意參數
*/
public warn(...args: unknown[]) {
this._log(LogLevel.Warning, args)
return this
}
/**
* 打印輸出錯誤信息 ❌
*
* @param args 任意參數
*/
public error(...args: unknown[]) {
this._log(LogLevel.Error, args)
return this
}
在 _log()
方法中,通過 __DEV__
環境變量區分 “生產” 和“開發”:
if (!__DEV__) return
這種變量可以理解爲 “開關”:
生產環境則控制檯不輸出信息,在實際應用中,可以擴展 “是否輸出信息” 的變量,來針對性擴展,例如線上需要通過特定參數展示調試日誌,用於線上定位問題,那麼就可以綜合多個條件來決定是否輸出控制檯,畢竟編程最核心的問題是解決需求。
在開發模式下,針對不同的信息類型,會標註不同的顏色:
Chrome 瀏覽器下的效果
與此同時,在每個 “輸出” 方法中都返回了 this
(當前實例),因而便可以爲鏈式調用方法提供了使用基礎。
4.4 支持修改 namespace
namespace 最重要的作用是:區分在不同組件或文件下的日誌,便於問題定位排查。
由於 Logger
將所有的輸出集中到了統一文件,在 console.log()
中文件定位永遠是 Logger
類定義實現所在文件,因此需要 namespace 來區分。
新增 setNamespace()
方法:
/**
* 設置命名空間(日誌前綴)
* @param namespace
*/
public setNamespace(namespace = '') {
this.namespace = `[${namespace}]`
return this
}
在 TypeScript 環境下,會提供代碼提示,例如某個文件下輸出錯誤信息的方式:
而 setNamespace()
方法,並不是每次都需要調用的,只需在文件中調用一次即可。
4.5 埋點遠程上報
在一些關鍵時機,例如進入頁面、點擊 “付費按鈕” 等一些關鍵操作上,一般會加上一些上報到遠程,用於記錄用戶操作路徑,以此便於在出現問題後,復現 BUG 並“對症下藥”。
而埋點上報一般有三類:代碼埋點、可視化埋點、無痕埋點
我們這裏通過給 Logger 增加遠程上報的方式就是代碼埋點
一般情況下,埋點上報屬於 “前端監控” 方面,前端監控是一個獨立的管理系統,它的職能是負責前端項目的監控、異常報警等,因此通常會有用於項目集成的前端 SDK
有了 Logger 實例,我們可以在 Logger 中直接統一集成 “前端監控 SDK” 的主動上報方法即可!
在 Logger 類中新增三個方法:
-
reportLog()
:上報日誌 -
reportEvent()
:上報事件 -
reportException()
:上報異常
/**
* 遠程上報
* TODO: 根據基建環境自定義擴展
*/
public reportLog() {
this.info() // 用於在本地輸出
}
public reportEvent() {
this.info()
}
public reportException() {
this.error()
}
至於爲什麼添加着兩個方法,實際是根據 “前端監控 SDK” 提供的 API 來決定
例如常見的 “Sentry - 應用監控錯誤溯源 [2]” 平臺,針對主動上報,提供了三種方法,通常爲了保持一致性,降低心智負擔,因此新增對應的三個上報方法。
具體的上報參數和邏輯,則需要大家根據自己的業務區擴展。
五、Logger 的可擴展性 ⚙️
從上面 Logger 類的實現,可以發現一個明顯的問題,如果業務需要擴展功能,則需要修改 Logger 類內部的方法,Logger 類中的方法和邏輯,我們可以理解爲是所有業務都通用的,業務定製化的功能應該通過額外擴展方式來完善。
那有沒有什麼辦法,可以實現不修改方法,而擴展 Logger 的功能吶?
5.1 擴展方案
有幾個方案:
-
繼承 Logger 類擴展
-
增加回調函數作爲參數
個人推薦第二個方案,但如果每一次調用,都按照如下方式:
logger.info('message', () => {})
但這種設計比較粗糙
5.2 攔截器
參考 Axios 的攔截器設計,也就是 AOP(面向切面編程模式)的設計思想,來擴展 _log()
方法。
新增類型申明:
/**
* 日誌的配置類型
*/
type LoggerConfigType = {
/** 命名空間 */
namespace?: string
}
/**
* 攔截器函數類型
*/
type InterceptorFuncType = (config: LoggerConfigType) => void
將 Logger 的配置集中的 config
私有變量中,並新增 addBeforeFunc()
和 addAfterFunc()
兩個方法,用於新增自定義 “攔截器” 函數
其中一個細節是,日誌打印之後的攔截器,按照 FCLS(First Come Last Serve,先到後服務)的策略,和 Axios 的響應攔截器執行順序對齊,與此同時,攔截器函數中會注入當前 Logger 的 config
配置。
通過簡單的 “攔截器”,即可實現功能的擴展,這種方式的功能擴展不會影響到主體功能,後期的維護升級是無侵入性的,還算比較優雅的,是吧!
5.3 其他方案
這裏還可以考慮更多設計,例如參考發佈訂閱設計模式來改造,通過生命週期的關鍵點,被動觸發,主動通知並執行所有訂閱了對應消息的事件,可以參閱《聊一聊發佈訂閱設計模式 [3]》
也可以用插件模式方式來實現擴展,類似發佈訂閱模式,給 _log()
函數添加執行的鉤子函數🪝(回調函數),例如這種設計下,把 “埋點上報” 等功能拆分成插件,再實現一個簡單的事件隊列模型,集成一下子!
六、總結
至此,一個基本的日誌工具就實現完成了,但並未完完全全遵守設計原則,這裏在生產實踐中還需要封裝、抽離相應 “職責”,增加可維護性。
在團隊中以此作爲基礎結構,然後針對團隊、項目、業務的特點做適當的擴展,構建符合當前團隊特性的通用日誌工具模塊,應該也不是什麼難事!
參考資料
[1]
Axios 源碼閱讀筆記: https://juejin.cn/post/7031945393826955294
[2]
Sentry - 應用監控錯誤溯源: https://sentry.io/welcome/
[3]
聊一聊發佈訂閱設計模式: https://juejin.cn/post/6991749405686628365
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/WdtLHiVAcQ95Ncf5bXZexw