طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 21 · الوحدة 9: تجميع البرامج ونشرها

البناء والتجميع بـ Vite

من مئات الملفات إلى مجلد dist واحد جاهز للنشر ⏱ نحو 40 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تميّز بين خادم التطوير ونسخة الإنتاج في Vite.
  • تشرح التجميع والتصغير والتقسيم والتجزئة.
  • تستخدم متغيرات البيئة VITE_ وتعرف حدودها.
  • تقدّم dist من Express بتخزين مؤقت صحيح ومسار SPA عام.

١من كود التطوير إلى ملفات الإنتاج

أثناء التطوير يقدّم Vite ملفاتك كما هي تقريبًا: كل ملف JSX يُحوَّل لحظة طلبه، وأي حفظ يظهر فورًا في المتصفح (HMR). هذا مريح لكنه غير مناسب للمستخدمين: مئات الملفات الصغيرة، وكود غير مصغّر، ورسائل تطوير.

البناء Build يحوّل المشروع إلى ملفات قليلة جاهزة للنشر في مجلد dist:

الخطوةماذا تفعلالفائدة
التحويل TransformJSX ← JavaScript عادي يفهمه المتصفحلا يحتاج المتصفح Babel
التجميع Bundlingدمج مئات الوحدات (ومكتبات node_modules) في حزم قليلةطلبات شبكة أقل
التصغير Minifyحذف المسافات والتعليقات وتقصير الأسماءحجم أصغر
هزّ الشجرة Tree shakingحذف الكود المستورد غير المستخدمحجم أصغر
التقسيم Code splittingفصل أجزاء لا تُحمَّل إلا عند الحاجةفتح أسرع للصفحة الأولى
التجزئة Hashingإضافة بصمة المحتوى لاسم الملف: index-DffAr-dX.jsتخزين مؤقت طويل بلا مشكلات تحديث

٢مشروع Vite: الأوامر الثلاثة

🖥 التطوير
$ npm create vite@latest shop -- --template react$ cd shop && npm install$ npm run dev   VITE v8.3.4  ready in 233 ms   ➜  Local:   http://localhost:5173/  ➜  Network: use --host to expose
الأمرالوظيفة
npm run devخادم التطوير مع التحديث الفوري (المنفذ 5173)
npm run buildيبني نسخة الإنتاج في dist
npm run previewيقدّم dist محليًّا لتجربتها قبل النشر

وهذا ناتج npm run build حقيقيًّا لمشروع صغير فيه مكوّن يُحمَّل عند الطلب:

🖥 البناء
$ npm run build vite v8.3.4 building client environment for production...transforming...✓ 18 modules transformed.rendering chunks...computing gzip size...dist/index.html                    0.45 kB │ gzip:  0.29 kBdist/assets/index-nqMpL4T3.css     1.78 kB │ gzip:  0.81 kBdist/assets/Reports-hyDh2IOE.js    0.13 kB │ gzip:  0.15 kBdist/assets/index-DffAr-dX.js    221.37 kB │ gzip: 69.37 kB ✓ built in 186ms

لاحظ أن ملف التطبيق 221 كيلوبايت (معظمه React نفسها)، لكنه يصل للمستخدم مضغوطًا بـ gzip في 69 كيلوبايت. والمكوّن Reports صار ملفًّا مستقلًّا لا يُحمَّل إلا حين يُعرض، لأننا استوردناه بـ lazy:

import { useState, lazy, Suspense } from 'react';const Reports = lazy(() => import('./Reports.jsx'));   // ملف مستقل في البناء export default function App() {  const [show, setShow] = useState(false);  return (    <main>      <h1>{import.meta.env.VITE_SHOP_NAME}</h1>      <button onClick={() => setShow(true)}>التقارير</button>      {show && <Suspense fallback={<p>…</p>}><Reports /></Suspense>}    </main>  );}

٣متغيرات البيئة في الواجهة

