Node.js: Express متقدم

آخر تحديث: 2026-08-26

1. أزمة الإنتاج التي واجهها بوب

واجهت واجهة برمجة التطبيقات (API) الخاصة بـ«بوب» مشاكل في أول يوم لها: فقد أرسل أحد المستخدمين عنوان بريد إلكتروني يحتوي على أحرف مشوشة، مما تسبب في حدوث خطأ في قاعدة البيانات أدى إلى تعطل الموقع بالكامل؛ وقام شخص آخر بتحميل صورة بحجم 500 ميغابايت، مما أدى إلى امتلاء مساحة القرص على الفور؛ كما كتب أحد المنافسين برنامجًا نصيًّا أرسل 1,000 طلب في الثانية، مما تسبب في إرجاع الخادم لخطأ 502.

يكفي وجود app.use(errorHandler) واحد فقط لمنع 80% من حوادث الإنتاج.

TEXT 📖 للعرض فقط
Bob Mine Clearance Timeline:
Day 1  📧 Invalid email address → Database Error → 500 Error
Day 2  🖼️ 500MB Upload → Disk Full → Service Outage
Day 3  🤖 1000 req/s → CPU 100% → 502 Bad Gateway
Day 4  🔒 Add middleware → Take Them One by One → Stable service


2. البرمجيات الوسيطة لمعالجة الأخطاء

(1) نظام التوقيع ذو المعلمات الأربعة

Express identifies error middleware by the number of parameters—there must be 4 parameters (err, req, res, next); if one is missing, it becomes a regular middleware.

▶ مثال: برمجيات وسيطة عالمية لمعالجة الأخطاء

JAVASCRIPT
const express = require('express');
const app = express();

app.get('/boom', (req, res, next) => {
  try {
    throw new Error('Blown up on purpose');
  } catch (err) {
    next(err);
  }
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    code: err.status || 500,
    data: null,
    message: err.message
  });
});

app.listen(3000);
▶ جرّب الكود

(2) فئات الأخطاء التجارية المخصصة

▶ مثال: التمييز بين الأخطاء التشغيلية وأخطاء HTTP

