انتخاب ابزار مناسب

مرجع API همهٔ صادرات‌ها را فهرست می‌کند. این صفحه به پرسشی پاسخ می‌دهد که آنجا بی‌پاسخ می‌ماند: از میان دو چیز شبیه به هم، کدام را می‌خواهم و چرا.

وقتی به کار می‌آید که تقریباً می‌دانی چه لازم داری («این کمتر اجرا شود»، «فقط یک بار»، «این را ذخیره کن»، «با یک کارگر حرف بزن») اما نمی‌دانی کدام صادرات آن کار را می‌کند.

نخست: بستر خودش این را ندارد؟

ranuts در پی جایگزینی کتابخانهٔ استاندارد نیست. اول سراغ بستر برو و ابزاری از اینجا را وقتی بردار که واقعاً چیزی به کار بیفزاید:

به‌جای… بستر خودش دارد… نمونهٔ ranuts را وقتی به کار ببر که…
cloneDeep(value) structuredClone(value) مقدار، تابع یا هر چیز دیگری دارد که structuredClone نمی‌پذیرد: آن یکی DataCloneError می‌اندازد، حال آنکه cloneDeep هرچه بتواند رونویسی می‌کند و باقی را با ارجاع نگه می‌دارد.
getAllQueryString(url) new URL(url).searchParams با یک فراخوان یک شیء ساده می‌خواهی، نه یک پیمایشگر.
localStorageGetItem(key) localStorage.getItem(key) کد جایی هم اجرا می‌شود که حافظه نیست یا بسته است: پوشش‌ها به‌جای خطا انداختن '' برمی‌گردانند (حالت خصوصی سافاری، SSR، iframe در جعبهٔ شنی).
escapeHtml(str) textContent = str داری یک رشته می‌سازی، نه یک گره.

کمتر انجام دادن یک کار

«این را کمتر صدا بزن» می‌تواند چهار معنای متفاوت داشته باشد:

آنچه می‌خواهی… به کار ببر رفتار
فقط آخرین فراخوان از یک رگبار (یک کادر جست‌وجو، یک تغییر اندازه) debounce(fn, ms) به فاصلهٔ ms پس از پایان رگبار اجرا می‌شود. در میانهٔ رگبار هیچ چیز اجرا نمی‌شود.
در میانهٔ رگبار، با آهنگی یکنواخت (جای پیمایش، نمایش پیشرفت) throttle(fn, ms) نخستین فراخوان بی‌درنگ اجرا می‌شود و پس از آن، در هر ms دست‌بالا یکی.
که در تمام عمر دقیقاً یک بار اجرا شود (یک راه‌اندازی، یک هشدار یک‌باره) once(fn) نخستین فراخوان محاسبه می‌کند و همهٔ فراخوان‌های بعدی همان نتیجه را برمی‌گردانند.
که فراخوان‌های هم‌زمان یک درخواست در جریان را با هم قسمت کنند singleFlight(fn) گونهٔ ناهمگام once: تا وقتی فراخوانی در انتظار است، بقیه به آن می‌پیوندند.

memoize نام پیشین once است و همان کار را می‌کند: برخلاف آنچه نامش می‌رساند، نتیجه را به ازای هر آرگومان نگه نمی‌دارد. در کد تازه once بنویس.

تفاوتی که به کار می‌آید این است: با debounce روی دستگیرهٔ فشردن کلید، تا وقتی کاربر تایپ می‌کند هیچ چیز اجرا نمی‌شود؛ با throttle تمام مدت چیزی اجرا می‌شود، فقط نه به ازای هر کلید. پیشنهاد جست‌وجو debounce می‌خواهد و شمارندهٔ «نویسه‌های باقی‌مانده» throttle.

مهار کردن کار ناهمگام

