تمام مسیر کتاب در یک نسخهٔ پرچگالی: چرا Node تکنخی اما قدرتمند است، از کالبک تا async/await، استریمها، الگوهای طراحی Guru، مقیاسپذیری و پیامرسانی — با جدول «مسئله ← الگو»، کدِ حداقلی و خودآزمایی برای تثبیت.
Node یعنی جاوااسکریپت بیرون از مرورگر: موتور V8 برای اجرا + libuv برای I/O ناهمگام. معجزهاش I/O غیرمسدودکننده (Non-blocking) است — مثل پیشخدمت حرفهای که سفارشها را میگیرد و منتظر آشپز نمیماند؛ در نتیجه یک نخ، هزاران اتصال را زنده نگه میدارد. ساختار پشت آن Reactor Pattern است: عملیات ورودی به «دمالتیپلکسر رویداد» سپرده میشود و Event Loop تکنخی، رویدادهای آماده را یکییکی به هندلرها میرساند.
هرگز نخ را بلوک نکن. کوچکترین حلقهٔ سنگین یا پردازش CPU-heavy، تمام اتصالها را فلج میکند. کار سنگین = worker_threads یا پروسهٔ جدا.
| ویژگی | CommonJS | ESM |
|---|---|---|
| دستور | require / module.exports | import / export |
| بارگذاری | پویا (هرجا بخواهی) | استاتیک و تحلیلپذیر قبل از اجرا |
| خروجی | کپی مقدار | Live Binding فقطخواندنی |
| نکتهٔ کلیدی | کشِ require = Singleton رایگان | Tree-shaking و حلقهٔ وابستگی با دقت بیشتر |
// CJS: هر فایل یک ماژول؛ خروجی همان چیزی که به module.exports بدهی
module.exports = { createServer }
// ESM: اتصال استاتیک — ابزارها میتوانند کد را بهینه کنند
import { readFile } from 'fs/promises'
export const helper = () => {}
مثلث تکامل: کالبک ← Promise ← async/await. با کالبک، چهار قانون انضباطی رعایت کن: اول خطا را چک کن (error-first)، همیشه خطا را پاس بده، از توابع بینام تودرتو فرار کن، و کد را ماژولار تقسیم کن — وگرنه به «هرم مرگ» میرسی. با Promise ها، موازیسازی یک خط است: Promise.all. و async/await همان است، فقط خواناتر.
// تلهٔ مرگبار Zalgo: تابعی که گاهی همگام، گاهی ناهمگام صدا میزند
function read (file, cb) {
if (cache.has(file)) cb(cache.get(file)) // همگام!
else fs.readFile(file, (err, data) => cb(data)) // ناهمگام
}
// درمان: همیشه ناهمگام با process.nextTick
if (cache.has(file)) process.nextTick(() => cb(cache.get(file)))
۱) داخل try/catch از return await استفاده کن، نه return خالی — وگرنه خطای Promise گرفته نمیشود.
۲) forEach + await منتظر هیچکس نمیماند؛ برای ترتیبی، for...of بزن، برای موازی، Promise.all.
EventEmitter قلب ارتباط در Node است (Observer Pattern): رویداد منتشر کن، شنونده ثبت کن. فقط رویداد error خاص است — بدون شنونده، پروسه میمیرد. استریمها همان فلسفه را برای داده اعمال میکنند: بهجای بارگذاری کل فایل ۸ گیگابایتی در RAM، تکهتکه پردازش کن — مثل خط مونتاژ که هر ایستگاه فقط یک کار میکند و لایهها روی هم سوار میشوند (بخوان ← رمزگشایی ← فشردهسازی ← بنویس).
// الگوی ترکیب استریمها؛ همیشه pipeline نه pipe (مدیریت خطا درست)
import { pipeline } from 'stream/promises'
import { createReadStream, createWriteStream } from 'fs'
import { createGzip } from 'zlib'
await pipeline(
createReadStream('big.log'),
createGzip(),
createWriteStream('big.log.gz')
)
مهمترین مفهوم این فصل: backpressure — وقتی تولیدکننده تندتر از مصرفکننده است، جریان باید موقتاً متوقف شود (رویداد drain). تابع pipeline این را + جمعکردن خطاها برایت حل میکند.
| مسئلهٔ تو | الگو | در یک خط |
|---|---|---|
| ساخت شیء وابسته به شرایط (مثل PROD/DEV) | Factory | بهجای new، تابعی بساز که تصمیم را میگیرد |
| فقط یک نمونه در کل برنامه | Singleton / DI | کش ماژول این را میدهد؛ اما وابستگیها را تزریق کن تا تستپذیر بماند |
| لاگ، کش یا کنترل دسترسی بدون دستزدن به کد | Proxy | همان API، رفتار اضافه — مثل نگهبان جلوی در |
| اتصال دو API ناسازگار | Adapter | مترجم بین دو دنیا |
| رفتار وابسته به وضعیت داخلی (اتصال قطع/وصل) | State | هر وضعیت، یک کلاس با همان API |
| چند الگوریتم قابلتعویض (JSON/INI) | Strategy | خانوادهٔ الگوریتمهای همامضا، قابلجابهجایی |
| زنجیرهٔ پردازش درخواستها | Middleware | هر لایه، درخواست را میگیرد، شاید تغییر میدهد، به بعدی میدهد — پایهٔ Express |
| پیمایش سفارشی (حتی ناهمگام) | Iterator / Generator | پروتکلهای iterator/iterable + async generator |
چون این جدول در واقع «واژگان طراحی» توست: وقتی در Code Review میگویی «اینجا Proxy بگذار» یعنی ده دقیقه توضیح را در دو کلمه فشرده کردهای.
سه راه رشد — مکعب مقیاس (Scale Cube): محور X کلون کردن (چند نمونه یکسان پشت Load Balancer با cluster روی همهٔ هستهها)، محور Y تقسیم بر وظیفه (میکروسرویس)، محور Z شارد کردن داده (هر نمونه بخشی از داده). ترتیب عملی کتاب: اول X با cluster، بعد reverse proxy مثل Nginx، بعد Y و Z وقتی واقعاً لازم شد.
| ابزار | چه میکند | تله |
|---|---|---|
| cluster | یک پروسه به ازای هر هسته، همپورت | وضعیت حافظه مشترک نیست — session حافظهای ممنوع |
| sticky session | اتصال stateful (مثل WebSocket) همیشه به همان نمونه | بار متوازن میشود؛ برای stateless نیازی نیست |
| Nginx / Consul | توزیع بار + کشف سرویس پویا | نقطهٔ شکست مرکزی — همیشه redundant |
وقتی سرویسها جدا میشوند، سه الگوی انتقال داری: Pub/Sub (یک ناشر، چند مشترک — مثل رادیو؛ با Redis یا ZeroMQ)، صف کار (هر پیام فقط یک مصرفکننده — توزیع بار با الگوی «مصرفکنندههای رقیب» در RabbitMQ) و استریم (جریان ماندگار با قابلیت replay — Redis Streams). برای درخواست/پاسخِ ناهمگام دو شناسه لازم است: correlationId (پیوند پاسخ به درخواست) و returnAddress (کجا جواب بدهی).
پیام = قرارداد. یکبار نسخهبندی کن و هرگز شکل پیام را بدون مهاجرت عوض نکن؛ در Pub/Sub تحویل «حداکثر یکبار» است، پس مصرفکنندهات باید idempotent باشد.
۱) پردازش سنگین روی Event Loop (حلقهٔ یکمیلیونتایی = فلج همهٔ کاربران).
۲) تابع ناهمگامِ دوچهره (Zalgo) — همیشه nextTick.
۳) کش کردن Promise ردشده — خطا ابدی میشود؛ ردشدهها را کش نکن.
۴) pipe() بدون مدیریت خطا — pipeline() جایگزین مدرن است.
۵) uncaughtException با ادامهٔ اجرا — وضعیت ناسالم است؛ لاگ کن و خارج شو.
- Node = V8 + libuv؛ Reactor و Event Loop تکنخی.
- قانون طلایی: نخ را هرگز بلوک نکن؛ کار سنگین = worker_threads.
- ماژول: CJS ساده و پویا؛ ESM استاتیک با Live Binding.
- مسیر تکامل: کالبک ← Promise ← async/await؛ Zalgo را بشناس.
- EventEmitter قلب رویدادهاست؛ error بدون شنونده = مرگ پروسه.
- استریم = خط مونتاژ؛ backpressure را با pipeline() مدیریت کن.
- الگوها واژگان طراحیاتاند: Factory، Proxy، Adapter، State، Middleware.
- مقیاس: اول cluster (محور X)، بعد Nginx، بعد تقسیم Y/Z.
- stateful = sticky session؛ stateless = آزادی کامل در مقیاس.
- پیام = قرارداد؛ مصرفکننده idempotent، تحویل حداکثر یکبار.
هشت سؤال برای تثبیت؛ روی هر سؤال کلیک کن تا جواب باز شود. ۶ پاسخ درست یعنی آمادهای، وگرنه بخش مربوطه را دوباره بخوان.
۱. چرا Node با یک نخ، هزاران اتصال را اداره میکند؟
۲. فرق اصلی CJS و ESM؟
۳. تلهٔ Zalgo چیست و درمانش؟
۴. چرا pipeline() بهتر از pipe() است؟
۵. backpressure یعنی چه؟
۶. کی Proxy و کی Adapter؟
۷. سه محور مکعب مقیاس؟
۸. چرا مصرفکنندهٔ پیام باید idempotent باشد؟
وقتی الگو را میدانی، مسئله را هم بلدی نام ببری.