JAVASCRIPT
class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} Not found`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) return next(new NotFoundError('User'));
  res.json({ code: 0, data: user, message: 'ok' });
});
▶ جرّب الكود

(3) مقارنة بين طرق معالجة الأخطاء

الطريقة عدد المعلمات النطاق حالات الاستخدام دعم التشغيل غير المتزامن
app.use(errHandler) 4 جميع الأخطاء العامة معالجة شاملة نهائية يتطلب تدخلًا يدويًّا next(err)
try/catch + next(err) مسار واحد رمز المزامنة المزامنة فقط
express-async-errors 0 خطأ عالمي في المعالجة غير المتزامنة توجيه async/await تلقائي
وحدة domain (مهملة) على مستوى العملية غير موصى بها
process.on('uncaughtException') على مستوى العملية خط الدفاع الأخير عالمي
وعد .catch() وعد واحد عملية غير متزامنة واحدة واحد


3. express-validator Request Validation

(1) كيفية استخدام سلسلة التحقق والبرمجيات الوسيطة

يعتمد express-validator على مكتبة validator.js. ويستخدم «سلسلة التحقق من الصحة» لتعريف القواعد بطريقة إعلانية، كما يقوم تلقائيًا بتجميع الأخطاء عند فشل عملية التحقق من الصحة.

▶ مثال: التحقق من تسجيل المستخدم

JAVASCRIPT
const { body, validationResult } = require('express-validator');

app.post('/register',
  body('username')
    .isLength({ min: 3, max: 20 }).withMessage('The username must3-20Character')
    .isAlphanumeric().withMessage('Usernames must consist of alphanumeric characters only.'),
  body('email')
    .isEmail().withMessage('Invalid email address')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
    .matches(/\d/).withMessage('Passwords must contain numbers')
    .matches(/[A-Z]/).withMessage('The password must contain uppercase letters.'),
  body('age')
    .optional()
    .isInt({ min: 1, max: 150 }).withMessage('Age requirement1-150'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        data: errors.array(),
        message: 'Request verification failed'
      });
    }
    next();
  },
  (req, res) => {
    res.json({ code: 0, data: req.body, message: 'ok' });
  }
);
▶ جرّب الكود

(2) أدوات التحقق الشائعة في express-validator

أداة التحقق الغرض مثال المُعدِّلات المرتبطة
isEmail() البريد الإلكتروني body('email').isEmail() .normalizeEmail()
isLength() الطول body('name').isLength({min:2,max:50})
isInt() / isFloat() الرقم query('page').isInt({min:1}) .toInt()
isBoolean() منطقية body('active').isBoolean() .toBoolean()
isDate() التاريخ body('birthday').isDate() .toDate()
isURL() رابط body('website').isURL()
isIn() التعداد body('role').isIn(['admin','user'])
matches() التعبير النمطي body('code').matches(/^\d{6}$/)
isMongoId() ObjectId param('id').isMongoId()
optional() اختياري body('nickname').optional().isLength({max:30}) {nullable:true}

(3) مقارنة بين «express-validator» و«Joi»

معايير المقارنة express-validator Joi
طريقة التكامل برمجيات وسيطة Native Express مكتبة تحقق مستقلة؛ تتطلب تكاملاً يدويًّا
أسلوب الصياغة الاستدعاءات المتسلسلة، التعريف حقلًا حقلًا كائن المخطط، التعريف لمرة واحدة
التبعيات الأساسية validator.js التنفيذ المخصص
مجموعة الأخطاء validationResult() التجميع التلقائي validate().error المعالجة اليدوية
حالات الاستخدام التكامل السريع مع مشاريع Express أي مشروع Node.js
منحنى التعلم منخفض (إذا كنت على دراية بـ validator.js) متوسط (بناء جملة فريد في Schema)
تحويل النوع .toInt() .toDate()، إلخ. التحويل التلقائي للنوع
حجم المجتمع عدد التنزيلات الأسبوعية ~1.5 مليون عدد التنزيلات الأسبوعية ~5 ملايين


4. تحميل الملفات باستخدام multer

(1) ثلاث استراتيجيات للتخزين

يوفر multer ثلاثة أوضاع للتحميل: single وarray وfields. وتشمل خيارات التخزين memoryStorage (الذاكرة) وdiskStorage (القرص).

▶ مثال: التخزين على القرص + تصفية الملفات

JAVASCRIPT
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
    cb(null, uniqueName);
  }
});

const fileFilter = (req, file, cb) => {
  const allowed = /\.(jpg|jpeg|png|gif|webp)$/i;
  if (allowed.test(path.extname(file.originalname))) {
    cb(null, true);
  } else {
    cb(new Error('Supports only jpg/png/gif/webp Format'), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: 5 * 1024 * 1024 }
});

app.post('/avatar', upload.single('avatar'), (req, res) => {
  if (!req.file) return res.status(400).json({ code: 400, data: null, message: 'Please upload the file' });
  res.json({
    code: 0,
    data: { url: `/uploads/${req.file.filename}`, size: req.file.size },
    message: 'ok'
  });
});

app.post('/photos', upload.array('photos', 9), (req, res) => {
  const urls = req.files.map(f => `/uploads/${f.filename}`);
  res.json({ code: 0, data: urls, message: 'ok' });
});
▶ جرّب الكود

(2) مقارنة بين خيارات تكوين مولتر

عنصر التكوين النوع القيمة الافتراضية الوصف مثال
storage محرك التخزين memoryStorage محرك التخزين multer.diskStorage({...})
dest سلسلة الدليل المستهدف (إما هذا أو "storage") 'uploads/'
fileFilter الوظيفة السماح بكل شيء استدعاء رد تصفية الملفات (req,file,cb)=>{...}
limits.fileSize العدد بلا حدود الحد الأقصى لعدد البايتات لكل ملف 5*1024*1024
limits.files العدد غير محدود الحد الأقصى لعدد الملفات المسموح بتحميلها 9
limits.fields العدد غير محدود الحد الأقصى لعدد الحقول غير المتعلقة بالملفات 10
limits.fieldSize رقم 1 ميغابايت الحد الأقصى لعدد البايتات للحقول غير المتعلقة بالملفات 1024*100
limits.parts العدد غير محدود العدد الإجمالي للأجزاء المتعددة 20

(3) التخزين في الذاكرة مقابل التخزين على القرص

البعد سعة الذاكرة سعة القرص
موقع التخزين الذاكرة (المخزن المؤقت) ملف القرص
req.file العقار buffer path، filename
حالات الاستخدام الملفات الصغيرة، المعالجة في الوقت الفعلي (مثل ضغط الصور وحفظها) الملفات الكبيرة، التخزين الدائم
الأداء سريع (لا يتطلب عمليات إدخال/إخراج على القرص) أبطأ قليلاً (يتطلب الكتابة على القرص)
مخاطر الذاكرة نفاد الذاكرة (OOM) عند معالجة الملفات الكبيرة أو في ظل التزامن العالي لا شيء
إعادة تشغيل «Lost» نعم لا
التحكم في اسم الملف غير مطلوب يتطلب استدعاءً مخصصًا filename


5. تكوين خدمة الملفات الثابتة

(1) تفاصيل express.static

▶ مثال: خدمة ثابتة مع دلائل متعددة + التحكم في ذاكرة التخزين المؤقت

JAVASCRIPT
const express = require('express');
const app = express();

app.use('/static', express.static('public', {
  maxAge: '7d',
  etag: true,
  lastModified: true,
  immutable: true,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache');
    }
    if (filePath.match(/\.(jpg|png|gif|webp|svg)$/)) {
      res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
    }
  }
}));

app.use('/uploads', express.static('uploads', {
  maxAge: '30d',
  dotfiles: 'deny'
}));
▶ جرّب الكود

(2) الاعتبارات الأمنية للخدمات الثابتة



6. نموذج الرد الموحد

(1) مواصفات {code, data, message}

▶ مثال: برنامج وسيط لتغليف الاستجابات

JAVASCRIPT
const responseHandler = (req, res, next) => {
  res.success = (data = null, message = 'ok') => {
    res.json({ code: 0, data, message });
  };
  res.fail = (message = 'Operation Failed', code = -1, data = null) => {
    res.json({ code, data, message });
  };
  res.paginate = (list, total, page, pageSize) => {
    res.json({
      code: 0,
      data: { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
      message: 'ok'
    });
  };
  next();
};

app.use(responseHandler);

app.get('/users', (req, res) => {
  const users = getUserList();
  res.success(users);
});

app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) return res.fail('User does not exist', 404);
  res.success(user);
});
▶ جرّب الكود

(2) القوانين الستة الحديدية للتوسع الدولي



7. تحديد الحد الأقصى لمعدل الاستخدام

(1) تكوين «express-rate-limit»

▶ مثال: استراتيجية تحديد الحد الأقصى لمعدل الطلبات على مستويات متعددة

JAVASCRIPT
const rateLimit = require('express-rate-limit');

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { code: 429, data: null, message: 'Too many requests,Please try again later.' }
});

const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 30,
  message: { code: 429, data: null, message: 'API Exceeded the call limit' }
});

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  message: { code: 429, data: null, message: 'Too many failed login attempts, please try again in 15 minutes' }
});

app.use(globalLimiter);
app.use('/api/', apiLimiter);
app.use('/auth/login', loginLimiter);
▶ جرّب الكود

(2) قائمة برامج الوسيطة الأمنية

البرامج الوسيطة الغرض السلوك الافتراضي إعدادات التكوين الرئيسية حزمة npm
helmet رؤوس أمان HTTP تعيين 15 رأس أمان contentSecurityPolicy، hsts helmet
express-rate-limit تحديد معدل الاستخدام windowMs، max express-rate-limit
cors التحكم عبر المجالات رفض جميع الطلبات عبر المجالات origin، methods، credentials cors
express-validator التحقق من صحة المدخلات body()، query()، param() express-validator
multer تحميل الملف limits، fileFilter multer
express-mongo-sanitize حقن NoSQL إزالة $ و. express-mongo-sanitize
xss-clean تنقية XSS الترميز الهروبي لـ HTML xss-clean
hpp تلوث المعلمات أخذ القيمة الأخيرة whitelist hpp
compression ضغط gzip — ضغط threshold، level


8. تهيئة البيئة: dotenv

(1) الاستخدام الأساسي لـ dotenv

▶ مثال: التحميل الهرمي لمتغيرات البيئة

JAVASCRIPT
const dotenv = require('dotenv');
const path = require('path');

dotenv.config({ path: path.resolve(process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : '.env') });

const config = {
  port: parseInt(process.env.PORT, 10) || 3000,
  env: process.env.NODE_ENV || 'development',
  db: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 27017,
    name: process.env.DB_NAME || 'myapp_dev'
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
  },
  upload: {
    maxFileSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 5 * 1024 * 1024,
    allowedTypes: (process.env.ALLOWED_TYPES || 'jpg,jpeg,png,gif,webp').split(',')
  },
  rateLimit: {
    windowMs: parseInt(process.env.RATE_WINDOW_MS, 10) || 15 * 60 * 1000,
    max: parseInt(process.env.RATE_MAX, 10) || 100
  }
};

module.exports = config;
▶ جرّب الكود

▶ مثال:(2) أفضل الممارسات المتعلقة بملفات .env

TEXT 📖 للعرض فقط
# .env              ← Default(Development),Do not submit to Git
# .env.production   ← Production Environment,Strictly Restrict Access
# .env.test         ← Test Environment

PORT=3000
NODE_ENV=development

DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp_dev

JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRES_IN=7d

MAX_FILE_SIZE=5242880
ALLOWED_TYPES=jpg,jpeg,png,gif,webp

RATE_WINDOW_MS=900000
RATE_MAX=100
TEXT 📖 للعرض فقط
# .gitignore Must include
.env
.env.*
!.env.example


9. العملية الكاملة لمعالجة الطلبات السريعة

▶ مثال:(1) مخطط تدفق حورية البحر

100%
flowchart TD
    A[Client Request] --> B[helmet Safety Head]
    B --> C[cors Cross-domain validation]
    C --> D[rate-limit Traffic Flow Inspection]
    D -->|429| E[Return Rate-Limiting Response]
    D -->|Through| F[express.json Analysis Body]
    F --> G[multer File Upload Processing]
    G -->|The file is too large/Format error| H[next error]
    G -->|Through| I[express-validator Verification]
    I -->|Verification Failed| J[Back 400 Validation Error]
    I -->|Through| K[Business Routing Processing]
    K -->|Business Error| L[next error]
    K -->|Success| M[Unified Response Encapsulation success/fail]
    M --> N[Back JSON Response]
    L --> O[Global Error Handling Middleware]
    H --> O
    O --> P[Standardized Error Responses code/data/message]
    P --> N

    style E fill:#f66,stroke:#333,color:#fff
    style J fill:#f66,stroke:#333,color:#fff
    style P fill:#f66,stroke:#333,color:#fff
    style N fill:#6f6,stroke:#333

(2) مبادئ ترتيب تحميل البرامج الوسيطة



10. مثال شامل: إكمال تكوين أمان واجهة برمجة التطبيقات (Express API)

▶ مثال: خادم Express مخصص للإنتاج

JAVASCRIPT 📖 للعرض فقط
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const multer = require('multer');
const { body, param, validationResult } = require('express-validator');
const compression = require('compression');
const path = require('path');

const config = require('./config');

const app = express();

app.use(helmet());
app.use(cors({ origin: config.corsOrigin, credentials: true }));
app.use(compression({ threshold: 1024 }));
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));

const globalLimiter = rateLimit({
  windowMs: config.rateLimit.windowMs,
  max: config.rateLimit.max,
  standardHeaders: true,
  legacyHeaders: false,
  message: { code: 429, data: null, message: 'error.rate_limited' }
});
app.use(globalLimiter);

const upload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/',
    filename: (req, file, cb) => {
      const ext = path.extname(file.originalname);
      cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
    }
  }),
  fileFilter: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    if (config.upload.allowedTypes.some(t => `.${t}` === ext)) {
      cb(null, true);
    } else {
      cb(new Error('error.invalid_file_type'), false);
    }
  },
  limits: { fileSize: config.upload.maxFileSize, files: 5 }
});

const responseHandler = (req, res, next) => {
  res.success = (data = null, message = 'ok') => {
    res.json({ code: 0, data, message });
  };
  res.fail = (message = 'error.internal', code = -1, data = null) => {
    res.json({ code, data, message });
  };
  next();
};
app.use(responseHandler);

const validate = (rules) => [
  ...rules,
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        data: errors.array().map(e => ({ field: e.path, message: e.msg })),
        message: 'error.validation_failed'
      });
    }
    next();
  }
];

app.post('/api/users',
  validate([
    body('username').isLength({ min: 3, max: 20 }).withMessage('error.username_length'),
    body('email').isEmail().withMessage('error.invalid_email').normalizeEmail(),
    body('password').isLength({ min: 8 }).withMessage('error.password_length')
  ]),
  (req, res) => {
    const user = createUser(req.body);
    res.success(user, 'ok');
  }
);

app.post('/api/upload',
  upload.array('files', 5),
  (req, res) => {
    if (!req.files || req.files.length === 0) {
      return res.fail('error.no_file_uploaded', 400);
    }
    const urls = req.files.map(f => `/uploads/${f.filename}`);
    res.success(urls, 'ok');
  }
);

app.get('/api/users/:id',
  validate([param('id').isMongoId().withMessage('error.invalid_id')]),
  (req, res) => {
    const user = findUser(req.params.id);
    if (!user) return res.fail('error.user_not_found', 404);
    res.success(user);
  }
);

app.use('/uploads', express.static('uploads', { maxAge: '30d', dotfiles: 'deny' }));

app.use((req, res) => {
  res.status(404).json({ code: 404, data: null, message: 'error.not_found' });
});

class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.isOperational = true;
  }
}

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(413).json({ code: 413, data: null, message: 'error.file_too_large' });
    }
    return res.status(400).json({ code: 400, data: null, message: 'error.upload_failed' });
  }
  const status = err.status || 500;
  const message = err.isOperational ? err.message : 'error.internal';
  if (config.env === 'development') console.error(err.stack);
  res.status(status).json({ code: status, data: null, message });
});

app.listen(config.port, () => {
  console.log(`Server running on port ${config.port} [${config.env}]`);
});
120 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

11. ملخص هذا الدرس



❓ أسئلة شائعة

س أيهما يُنفَّذ أولاً، البرامج الوسيطة أم المسارات؟
ج يتم تنفيذها بالترتيب الذي تم تسجيلها به. تُنفَّذ البرامج الوسيطة العامة المسجَّلة باستخدام app.use قبل البرامج الوسيطة الخاصة بالمسارات، أما البرامج الوسيطة الموجودة داخل المسارات فتُنفَّذ بالترتيب المحدد في المسارات.
س كيف يتم تطبيق نظام إصدارات واجهة برمجة التطبيقات (API)؟
ج هناك ثلاث طرق شائعة: مسارات عناوين URL (/v1/users)، ورؤوس الطلبات (Accept: application/vnd.api.v1+json)، ومعلمات الاستعلام (?version=1).
س هل تتطلب البرمجيات الوسيطة الخاصة بمعالجة الأخطاء 4 معلمات؟
ج نعم. يتعرف Express على البرمجيات الوسيطة الخاصة بمعالجة الأخطاء من خلال عدد المعلمات؛ فيجب أن تكون موقعة على النحو التالي: (err, req, res, next)، وإلا فسيتم التعامل معها على أنها برمجيات وسيطة عادية.
س كيف يمكنني تطبيق تقييد معدل الطلبات؟
ج استخدم البرمجيات الوسيطة express-rate-limit، وقم بتعيين النافذة الزمنية windowMs وعدد الطلبات max، وقم بإرجاع رمز الحالة 429 عند تجاوز الحد الأقصى.
س كيف يمكنني تحسين أداء تقديم الملفات الثابتة؟
ج في بيئة الإنتاج، استخدم Nginx أو شبكة توزيع المحتوى (CDN) لاستضافة الملفات الثابتة، واجعل Express يتولى معالجة واجهات برمجة التطبيقات (API) الديناميكية فقط؛ أما في بيئة التطوير، فيمكنك استخدام ذاكرة التخزين المؤقت maxAge التي يوفرها express.static.

📖 ملخص

📝 تمارين

  1. إضافة برنامج وسيط عام لمعالجة الأخطاء إلى مشروع Express قائم للتمييز بين الأخطاء التشغيلية (isOperational) والأخطاء غير المعروفة؛ وبالنسبة للأخطاء غير المعروفة، إرجاع رسالة عامة دون الكشف عن تتبع المكدس.
  2. استخدم express-validator لكتابة سلسلة تحقق كاملة لواجهة برمجة التطبيقات الخاصة بتسجيل المستخدم (اسم المستخدم، والبريد الإلكتروني، وقوة كلمة المرور، وتأكيد كلمة المرور)، وقم بإرجاع مصفوفة من الأخطاء على مستوى الحقول في حالة فشل عملية التحقق.
  3. قم بتكوين multer لتمكين تحميل صور الملف الشخصي (ملف واحد، بحد أقصى 2 ميغابايت، بتنسيق JPG أو PNG فقط). قم بإرجاع عنوان URL للملف عند نجاح التحميل، وإرجاع السبب المحدد للفشل في حالة فشل التحميل.
  4. إضافة «express-rate-limit» لتطبيق الحد التدريجي لعدد الطلبات: 100 طلب إجمالاً كل 15 دقيقة، و30 طلبًا على واجهة برمجة التطبيقات (API) في الدقيقة، و5 محاولات تسجيل دخول فاشلة كل 15 دقيقة.
  5. قم بإنشاء ملف باسم .env.example يتضمن قائمة بجميع متغيرات البيئة وقيمها الافتراضية، وتأكد من إضافة .env إلى .gitignore.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%