آنچه می‌خواهی… به کار ببر
کارهای بسیاری را اجرا کنی، اما هر بار فقط n تا new QuestQueue({ simultaneous: n })
از پیمانی که زیادی طول می‌کشد دست بکشی withTimeout(promise, ms)
…و به‌جای خطا انداختن با یک مقدار پیش‌فرض ادامه دهی withTimeoutFallback(promise, ms, fallback)
پیمانی که از جایی کاملاً دیگر برآورده‌اش می‌کنی deferred()
گام‌های ناهمگام را به سبک Koa زنجیر کنی، جوری که هر گام بتواند گام بعدی را در بر بگیرد compose(middleware)

اگر همه را یک‌جا می‌خواهی، Promise.all درست است؛ و اگر «همه یک‌جا» شصت اتصال باز می‌کند، QuestQueue درست است. withTimeout رد می‌کند: آن را با یک catch همراه کن، یا اگر سررسید زمان برای تو خطا نیست، گونهٔ دارای مقدار پشتیبان را بردار.

ذخیره کردن چیزی

عمر و اندازه به کار ببر
رشته‌ای کوچک که از بارگذاری دوباره جان به در ببرد localStorageSetItem / localStorageGetItem / localStorageRemoveItem
دادهٔ ساختارمند، رکوردهای بسیار، یا بیش از چند مگابایت new WebDB({ dbName, stores }): پوششی از جنس Promise روی IndexedDB
یک مقدار که از این صفحه به صفحهٔ بعد سپرده می‌شود createHandoff({ dbName, storeName, key })

پوشش‌های localStorage* از آن رو هستند که فراخوان‌های بومی، هرجا حافظه در دسترس نباشد خطا می‌اندازند (حالت خصوصی سافاری، iframe در جعبهٔ شنی، مرورگری که دادهٔ سایت را بسته است)، و فروپاشی هنگام خواندن خرابی بدتری است از نبود یک ترجیح. این پوشش‌ها '' برمی‌گردانند و راه خود را ادامه می‌دهند.

createHandoff برای حالتی است که هیچ‌کدام از آن دو جور درنمی‌آید: مقداری که باید دقیقاً از یک جابه‌جایی صفحه جان به در ببرد و بعد از میان برود.

گفت‌وگو میان بافت‌های اجرا

میان… به کار ببر
یک صفحه و یک Web Worker، از جنس درخواست و پاسخ new WorkerClient({ create }): پاسخ‌ها را با شناسهٔ درخواست جفت می‌کند
هر دو سر از یک MessagePort createPortBridge(port)
دو پنجره یا iframe که باید یکدیگر را پیدا کنند یک سو acceptPortBridge() و سوی دیگر دست‌دادن

اگر کارگر به پرسش‌ها پاسخ می‌دهد، آنچه باید برداری WorkerClient است: بدون شناسهٔ درخواست، دو فراخوان هم‌پوشان نمی‌توانند بگویند پاسخی که رسید از آنِ کدام است. پل یک لایه پایین‌تر است: وقتی رفت‌وآمد از جنس درخواست و پاسخ نیست، یا وقتی راه انتقال از پیش موجود است، از آن استفاده کن.

کار با شیءها

آنچه می‌خواهی… به کار ببر یادداشت
رونوشتی که با هیچ چیز دیگری مشترک نباشد cloneDeep(value) از پس ارجاع‌های حلقوی و نوع‌های درون‌ساختهٔ رایج برمی‌آید.
بدانی دو مقدار مثل هم هستند یا نه isEqual(a, b) مقایسهٔ ژرف، نه یکی بودن ارجاع.
دو شیء را با هم درآمیزی merge(a, b) ادغام سطحی: کلیدهای b برنده‌اند و a دستخوش تغییر می‌شود.
چند کلید را بیندازی filterObj(obj, keys) رونوشتی بدون کلیدهای نام‌برده برمی‌گرداند.

زبان و متن

  • resolveLocale({ supported, … }) برمی‌گزیند که کدام‌یک از زبان‌های تو به کار رود، آن هم با همان زنجیرهٔ همیشگی (انتخاب صریح، حافظه، navigator.languages، مقدار پشتیبان). پاسخش «کدام زبان» است، نه «این رشته چه می‌گوید».
  • createI18n / useI18n (ranuts/i18n) موتور ترجمه است: واژه‌نامه‌های تخت پیام، جاگذاری با {param} و جابه‌جایی در زمان اجرا.
  • segmentByRanges و paginateText برای چیدن متن‌اند: نخستی جابه‌جایی‌ها و برجسته‌سازی، و دومی بریدن متن به صفحه‌هایی که در یک کادر جا شوند.

