Build و Deployment: از کد تا برنامه‌ای که کاربر می‌بیند

در درس ۲۵-۱ دیدیم Build Tool کد ما را به چیزی تبدیل می‌کند که مرورگر بفهمد. این درس مرحله بعد را بررسی می‌کند: خروجی Build دقیقاً چیست، آن را کجا و چگونه منتشر می‌کنیم (Deployment)، و چه تفاوت‌هایی بین محیط توسعه و محیط واقعی وجود دارد که بسیاری از مشکلات استقرار از آن‌ها می‌آید.

خروجی Build چیست

دستور Build (مثلاً npm run build) کدی را که نوشته‌ایم به یک پوشه خروجی (در Vite به‌طور پیش‌فرض dist) تبدیل می‌کند. محتوای آن معمولاً این است:

html
dist/
  index.html
  assets/
    index-a1b2c3.js      ← بسته اصلی (فشرده و Minify‌شده)
    Settings-d4e5f6.js   ← تکه جدا برای یک صفحه lazy (درس ۲۰)
    index-g7h8i9.css

دو نکته در این ساختار مهم است. اول، تکه‌های جدا همان نتیجه Code Splitting فصل بیستم هستند. دوم، نام فایل‌ها یک رشته تصادفی (Hash) دارند که از محتوای فایل ساخته می‌شود؛ اگر محتوا عوض شود، نام هم عوض می‌شود. این برای کش مرورگر اهمیت دارد، که در ادامه می‌آید.

پیش از انتشار: نسخه Production را محلی امتحان کنید

نسخه Build‌شده با نسخه توسعه تفاوت دارد. در حالت Production، هشدارها و بررسی‌های توسعه حذف می‌شوند (از جمله اجرای دوباره Effectها که در درس ۶-۴ دیدیم فقط در حالت توسعه رخ می‌دهد). پس ممکن است چیزی که در توسعه کار می‌کرد، در Production رفتار متفاوتی داشته باشد، یا برعکس. بیشتر ابزارها دستوری برای سرو کردن خروجی Build به‌صورت محلی دارند (مثلاً vite preview)؛ پیش از هر انتشار، یک‌بار برنامه را با همان خروجی امتحان کنید.

سه شکل اصلی Deployment

شکل چه چیزی منتشر می‌شود به چه نیاز دارد
میزبانی استاتیک (SPA) فایل‌های dist؛ رندر کامل در مرورگر فقط یک فضای فایل یا CDN، بدون سرور
تولید استاتیک (SSG / Static Export) یک HTML برای هر مسیر که در زمان Build ساخته می‌شود باز هم فقط فایل ثابت؛ اما Framework باید این حالت را پشتیبانی کند
رندر روی سرور (SSR / Server Components با هر درخواست) یک برنامه سروری که با هر درخواست صفحه می‌سازد یک محیط اجرا: سرور Node، توابع Serverless یا Edge، یا کانتینر

این دقیقاً همان دو حالت «Server Components بدون سرور زنده» و «با سرور» درس ۲۴-۱ است. تصمیم بین آن‌ها معمولاً به نوع محتوا بستگی دارد: صفحه‌ای که برای همه کاربران یکسان است و کم تغییر می‌کند (مثل یک مقاله) را می‌توان استاتیک ساخت و ارزان و سریع عرضه کرد؛ صفحه‌ای که به کاربر یا داده لحظه‌ای وابسته است، به رندر با هر درخواست یا به دریافت داده سمت کلاینت نیاز دارد.

مشکل کلاسیک SPA: آدرس مستقیم و 404

در فصل پانزدهم دیدیم Client-Side Routing بدون درخواست جدید به سرور کار می‌کند. اما اگر کاربر مستقیماً آدرس /products را در مرورگر باز کند یا صفحه را Refresh کند، مرورگر همان آدرس را از سرور می‌خواهد. یک سرور استاتیک فایلی به نام products ندارد و ۴۰۴ برمی‌گرداند. راه‌حل، تنظیم سرور است که برای هر مسیر ناشناخته، فایل index.html را بدهد تا برنامه بارگذاری شود و Router مسیر را در مرورگر تشخیص دهد:

