在 Node.js 中,提供了console模組,這是一個簡單的偵錯控制檯,其功能類似於瀏覽器提供的 JavaScript 控制檯。
本系列所有的範例原始碼都已上傳至Github,點選此處獲取。
與瀏覽器一樣,Node.js 也提供了一個全域性變數 console(範例化 Console 類),可呼叫 log()、error() 等方法。
1)同步還是非同步
console 的方法既不像瀏覽器中那樣始終同步,也不像 Node.js 中的流那樣始終非同步。
是否為同步取決於連結的是什麼流以及作業系統是 Windows 還是 POSIX:
可移植作業系統介面(Portable Operating System Interface,縮寫為POSIX)是 IEEE 為要在各種 UNIX 作業系統上執行軟體,而定義API的一系列互相關聯的標準的總稱。
console 的這些行為部分是出於歷史原因,因為一旦將它們更改,那麼會導致向後不相容(即不相容舊版本)。
注意,同步寫入會阻塞事件迴圈,直至寫入完成。
在輸出到檔案的情況下,這可能幾乎是瞬時的。
但在系統負載高、接收端未讀取管道或終端或檔案系統速度較慢的情況下,事件迴圈可能經常被阻塞足夠長,足以對效能產生嚴重的負面影響。
2)log() 和 error()
在src/lib/internal/console/constructor.js中,儲存著 console.log() 和 console.error() 兩個方法的原始碼。
下面是刪減過的原始碼,可以看到在輸出之前會自動加換行符,並且 log() 和 error() 使用的輸出方法還不同。
function(streamSymbol, string) { const groupIndent = this[kGroupIndent]; const useStdout = streamSymbol === kUseStdout; // 若是普通輸出,則用 process.stdout 輸出,若是錯誤,則用 process.stderr 輸出 const stream = useStdout ? this._stdout : this._stderr; const errorHandler = useStdout ? this._stdoutErrorHandler : this._stderrErrorHandler; if (groupIndent.length !== 0) { if (StringPrototypeIncludes(string, "\n")) { string = StringPrototypeReplace(string, /\n/g, `\n${groupIndent}`); } string = groupIndent + string; } // 末尾加換行 string += "\n"; try { // 控制檯輸出 stream.write(string, errorHandler); } catch (e) { // Console is a debugging utility, so it swallowing errors is not // desirable even in edge cases such as low stack space. if (isStackOverflowError(e)) throw e; // Sorry, there's no proper way to pass along the error here. } finally { stream.removeListener("error", noop); } }
若是 log() 輸出,則用 process.stdout.write() 方法;若是 error() 輸出,則用 process.stderr.write() 方法。
process.stdout 返回的是一個流,配合 util 模組的inspect()方法,可將物件解析成字串,並在控制檯著色,如下所示。
const util = require('util'); const str = util.inspect({ name: "strick" }, { colors: true }); process.stdout.write(str +'\n');
目前在我們的專案中,使用的紀錄檔庫是bunyan.js,這個庫不僅支援終端和 Node.js 環境,還支援瀏覽器環境。
1)原理
從 Github 上下載原始碼後,可以看到 bin 和 lib 兩個目錄,終端的程式碼存於前者,專案中參照的程式碼存於後者。
下面是一個簡單的範例,引入後也不用範例化,可直接呼叫方法,log.info() 相當於 console.log(),不過前者會自動將物件轉換成字串。
const bunyan = require('bunyan'); const log = bunyan.createLogger({name: "example"}); log.info("strick");
在紀錄檔內部,會為每條紀錄檔維護一個物件,像上面的 strick 字串,在內部生成的物件格式如下。
{ name: 'example', hostname: '192.168.0.101', pid: 94371, level: 30, msg: 'strick', time: 2022-05-11T07:21:51.310Z, v: 0 }
下面是紀錄檔輸出的核心程式碼(已做刪減),其中 s.stream 使用的也是 process.stdout 類。
fastAndSafeJsonStringify() 函數用於將物件轉換成字串,它的工作原理會在後文分析。
Logger.prototype._emit = function (rec, noemit) { var i; // 將物件轉換成字串 var str; if (noemit || this.haveNonRawStreams) { str = fastAndSafeJsonStringify(rec) + os.EOL; } if (noemit) return str; var level = rec.level; for (i = 0; i < this.streams.length; i++) { var s = this.streams[i]; if (s.level <= level) { // 輸出原始物件或字串 s.stream.write(s.raw ? rec : str); } } return str; };
預設執行 node src.js,在控制檯輸出時是沒有著色的,不便於閱讀。
在本地偵錯 bunyan 庫時,可以加 node src.js | ../bin/bunyan,執行此命令後,不僅能著色,還會格式化成可讀性更高的字串,如下所示。
注意,在專案中載入此庫後,命令中就不需要加路徑了,例如 node index.js | bunyan。
2)fastAndSafeJsonStringify()
此函數會在 JSON 物件序列化時,處理物件中的迴圈和 getter 異常,如下所示。
第一組 try-catch 用於處理JSON.stringify()的異常,第二組 try-catch 用於處理 JSON.stringify(rec, safeCycles()) 的異常。
最後會判斷是否安裝了safe-json-stringify庫,若安裝就用該庫處理,否則就輸出報錯資訊。
function fastAndSafeJsonStringify(rec) { try { return JSON.stringify(rec); } catch (ex) { try { return JSON.stringify(rec, safeCycles()); } catch (e) { // 安裝了 safe-json-stringify 庫 if (safeJsonStringify) { return safeJsonStringify(rec); } else { var dedupKey = e.stack.split(/\n/g, 3).join("\n"); _warn( "bunyan: ERROR: Exception in " + "`JSON.stringify(rec)`. You can install the " + '"safe-json-stringify" module to have Bunyan fallback ' + "to safer stringification. Record:\n" + _indent(format("%s\n%s", util.inspect(rec), e.stack)), dedupKey ); return format( "(Exception in JSON.stringify(rec): %j. " + "See stderr for details.)", e.message ); } } } }
接下來看一個迴圈參照的例子,如下所示,在呼叫 JSON.stringify() 方法時會報錯:TypeError: Converting circular structure to JSON。
一般自己寫的 JSON 物件很少會出現迴圈參照,但是在一些比較複雜的物件內部有可能會出現。
const man = { child: {} } man.child = man; JSON.stringify(man);
在 bunyan.js 的內部給出了兩種解決方案,一種是用Set資料型別處理,另一種是用陣列處理。
解決思路其實差不多,都是依次將屬性值加到資料結構中,當判斷到已存在時,就返回 [Circular],終止其後面屬性的序列化。
var safeCycles = typeof (Set) !== 'undefined' ? safeCyclesSet : safeCyclesArray; // Set 方式 function safeCyclesSet() { var seen = new Set(); return function (key, val) { // 若 value 不存在或不是物件型別,則返回該值 if (!val || typeof val !== "object") { return val; } // 若 seen 中包含該值,則返回 [Circular] if (seen.has(val)) { return "[Circular]"; } seen.add(val); return val; }; } // 陣列方式 function safeCyclesArray() { var seen = []; return function (key, val) { if (!val || typeof val !== "object") { return val; } if (seen.indexOf(val) !== -1) { return "[Circular]"; } seen.push(val); return val; }; }
JSON.stringify() 方法的第二個引數是一個回撥,被序列化的值的每個屬性都會經過該函數的處理。
在呼叫 safeCyclesSet() 函數後,就能輸出 {"child":"[Circular]"}。
JSON.stringify(man, safeCyclesSet()); // {"child":"[Circular]"}
參考資料: