← בלוג

רינדור React לתוך console.log

⁦Text.js⁩ מצייר אפליקציית React במסוף של DevTools כקריאת console.log מעוצבת אחת לכל פריים.

Adam

⁦Text.js⁩ מרנדר אפליקציות React במסוף של ⁦Chrome DevTools⁩. מלבד שדה קלט מוסתר, אין בעמוד ממשק משתמש. כל פריים של האפליקציה הוא קריאה יחידה ל-console.log.

המסוף כתצוגה

console.log מקבל מחרוזת עיצוב. כל %c מחיל את ה-CSS שבארגומנט הבא על הטקסט שאחריו, עד ל-%c הבא:

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

הסגנונות האלה יכולים לקבוע צבעים, רקעים, הדגשה, כתב נטוי וקו תחתון, וגופן המסוף הוא ברוחב קבוע (Menlo ב-macOS). עם גופן ברוחב קבוע וצבעים שיכולים להשתנות בכל רצף תווים, המסוף מתפקד כרשת של תאי תווים מעוצבים. אפשר לצייר עליו מסגרות, חלוניות, שדות קלט ותרשימים.

פריים הוא רשת של תאים

מנוע הרינדור שומר כל פריים ב-CellBuffer. התווים נשמרים במערך מטיפוס מוגדר אחד, צבעי החזית והרקע בשני מערכים נוספים, ותכונות כמו הדגשה וקו תחתון במערך רביעי. המערכים מוקצים פעם אחת עבור גודל נתון ומשמשים שוב בכל פריים.

כדי לרשום פריים במסוף, הדרייבר עובר על כל שורה ומקבץ תאים סמוכים בעלי אותו סגנון לרצפים. כל רצף הופך ל-span אחד של %c, והשורות מחוברות באמצעות \n. לוח הבקרה שלנו לבדיקות, בגודל ⁦100×32⁩ ובו 3,200 תאים, מקודד לכ-200 רכיבי span.

קריאה אחת לכל פריים

רישום של כל שורה כהודעה נפרדת לא עובד. כל הודעה מקבלת מסגרת וקישור למקור משלה ב-DevTools, ו-DevTools מכווץ הודעות רצופות זהות מאחורי מונה חזרות, כך ששתי שורות זהות בטבלה יתמזגו לאחת. פריים שנרשם כהודעה יחידה נשאר שלם.

תחילה צריך לבצע escaping לשני דברים בטקסט. תו % מילולי נכתב כ-%%, כך שטקסט כמו 100% לא יוכל לצרוך ארגומנט סגנון. DevTools גם הופך כתובות URL לקישורים וצובע אותם מחדש בצבעים משלו, ולכן מוסיפים ל-:// ול-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, והופך אותן לאירועי מקלדת עבור האפליקציה. לחיצה על העמוד מעבירה אליו את המיקוד.

ההמשך

אבות הטיפוס של @textjs/core בגרסאות ⁦0.0.x⁩ ב-npm מכילים הכול בחבילה אחת. ⁦Text.js 0.1⁩ מעביר את מנוע הרינדור המתואר כאן אל @textjs/core, את מנגנון ההתאמה של React אל @textjs/react, ומוסיף ⁦App Router⁩.