البناء والتجميع بـ Vite
من مئات الملفات إلى مجلد dist واحد جاهز للنشر ⏱ نحو 40 دقيقة.
- تميّز بين خادم التطوير ونسخة الإنتاج في
Vite. - تشرح التجميع والتصغير والتقسيم والتجزئة.
- تستخدم متغيرات البيئة
VITE_وتعرف حدودها. - تقدّم
distمنExpressبتخزين مؤقت صحيح ومسارSPAعام.
١من كود التطوير إلى ملفات الإنتاج
أثناء التطوير يقدّم Vite ملفاتك كما هي تقريبًا: كل ملف JSX يُحوَّل لحظة طلبه، وأي حفظ يظهر فورًا في المتصفح (HMR). هذا مريح لكنه غير مناسب للمستخدمين: مئات الملفات الصغيرة، وكود غير مصغّر، ورسائل تطوير.
البناء Build يحوّل المشروع إلى ملفات قليلة جاهزة للنشر في مجلد dist:
| الخطوة | ماذا تفعل | الفائدة |
|---|---|---|
| التحويل Transform | JSX ← 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.development | npm run dev |
.env.production | npm 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 (الأصل نفسه). وثلاث تفاصيل مهمة:
- الملفات المجزّأة
assets/*لا يتغير محتواها أبدًا (أي تعديل يغيّر اسمها)، فنسمح للمتصفح بتخزينها سنة كاملة. - أما
index.htmlفلا يُخزَّن، حتى يجد المستخدم أسماء الملفات الجديدة بعد كل نشر. - تطبيق الصفحة الواحدة
SPAفيه مسارات مثل/products/7تعالجهاReact Routerفي المتصفح؛ فإن فتحها المستخدم مباشرة يجب أن يعيد الخادمindex.htmlلا 404.
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);
<!doctype html><html lang="ar" dir="rtl"> <head> <meta charset="UTF-8" /> <title>متجر الكلية</title> <script type="module" crossorigin src="/assets/index-DffAr-dX.js"></script> <link rel="stylesheet" crossorigin href="/assets/index-nqMpL4T3.css"> </head> <body> <div id="root"></div> </body></html>
import{t as e}from"./vendor.js";/* … كود التطبيق بعد التصغير … */main{max-width:40rem;margin:auto}h1{color:#149eca}افتح كل طلب وانظر ترويسة 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.الخلاصة
devللتطوير، وbuildينتجdist، وpreviewلتجربته.- البناء: تحويل وتجميع وتصغير وهزّ شجرة وتقسيم وتجزئة.
import.meta.env.VITE_…يُستبدل وقت البناء؛ لا أسرار فيه.- الأصول المجزّأة تُخزَّن سنة، و
index.htmlبلا تخزين. Express: /apiأولًا، ثمstatic، ثم/{*splat}←index.html.
تحدٍّ برمجي: خادم الإنتاج
متوسطمجلد 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.
👀 اعرض الاستجابات المطلوبة
GET /api/versionGET /api/usersGET /assets/index-DffAr-dX.jsGET /cartGET /
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- الإصدار
- مسار API مجهول ← 404
- الأصول مع immutable
- /cart يرد 200
- … بمحتوى index.html
- والرئيسية كذلك
💡 تلميح 1
💡 تلميح 2
/{*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);
<!doctype html><html lang="ar" dir="rtl"> <head> <meta charset="UTF-8" /> <title>متجر الكلية</title> <script type="module" crossorigin src="/assets/index-DffAr-dX.js"></script> <link rel="stylesheet" crossorigin href="/assets/index-nqMpL4T3.css"> </head> <body> <div id="root"></div> </body></html>
import{t as e}from"./vendor.js";/* … كود التطبيق بعد التصغير … */