nginx
location / {
  try_files $uri /index.html;
}

بسیاری از پلتفرم‌های میزبانی همین قاعده را با یک گزینه یا فایل تنظیمات ساده فراهم می‌کنند. اگر برنامه روی یک زیرمسیر (مثلاً example.com/app/) میزبانی می‌شود، باید آدرس پایه را هم به Build Tool بگویید (مثلاً گزینه base در Vite) و Router را هم با همان پایه تنظیم کنید.

کش مرورگر و نام‌های Hash‌دار

چون نام فایل‌های assets با محتوا عوض می‌شود، می‌توان آن‌ها را برای مدتی بسیار طولانی کش کرد؛ اگر کدی عوض شود، نام فایل جدید است و مرورگر خودش آن را می‌گیرد. اما خود index.html نباید طولانی کش شود، چون نام آن ثابت است و به نام فایل‌های جدید اشاره می‌کند:

html
فایل‌های assets/ (نام دارای Hash):
  Cache-Control: public, max-age=31536000, immutable

index.html:
  Cache-Control: no-cache

این تنظیم معمولاً روی CDN یا سرور انجام می‌شود، و بسیاری از پلتفرم‌ها پیش‌فرض‌های مناسبی دارند؛ اما بدانید که چرا وجود دارد، تا اگر کاربران نسخه قدیمی را می‌دیدند، بدانید کجا را بررسی کنید.

متغیرهای محیطی: آنچه در Build ثابت می‌شود

برنامه‌ها معمولاً تنظیماتی دارند که بین محیط‌ها فرق می‌کند، مثل آدرس API. ابزارهای Build این مقادیر را از فایل‌های .env یا محیط می‌خوانند. در Vite، فقط متغیرهایی با پیشوند VITE_ به کد مرورگر می‌رسند (در Next.js پیشوند NEXT_PUBLIC_ همین نقش را دارد):

javascript
// .env.production
// VITE_API_URL=https://api.example.com

const apiUrl = import.meta.env.VITE_API_URL;

دو نکته حیاتی:

  • مقدار در زمان Build داخل بسته نوشته می‌شود: برای تغییر آن باید دوباره Build کرد، نه فقط تنظیمات سرور را عوض کرد.
  • هر مقدار با پیشوند عمومی، برای همه کاربران قابل مشاهده است: همان نکته امنیتی درس ۲۴-۴ که هرچه به مرورگر برسد دیده می‌شود. کلید محرمانه، رمز پایگاه داده یا توکن‌های سرور را هرگز در متغیر عمومی نگذارید. چنین مقادیری فقط باید روی سرور (مثلاً در یک Server Component یا یک Server Function) استفاده شوند.

ارتباط با API: CORS

اگر SPA روی یک دامنه و API روی دامنه‌ای دیگر باشد، مرورگر به‌طور پیش‌فرض درخواست بین دو دامنه را مسدود می‌کند، مگر این‌که سرور API صراحتاً اجازه دهد (تنظیمات CORS). این تنظیم سمت سرور است، نه کد React. در حالت توسعه، ابزارهایی مثل Vite گزینه Proxy دارند (server.proxy) تا درخواست‌ها از طریق سرور توسعه عبور کنند؛ اما این فقط در توسعه کار می‌کند و در Production باید CORS یا یک Proxy واقعی تنظیم شود.

حجم بسته: سنجیدن پیش از انتشار

همان‌طور که در درس ۲۰-۳ دیدیم، تصمیم Code Splitting باید بر اساس داده باشد. اکثر Build Toolها پلاگین یا ابزاری برای تحلیل بسته دارند که نمایش می‌دهد کدام وابستگی چقدر حجم اضافه کرده است. پیش از انتشار، یک‌بار حجم بسته اصلی را بررسی کنید؛ یک کتابخانه بزرگ که فقط در یک صفحه کم‌استفاده لازم است، کاندیدای خوبی برای lazy است.

