在 Node.js 中,提供了[console](https://nodejs.org/dist/latest-v18.x/docs/api/console.html)模塊,這是一個簡單的調試控制臺,其功能類似于瀏覽器提供的 JavaScript 控制臺。
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。?
## 一、原理
  與瀏覽器一樣,Node.js 也提供了一個全局變量 console(實例化 Console 類),可調用 log()、error() 等方法。
**1)同步還是異步**
  console 的方法既不像瀏覽器中那樣始終同步,也不像 Node.js 中的流那樣始終異步。
  是否為同步取決于鏈接的是什么流以及操作系統是 Windows 還是 POSIX:
* 文件:在 Windows 和 POSIX 中都是同步。
* TTY(終端):在 Windows 上是異步,在 POSIX 上是同步。
* 管道和套接字:在 Windows 上是同步,在 POSIX 上是異步。
  可移植操作系統接口(Portable Operating System Interface,縮寫為POSIX)是 IEEE 為要在各種 UNIX 操作系統上運行軟件,而定義API的一系列互相關聯的標準的總稱。
  console 的這些行為部分是出于歷史原因,因為一旦將它們更改,那么會導致向后不兼容(即不兼容舊版本)。
  注意,同步寫入會阻塞事件循環,直至寫入完成。
  在輸出到文件的情況下,這可能幾乎是瞬時的。
  但在系統負載高、接收端未讀取管道或終端或文件系統速度較慢的情況下,事件循環可能經常被阻塞足夠長,足以對性能產生嚴重的負面影響。
**2)log() 和 error()**
  在[src/lib/internal/console/constructor.js](https://github.com/nodejs/node/blob/master/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()](https://nodejs.org/dist/latest-v18.x/docs/api/util.html#utilinspectobject-options)方法,可將對象解析成字符串,并在控制臺著色,如下所示。
~~~
const util = require('util');
const str = util.inspect({ name: "strick" }, { colors: true });
process.stdout.write(str +'\n');
~~~
:-: 
## 二、bunyan.js
  目前在我們的項目中,使用的日志庫是[bunyan.js](https://github.com/trentm/node-bunyan),這個庫不僅支持終端和 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()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)的異常,第二組 try-catch 用于處理 JSON.stringify(rec, safeCycles()) 的異常。
  最后會判斷是否安裝了[safe-json-stringify](https://github.com/debitoor/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](http://www.hmoore.net/pwstrick/fe-questions/1094987)數據類型處理,另一種是用數組處理。
  解決思路其實差不多,都是依次將屬性值加到數據結構中,當判斷到已存在時,就返回 \[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]"}
~~~
參考資料:
[日志模塊](https://www.nodejs.red/#/nodejs/console)
[process對象](https://javascript.ruanyifeng.com/nodejs/process.html)
[官網 process](https://nodejs.org/dist/latest-v18.x/docs/api/process.html#a-note-on-process-io)?[console](https://nodejs.org/dist/latest-v18.x/docs/api/console.html)
*****
> 原文出處:
[博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020