前端日志管理模塊的設(shè)計(jì)與實(shí)現(xiàn)
一、問題背景
在項(xiàng)目中,我們會(huì)頻繁用到 ??console.log()?
? 來輸出一些關(guān)鍵信息到控制臺(tái)中,有助于開發(fā)調(diào)試,以及問題的排查,待項(xiàng)目上線后,這些調(diào)試日志又得及時(shí)清除。
同時(shí)在前端質(zhì)量要求下,我們會(huì)做“前端埋點(diǎn)”,用于遠(yuǎn)程上報(bào)一些關(guān)鍵行為信息,用于在出問題時(shí)還原用戶的操作路徑,復(fù)現(xiàn) BUG,從而解決問題,而各種各樣的上報(bào)若是能在業(yè)務(wù)開發(fā)中抹平差異,也有助于研發(fā)提效。
因此,有必要在團(tuán)隊(duì)中封裝日志工具(Logger),用于統(tǒng)一管理日志輸出和格式化上報(bào),降低開發(fā)者對(duì)多平臺(tái)上報(bào)差異的心智負(fù)擔(dān)。
二、需求概述
預(yù)期日志管理工具(Logger)需要有如下能力:
- 支持區(qū)分?
?info?
??、??warn?
??、??error?
? 三種本地調(diào)試類型日志 - 支持遠(yuǎn)程上報(bào)自定義日志?
?report()?
? - 支持設(shè)置 namespace,用于區(qū)分代碼執(zhí)行的 scope
- 支持鏈?zhǔn)讲僮?/li>
- 區(qū)分生產(chǎn)環(huán)境和開發(fā)環(huán)境,生產(chǎn)環(huán)境禁止輸出日志到控制臺(tái)
- 支持功能可擴(kuò)展
三、方案設(shè)計(jì)
在閱讀完 Axios 的源碼后,個(gè)人認(rèn)為 Axios 里對(duì)于設(shè)計(jì)模式的應(yīng)用是非常靈活,同理,一個(gè)好的日志工具也應(yīng)當(dāng)遵守著一定的軟件設(shè)計(jì)模式原則。
作為項(xiàng)目中用到的日志工具,單例模式應(yīng)當(dāng)是更適合的選擇!
Logger 的打印輸出能力,本質(zhì)上還是借助了 ??window.console?
? 對(duì)象中的方法:
Console 對(duì)象
在面向?qū)ο缶幊讨?,我們可以認(rèn)為 ??console?
? 是一個(gè)已經(jīng)初始化的實(shí)例,同時(shí)也是一個(gè)單例,因?yàn)樗侨治ㄒ弧?/p>
而單例模式的最大好處就是全局唯一,對(duì)于做日志統(tǒng)一管理有著天然的友好支持基礎(chǔ)。
四、實(shí)現(xiàn)細(xì)節(jié) ??
接下來通過具體的代碼,來逐一實(shí)現(xiàn)并完善我們的 Logger 日志工具類。
1、ES Module 下的單例模式
在 ESM 規(guī)范下,我們可以直接通過直接導(dǎo)出實(shí)例方式(??export default new ClassName()?
?),來實(shí)現(xiàn)單例模式。
Logger 的基礎(chǔ)結(jié)構(gòu)就有了:
/**
* 日志打印工具,統(tǒng)一管理日志輸出&上報(bào)
*/
class Logger {
/** 命名空間(scope),用于區(qū)分所在執(zhí)行文件 */
private namespace: string
constructor(namespace = 'unknown') {
this.namespace = namespace
}
}
export default new Logger()
2、可擴(kuò)展的單例模式
參考 Axios 的設(shè)計(jì)[1],因此我們還提供 ??create()?
? 方法,為創(chuàng)建新實(shí)例留一個(gè)入口方法。
/**
* 創(chuàng)建新的 Logger 實(shí)例
*
* @param namespace 命名空間
* @returns Logger
*/
public create(namespace = 'unknown') {
return new Logger(namespace);
}
當(dāng)需要重新定義一個(gè) logger 實(shí)例時(shí),就可以參考如下方式:
import logger from '@/utils/logger'
const newLogger = logger.create('custom')
logger.info(newLogger === logger) // [unknown] false
3、定義“打印”類日志方法
需要區(qū)分 ??info?
??、??warn?
??、??error?
? 三種類型的日志,實(shí)現(xiàn)如下:
定義日志枚舉類型:
const enum LogLevel {
/** 普通日志 */
Log,
/** 警告日志 */
Warning,
/** 錯(cuò)誤日志 */
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 任意參數(shù)
*/
public info(...args: unknown[]) {
this._log(LogLevel.Log, args)
return this
}
/**
* 打印輸出警告信息 ?
*
* @param args 任意參數(shù)
*/
public warn(...args: unknown[]) {
this._log(LogLevel.Warning, args)
return this
}
/**
* 打印輸出錯(cuò)誤信息 ?
*
* @param args 任意參數(shù)
*/
public error(...args: unknown[]) {
this._log(LogLevel.Error, args)
return this
}
在 ??_log()?
?? 方法中,通過 ??__DEV__?
? 環(huán)境變量區(qū)分“生產(chǎn)”和“開發(fā)”:
if (!__DEV__) return
這種變量可以理解為“開關(guān)”:
生產(chǎn)環(huán)境則控制臺(tái)不輸出信息,在實(shí)際應(yīng)用中,可以擴(kuò)展“是否輸出信息”的變量,來針對(duì)性擴(kuò)展,例如線上需要通過特定參數(shù)展示調(diào)試日志,用于線上定位問題,那么就可以綜合多個(gè)條件來決定是否輸出控制臺(tái),畢竟編程最核心的問題是解決需求。
在開發(fā)模式下,針對(duì)不同的信息類型,會(huì)標(biāo)注不同的顏色:
Chrome 瀏覽器下的效果
與此同時(shí),在每個(gè)“輸出”方法中都返回了 ??this?
?(當(dāng)前實(shí)例),因而便可以為鏈?zhǔn)秸{(diào)用方法提供了使用基礎(chǔ)。
4、支持修改 namespace
namespace 最重要的作用是:區(qū)分在不同組件或文件下的日志,便于問題定位排查。
由于 ??Logger?
?? 將所有的輸出集中到了統(tǒng)一文件,在 ??console.log()?
?? 中文件定位永遠(yuǎn)是 ??Logger?
? 類定義實(shí)現(xiàn)所在文件,因此需要 namespace 來區(qū)分。
新增 ??setNamespace()?
? 方法:
/**
* 設(shè)置命名空間(日志前綴)
* @param namespace
*/
public setNamespace(namespace = '') {
this.namespace = `[${namespace}]`
return this
}
在 TypeScript 環(huán)境下,會(huì)提供代碼提示,例如某個(gè)文件下輸出錯(cuò)誤信息的方式:
而 ??setNamespace()?
? 方法,并不是每次都需要調(diào)用的,只需在文件中調(diào)用一次即可。
5、埋點(diǎn)遠(yuǎn)程上報(bào)
在一些關(guān)鍵時(shí)機(jī),例如進(jìn)入頁面、點(diǎn)擊“付費(fèi)按鈕”等一些關(guān)鍵操作上,一般會(huì)加上一些上報(bào)到遠(yuǎn)程,用于記錄用戶操作路徑,以此便于在出現(xiàn)問題后,復(fù)現(xiàn) BUG 并“對(duì)癥下藥”。
而埋點(diǎn)上報(bào)一般有三類:代碼埋點(diǎn)、可視化埋點(diǎn)、無痕埋點(diǎn)。
我們這里通過給 Logger 增加遠(yuǎn)程上報(bào)的方式就是代碼埋點(diǎn)。
一般情況下,埋點(diǎn)上報(bào)屬于“前端監(jiān)控”方面,前端監(jiān)控是一個(gè)獨(dú)立的管理系統(tǒng),它的職能是負(fù)責(zé)前端項(xiàng)目的監(jiān)控、異常報(bào)警等,因此通常會(huì)有用于項(xiàng)目集成的前端 SDK
有了 Logger 實(shí)例,我們可以在 Logger 中直接統(tǒng)一集成“前端監(jiān)控 SDK”的主動(dòng)上報(bào)方法即可!
在 Logger 類中新增三個(gè)方法:
- ?
?reportLog()?
?:上報(bào)日志。 - ?
?reportEvent()?
?:上報(bào)事件。 - ?
?reportException()?
?:上報(bào)異常。
/**
* 遠(yuǎn)程上報(bào)
* TODO: 根據(jù)基建環(huán)境自定義擴(kuò)展
*/
public reportLog() {
this.info() // 用于在本地輸出
}
public reportEvent() {
this.info()
}
public reportException() {
this.error()
}
至于為什么添加著兩個(gè)方法,實(shí)際是根據(jù)“前端監(jiān)控 SDK”提供的 API 來決定
例如常見的 “Sentry - 應(yīng)用監(jiān)控錯(cuò)誤溯源[2]” 平臺(tái),針對(duì)主動(dòng)上報(bào),提供了三種方法,通常為了保持一致性,降低心智負(fù)擔(dān),因此新增對(duì)應(yīng)的三個(gè)上報(bào)方法。
具體的上報(bào)參數(shù)和邏輯,則需要大家根據(jù)自己的業(yè)務(wù)區(qū)擴(kuò)展。
五、Logger 的可擴(kuò)展性 ??
從上面 Logger 類的實(shí)現(xiàn),可以發(fā)現(xiàn)一個(gè)明顯的問題,如果業(yè)務(wù)需要擴(kuò)展功能,則需要修改 Logger 類內(nèi)部的方法,Logger 類中的方法和邏輯,我們可以理解為是所有業(yè)務(wù)都通用的,業(yè)務(wù)定制化的功能應(yīng)該通過額外擴(kuò)展方式來完善。
那有沒有什么辦法,可以實(shí)現(xiàn)不修改方法,而擴(kuò)展 Logger 的功能吶?
1、擴(kuò)展方案
有幾個(gè)方案:
- 繼承 Logger 類擴(kuò)展。
- 增加回調(diào)函數(shù)作為參數(shù)。
個(gè)人推薦第二個(gè)方案,但如果每一次調(diào)用,都按照如下方式:
logger.info('message', () => {})
但這種設(shè)計(jì)比較粗糙
2、攔截器
參考 Axios 的攔截器設(shè)計(jì),也就是 AOP(面向切面編程模式)的設(shè)計(jì)思想,來擴(kuò)展 ??_log()?
? 方法。
新增類型申明:
/**
* 日志的配置類型
*/
type LoggerConfigType = {
/** 命名空間 */
namespace?: string
}
/**
* 攔截器函數(shù)類型
*/
type InterceptorFuncType = (config: LoggerConfigType) => void
將 Logger 的配置集中的 ??config?
?? 私有變量中,并新增 ??addBeforeFunc()?
?? 和 ??addAfterFunc()?
? 兩個(gè)方法,用于新增自定義“攔截器”函數(shù)
其中一個(gè)細(xì)節(jié)是,日志打印之后的攔截器,按照FCLS(First Come Last Serve,先到后服務(wù))的策略,和 Axios 的響應(yīng)攔截器執(zhí)行順序?qū)R,與此同時(shí),攔截器函數(shù)中會(huì)注入當(dāng)前 Logger 的 ??config?
? 配置。
通過簡單的“攔截器”,即可實(shí)現(xiàn)功能的擴(kuò)展,這種方式的功能擴(kuò)展不會(huì)影響到主體功能,后期的維護(hù)升級(jí)是無侵入性的,還算比較優(yōu)雅的,是吧!
3、其他方案
這里還可以考慮更多設(shè)計(jì),例如參考發(fā)布訂閱設(shè)計(jì)模式來改造,通過生命周期的關(guān)鍵點(diǎn),被動(dòng)觸發(fā),主動(dòng)通知并執(zhí)行所有訂閱了對(duì)應(yīng)消息的事件,可以參閱《聊一聊發(fā)布訂閱設(shè)計(jì)模式[3]》
也可以用插件模式方式來實(shí)現(xiàn)擴(kuò)展,類似發(fā)布訂閱模式,給 ??_log()?
? 函數(shù)添加執(zhí)行的鉤子函數(shù)??(回調(diào)函數(shù)),例如這種設(shè)計(jì)下,把“埋點(diǎn)上報(bào)”等功能拆分成插件,再實(shí)現(xiàn)一個(gè)簡單的事件隊(duì)列模型,集成一下子!
六、總結(jié)
至此,一個(gè)基本的日志工具就實(shí)現(xiàn)完成了,但并未完完全全遵守設(shè)計(jì)原則,這里在生產(chǎn)實(shí)踐中還需要封裝、抽離相應(yīng)“職責(zé)”,增加可維護(hù)性。
在團(tuán)隊(duì)中以此作為基礎(chǔ)結(jié)構(gòu),然后針對(duì)團(tuán)隊(duì)、項(xiàng)目、業(yè)務(wù)的特點(diǎn)做適當(dāng)?shù)臄U(kuò)展,構(gòu)建符合當(dāng)前團(tuán)隊(duì)特性的通用日志工具模塊,應(yīng)該也不是什么難事!