← Blog

Renderowanie React do console.log

Text.js rysuje aplikację React w konsoli DevTools jako jedno stylowane wywołanie console.log na klatkę.

Adam

Text.js renderuje aplikacje React w konsoli DevTools przeglądarki Chrome. Poza ukrytym polem wejściowym strona nie ma interfejsu użytkownika. Każda klatka aplikacji to jedno wywołanie console.log.

Konsola jako wyświetlacz

console.log przyjmuje ciąg formatujący. Każde %c stosuje CSS z następnego argumentu do tekstu, który po nim następuje, aż do kolejnego %c:

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

Te style pozwalają ustawiać kolory, tła, pogrubienie, kursywę i podkreślenie, a czcionka konsoli ma stałą szerokość znaków (Menlo w systemie macOS). Dzięki czcionce o stałej szerokości i kolorom, które mogą się zmieniać dla każdego ciągu znaków, konsola działa jak siatka komórek znakowych ze stylami. Można na niej rysować obramowania, panele, pola wejściowe i wykresy.

Klatka jako siatka komórek

Renderer przechowuje każdą klatkę w CellBuffer. Znaki znajdują się w jednej tablicy typowanej, kolory tekstu i tła w dwóch kolejnych, a atrybuty takie jak pogrubienie i podkreślenie w czwartej. Tablice są alokowane raz dla danego rozmiaru i używane ponownie w każdej klatce.

Aby zapisać klatkę w konsoli, sterownik przechodzi przez każdy wiersz i grupuje sąsiednie komórki o wspólnym stylu w ciągi. Każdy ciąg staje się jednym spanem %c, a wiersze są łączone za pomocą \n. Nasz testowy pulpit 100×32, czyli 3200 komórek, jest kodowany do około 200 spanów.

Jedno wywołanie na klatkę

Zapisywanie każdego wiersza jako osobnego komunikatu nie działa. Każdy komunikat dostaje własne obramowanie i link do źródła w DevTools, a DevTools zwija identyczne kolejne komunikaty pod licznikiem powtórzeń, więc dwa identyczne wiersze tabeli połączyłyby się w jeden. Klatka zapisana jako jeden komunikat pozostaje w całości.

Najpierw trzeba zabezpieczyć dwa elementy tekstu przed specjalną interpretacją. Dosłowny znak % zapisuje się jako %%, więc tekst taki jak 100% nie może zużyć argumentu stylu. DevTools zamienia też adresy URL w linki i rysuje je ponownie we własnych kolorach, dlatego do :// i www. dodawany jest word joiner (U+2060), który nie ma szerokości:

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;}

Bezczynne klatki bez wpisów w konsoli

Dla każdego wiersza bufora obliczany jest hash. Jeśli żaden hash nie zmienił się od ostatniej klatki, sterownik kończy działanie przed zbudowaniem ciągu znaków i nie dotyka konsoli. Aplikacja czekająca na dane wejściowe nie wykonuje żadnych wywołań.

Gdy wiersz się zmieni, sterownik czyści konsolę i zapisuje nową klatkę:

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

Ponownie kodowane są tylko wiersze, których hash się zmienił. Pozostałe używają spanów z poprzedniej klatki.

Dane wejściowe

Konsola nie odbiera naciśnięć klawiszy, więc zbiera je strona. Ukryty element <textarea> odbiera naciśnięcia klawiszy, wklejanie i kompozycję IME, po czym zamienia je w zdarzenia klawiatury dla aplikacji. Kliknięcie strony ustawia na nim fokus.

Dalsze kroki

Prototypy @textjs/core 0.0.x w npm zawierają wszystko w jednym pakiecie. Text.js 0.1 przenosi opisany tutaj renderer do @textjs/core, reconciler React do @textjs/react i dodaje App Router.