عنوان الخادم يختلف بين جهازك (http://localhost:3000) والإنتاج. Vite يقرأ ملفات .env ويعطيك المتغيرات التي تبدأ بـ VITE_ فقط عبر import.meta.env، ويستبدلها بقيمتها نصًّا داخل الملف المبني:

الملفيُستخدم مع
.envكل الأوضاع
.env.developmentnpm run dev
.env.productionnpm run build
# .env.productionVITE_API_URL=https://api.myshop.saVITE_SHOP_NAME=متجر الكلية
// api.jsconst BASE = import.meta.env.VITE_API_URL;   // يُستبدل وقت البناءfetch(`${BASE}/api/products`);

٤Express يقدّم الواجهة المبنية

في الإنتاج يكفي غالبًا خادم واحد: Express يقدّم ملفات dist، ويخدم /api. فلا حاجة لـ CORS (الأصل نفسه). وثلاث تفاصيل مهمة:

const express = require('express');const path = require('path');const app = express();const DIST = path.join(__dirname, 'dist'); // 1) الواجهة البرمجية أولًاapp.get('/api/health', (req, res) => res.json({ ok: true }));app.use('/api', (req, res) => res.status(404).json({ error: 'مسار API غير موجود' })); // 2) الملفات المبنية: الأصول المجزّأة تُخزَّن سنةapp.use('/assets', express.static(path.join(DIST, 'assets'), { maxAge: '1y', immutable: true }));app.use(express.static(DIST, { index: false })); // 3) أي مسار آخر = صفحة من صفحات React ← index.html بلا تخزينapp.get('/{*splat}', (req, res) => {  res.set('Cache-Control', 'no-cache');  res.sendFile(path.join(DIST, 'index.html'));}); app.listen(process.env.PORT || 3000);

افتح كل طلب وانظر ترويسة Cache-Control: الأصول max-age=31536000, immutable، والصفحة no-cache. والمسار /products/7 أعاد index.html نفسه لتتولاه React. لاحظ صيغة Express 5 للمسار العام: /{*splat} (لم تعد * وحدها مقبولة).

توقّع وتحقّق

ما الذي يُنتجه npm run build في Vite؟

الإنتاج.

لماذا يحمل اسم الملف بصمة مثل index-DffAr-dX.js ؟

Cache busting.

أي متغير يصل إلى كود الواجهة في Vite؟

البادئة.

ما فائدة lazy(() => import(...)) ؟

Code splitting.

الخلاصة

تحدٍّ برمجي: خادم الإنتاج

متوسط

مجلد dist جاهز من npm run build. اكتب server.js الذي يقدّمه مع الواجهة البرمجية.

المطلوب:
  • GET /api/version يرد {"version": "1.0.0"}، وأي مسار آخر تحت /api يرد 404 بـ JSON.
  • ملفات /assets/… تُقدَّم مع Cache-Control فيها immutable.
  • أي مسار غير ذلك (مثل /cart) يعيد محتوى dist/index.html بالحالة 200.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /api/version
  2. GET /api/users
  3. GET /assets/index-DffAr-dX.js
  4. GET /cart
  5. GET /
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • الإصدار
  • مسار API مجهول ← 404
  • الأصول مع immutable
  • /cart يرد 200
  • … بمحتوى index.html
  • والرئيسية كذلك

💡 تلميح 1
رتّب: مسارات /api أولًا، ثم الأصول، ثم المسار العام.
💡 تلميح 2
في Express 5 المسار العام يُكتب /{*splat}.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const path = require('path');const app = express();const DIST = path.join(__dirname, 'dist'); app.get('/api/version', (req, res) => res.json({ version: '1.0.0' }));app.use('/api', (req, res) => res.status(404).json({ error: 'غير موجود' })); app.use('/assets', express.static(path.join(DIST, 'assets'), { maxAge: '1y', immutable: true }));app.get('/{*splat}', (req, res) => res.sendFile(path.join(DIST, 'index.html'))); app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس