Skip to content

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

مرجع 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 }): پاسخ‌ها را با شناسهٔ درخواست جفت می‌کند
هر دو سر از یک MessagePortcreatePortBridge(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/utilsDOM/BOM، رشته، شیء، عدد، رنگ، زمان، حافظه و…مرورگر + node*
ranuts/nodeکارساز HTTP، مسیریاب، وب‌سوکت، fs، جریان‌ها، میان‌افزارفقط node
ranuts/visualموتور رسم دوبعدی (Canvas / WebGL / WebGPU)فقط مرورگر
ranuts/i18nموتور ترجمه، بدون وابستگی به DOMمرورگر + node
ranuts/swراهبردهای کش و نیمهٔ کارگرِ قرارداد پیش‌کشسرویس‌ورکر
ranuts/vnodeDOM مجازی به سبک Snabbdomمرورگر
ranuts/streamخواندن SSE، تا کردن جریان مدل، بودجهٔ توکنمرورگر + node
ranuts/conversationاز گزارش رویداد به گره‌های گفت‌وگوی قابل رسممرورگر + node

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

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

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

منتشرشده تحت مجوز MIT.