← وبلاگ

رندر React در console.log

⁦Text.js⁩ یک برنامهٔ React را با یک فراخوانی استایل‌دار console.log در هر فریم، در کنسول DevTools ترسیم می‌کند.

Adam

⁦Text.js⁩ برنامه‌های React را در کنسول DevTools مرورگر Chrome رندر می‌کند. صفحه به‌جز یک ورودی مخفی، رابط کاربری ندارد. هر فریم برنامه یک فراخوانی 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 پیام‌های یکسان پیاپی را پشت یک شمارندهٔ تکرار جمع می‌کند، بنابراین دو ردیف یکسان جدول در یک ردیف ادغام می‌شوند. فریمی که در یک پیام ثبت شود، یک‌تکه باقی می‌ماند.

ابتدا دو مورد در متن به گریزدهی نیاز دارند. نویسهٔ % به‌شکل %% نوشته می‌شود تا متنی مانند 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⁩ اضافه می‌کند.