← 部落格

將 React 渲染至 console.log

Text.js 在 DevTools 主控台中繪製 React 應用程式,每個影格呼叫一次帶樣式的 console.log。

Adam

Text.js 在 Chrome DevTools 主控台中渲染 React 應用程式。除了隱藏的輸入欄位,頁面沒有任何 UI。應用程式的每個影格都是一次 console.log 呼叫。

以主控台作為顯示介面

console.log 接受格式字串。每個 %c 都會將下一個引數中的 CSS 套用到後續文字,直到下一個 %c 為止:

console.log("%c▼ Text.js%c ready", "color:#ededed;font-weight:700", "color:#45a557");

這些樣式可以設定顏色、背景、粗體、斜體和底線,而主控台使用等寬字型(macOS 上為 Menlo)。有了等寬字型,加上每段連續字元都能變換顏色,主控台就能作為由帶樣式的字元儲存格組成的網格。邊框、面板、輸入欄位和圖表都能畫在上面。

影格是由儲存格組成的網格

渲染器將每個影格存放在 CellBuffer 中。字元存放在一個型別陣列中,前景色與背景色分別存放在另外兩個陣列中,粗體和底線等屬性則存放在第四個陣列中。這些陣列會依指定大小配置一次,並在每個影格重複使用。

為了記錄影格,驅動程式會逐列走訪,將樣式相同的相鄰儲存格分成連續區段。每個區段會成為一個 %c span,各列則以 \n 串接。我們的 100×32 測試儀表板共有 3200 個儲存格,編碼後約為 200 個 span。

每個影格呼叫一次

將每一列各自記錄為一則訊息並不可行。每則訊息在 DevTools 中都有自己的邊框和原始碼連結,而 DevTools 會將連續相同的訊息摺疊成一則,並顯示重複次數,因此表格中兩個相同的列會合併成一列。將整個影格記錄為單一訊息,就能保持完整。

文字中有兩種內容需要先跳脫。字面上的 % 會寫成 %%,這樣 100% 之類的文字就不會耗用樣式引數。DevTools 也會將網址轉成連結,並以自己的顏色重新繪製,因此會在 :// 和 www. 中加入沒有寬度的 word joiner(U+2060):

export function escapeConsoleText(text: string, allowLink = false): string {  let s = text;  if (s.includes("%")) s = s.replaceAll("%", "%%");  if (!allowLink) {    if (s.includes(LINK_SCHEME)) s = s.replaceAll(LINK_SCHEME, LINK_SCHEME_SAFE);    if (s.includes(WWW) || s.includes("WWW.")) s = s.replace(/www\./gi, (m) => `${m.slice(0, 3)}\u2060.`);  }  return s;}

閒置影格不記錄任何內容

緩衝區的每一列都會計算雜湊值。如果所有雜湊值都與上一個影格相同,驅動程式就會在建立字串前回傳,完全不操作主控台。等待輸入中的應用程式不會發出任何呼叫。

當某一列確實變更時,驅動程式會清除主控台並記錄新的影格:

const { frame, dirtyRows } = this.encode(buffer, options);if (frame === null) return SKIPPED;if (this.clearEachFrame) target.clear();target.log(frame.format, ...frame.args);

只有雜湊值變更的列會重新編碼。其餘各列會重複使用上一個影格的 span。

輸入

主控台不會接收按鍵輸入,因此由頁面收集。隱藏的 <textarea> 會接收按鍵、貼上和 IME 組字輸入,並將其轉成應用程式的按鍵事件。點擊頁面就會讓它取得焦點。

接下來

npm 上的 @textjs/core 0.0.x 原型將所有功能放在同一個套件中。Text.js 0.1 將本文介紹的渲染器移至 @textjs/core,將 React 協調器移至 @textjs/react,並新增 App Router。