CI/CD: تست و Build پیش از هر انتشار

در پروژه‌های جدی، انتشار دستی نیست. یک خط لوله خودکار (CI/CD) با هر تغییر کد اجرا می‌شود. ترتیب معمول:

html
نصب وابستگی‌ها (npm ci)
   ↓
بررسی کیفیت کد (Lint، بررسی نوع)
   ↓
تست‌ها (درس ۲۵-۶)
   ↓
Build
   ↓
انتشار: نسخه پیش‌نمایش برای هر Pull Request، نسخه نهایی از شاخه اصلی

این خط لوله همان شبکه ایمنی درس ۱۹-۱ را به یک شرط خودکار تبدیل می‌کند: اگر تستی شکست بخورد یا Build خراب شود، نسخه معیوب منتشر نمی‌شود. بسیاری از پلتفرم‌ها برای هر Pull Request یک آدرس پیش‌نمایش جداگانه هم می‌سازند تا تغییر قبل از ادغام دیده و امتحان شود.

پس از انتشار: مشاهده و بازگشت

  • گزارش خطا: همان‌طور که در درس ۱۷-۳ دیدیم، می‌توان خطاها را از Error Boundary و کدهای Async به یک سرویس گزارش خطا فرستاد، تا از مشکل کاربران واقعی باخبر شوید.
  • Source Map: کد Minify‌شده در پیام خطا خوانا نیست. Source Map اجازه می‌دهد خطا به کد اصلی نگاشت شود. اگر آن را عمومی منتشر کنید، کد اصلی‌تان دیده می‌شود؛ بسیاری از سرویس‌های گزارش خطا آن را خصوصی دریافت می‌کنند.
  • امکان بازگشت (Rollback): هر انتشار باید قابل بازگشت به نسخه قبلی باشد. پلتفرم‌های رایج معمولاً نسخه‌های قبلی را نگه می‌دارند.

انتخاب میزبان

پلتفرم‌ها زیادند و تغییر می‌کنند: سرویس‌های اختصاصی میزبانی فرانت‌اند (مثل Vercel، Netlify و Cloudflare)، فضای فایل با CDN (مثلاً در سرویس‌های ابری عمومی)، و میزبانی خودتان با کانتینر (Docker) یا سرور Node. معیار انتخاب بیشتر عملی است: شکل Deployment (استاتیک یا سروری)، قابلیت‌هایی که Framework‌تان لازم دارد، هزینه و محدودیت‌های دسترسی در منطقه شما، و تجربه تیم. جزئیات هر پلتفرم، مستندات خودشان را می‌خواهد.

چرا این درس مثال قابل اجرا ندارد

Build و Deployment به فایل‌های واقعی، سرور و پلتفرم میزبانی وابسته‌اند و در ویرایشگر درون‌صفحه‌ای قابل نمایش نیستند. دستورها و تنظیمات بالا را در یک پروژه واقعی امتحان کنید.

جمع‌بندی

Build یک پوشه خروجی با فایل‌های Minify‌شده و Hash‌دار می‌سازد که تکه‌های Code Splitting را هم شامل می‌شود. نسخه Production با نسخه توسعه فرق دارد و پیش از انتشار باید محلی امتحان شود. سه شکل اصلی انتشار، میزبانی استاتیک SPA، تولید استاتیک، و رندر روی سرور است. SPA با Client-Side Routing به یک قاعده بازگشت به index.html نیاز دارد. فایل‌های Hash‌دار را می‌توان بلندمدت کش کرد ولی index.html را نه. متغیرهای محیطی با پیشوند عمومی در Build به بسته نوشته می‌شوند و برای همه قابل مشاهده‌اند، پس رمز و کلید محرمانه نباید در آن‌ها باشد. CI/CD تست و Build را پیش از هر انتشار خودکار می‌کند، و پس از انتشار، گزارش خطا و امکان بازگشت لازم است. در درس بعدی، به مقایسه React به‌عنوان Library در برابر Framework می‌پردازیم.