resolveLocale را حتی اگر موتور i18n را به کار نمی‌بری بردار: تصمیمی که می‌گیرد، یعنی احترام گذاشتن به ترتیب کامل navigator.languages خواننده و نه فقط نخستین مورد آن، همان بخشی است که به‌آسانی اشتباه از آب درمی‌آید.

جاری کردن پاسخ یک مدل

سه لایه، که هر کدام به‌تنهایی هم به کار می‌آید:

  1. ranuts/stream: SSE را می‌خواند و سپس با createStreamAccumulator() تکه‌ها را در یک نمای لحظه‌ای تا می‌کند. نسبت به فراهم‌کننده بی‌طرف است: تکه‌های متن، استدلال و فراخوانی ابزار، از هرکس که آمده باشند، به یک شکل درمی‌آیند.
  2. ranuts/conversation: یک گزارش رویداد که فقط به آن افزوده می‌شود را با createConversationEngine() بر گره‌های قابل رسم می‌نگارد. تصمیم می‌گیرد هر ردیف چیست؛ چیزی نمی‌کشد.
  3. <r-conversation> در ranui: همان عنصری که آن گره‌ها را می‌کشد، نما را به پایین چسبیده نگه می‌دارد و ردیف‌ها را با هم می‌سنجد.

اگر فقط متن می‌کشی، همان لایهٔ ۱ بس است؛ وقتی رونوشت گفت‌وگو ساختاری پیدا کرد که نگاشتنش می‌ارزد، لایهٔ ۲ را بیفزا؛ و وقتی می‌خواهی پیمایش و هم‌سنجی ردیف‌ها هم حل شده باشد، لایهٔ ۳ را.

از کدام نقطهٔ ورود وارد کنیم

هر زیرمسیر بشکه‌ای مستقل است که می‌توان شاخه‌های بی‌مصرفش را تکاند. از همانی وارد کن که صاحب آن نماد است، هرگز از مسیر عمیق کد.

وارد کردن از شامل کجا اجرا می‌شود
ranuts بشکهٔ ریشه: ابزارهای کمکی به‌همراه سطح visual مرورگر + node
ranuts/utils DOM/BOM، رشته، شیء، عدد، رنگ، زمان، حافظه و… مرورگر + node*
ranuts/node کارساز HTTP، مسیریاب، وب‌سوکت، fs، جریان‌ها، میان‌افزار فقط node
ranuts/visual موتور رسم دوبعدی (Canvas / WebGL / WebGPU) فقط مرورگر
ranuts/i18n موتور ترجمه، بدون وابستگی به DOM مرورگر + node
ranuts/sw راهبردهای کش و نیمهٔ کارگرِ قرارداد پیش‌کش سرویس‌ورکر
ranuts/vnode DOM مجازی به سبک Snabbdom مرورگر
ranuts/stream خواندن SSE، تا کردن جریان مدل، بودجهٔ توکن مرورگر + node
ranuts/conversation از گزارش رویداد به گره‌های گفت‌وگوی قابل رسم مرورگر + node

* ranuts/utils دامنهٔ فراخی دارد: بیشترش رو به مرورگر است، اما یاری‌رسان‌های ناب (رشته، شیء، عدد، compose، cloneDeep و…) همه‌جا اجرا می‌شوند. ranuts/node را در کد مرورگر وارد نکن. fs، http و child_process را با خود می‌آورد.

هنوز مطمئن نیستی؟

در مرجع API بگرد: همهٔ صادرات‌ها آنجا هستند، با امضا و یک خط توضیح که از دل کد ساخته شده است. اگر پس از خواندن هر دو خط، دو مورد همچنان جایگزین‌پذیر به نظر می‌رسند، این یک ایراد مستندات است و گزارش کردنش می‌ارزد.