الوحدات: الاستيراد والتصدير
لماذا توجد الوحدات
في الأيام الأولى لجافاسكريبت، كانت جميع السكريبتات تتشارك نطاقا عاما واحدا. إذا قمت بتحميل ثلاثة ملفات جافاسكريبت منفصلة في HTML الخاص بك، فإن كل متغير ودالة معرفة في ملف واحد كانت مرئية لجميع الملفات الأخرى. هذا خلق تعارضات مستمرة في الأسماء وأخطاء غير متوقعة وكودا كان من المستحيل تقريبا صيانته على نطاق واسع. تخيل فريقا من عشرة مطورين يكتبون جميعهم دوال في نفس النطاق العام -- في النهاية سيسمي شخصان دالة بـ init() أو handleClick()، وسيقوم أحدهما بالكتابة فوق الآخر بصمت. الوحدات تحل هذه المشكلة بإعطاء كل ملف نطاقه الخاص وتوفير آليات صريحة لمشاركة ما هو مطلوب فقط.
كانت الرحلة إلى وحدات جافاسكريبت الحديثة طويلة وتطورت عبر عدة مراحل. فهم هذا التاريخ يساعدك على تقدير لماذا تعمل وحدات ES بالطريقة التي تعمل بها ولماذا لا تزال تصادف أنماطا أقدم في قواعد الكود الحالية.
عصر وسم Script: مشاكل النطاق العام
قبل وجود أي نظام وحدات، كان المطورون يحملون جافاسكريبت باستخدام عدة وسوم <script>. كل سكريبت كان ينفذ في نفس النطاق العام، مما يعني أن ترتيب وسوم السكريبت كان مهما بشكل حاسم. إذا كان app.js يعتمد على دالة من utils.js، كان عليك تحميل utils.js أولا. كانت التبعيات ضمنية ويديرها المطور بالكامل.
مثال: مشكلة النطاق العام
<!-- utils.js يعرف دالة عامة -->
<script src="utils.js"></script>
<!-- analytics.js يعرف أيضا formatDate بشكل عام -->
<script src="analytics.js"></script>
<!-- app.js يستدعي formatDate -- لكن أيهما؟ -->
<script src="app.js"></script>
<!-- آخر سكريبت يعرف formatDate هو الذي يفوز -->
<!-- هذه مشكلة صامتة وصعبة التتبع -->
مع نمو التطبيقات في التعقيد، أصبح هذا النهج غير قابل للإدارة. احتاج المطورون إلى طريقة لتغليف الكود وإعلان التبعيات الصريحة وتجنب تلويث النطاق العام.
نمط IIFE: التغليف المبكر
قبل وصول أنظمة الوحدات الرسمية، استخدم المطورون تعبيرات الدوال المستدعاة فوريا (IIFE) لإنشاء نطاقات خاصة. يلف IIFE الكود في دالة تنفذ فوريا، مما يبقي المتغيرات خاصة داخل نطاق الدالة. فقط القيم المرفقة صراحة بالكائن العام أو المرجعة من IIFE تكون متاحة خارجيا.
مثال: نمط وحدة IIFE
// mathUtils هو المتغير العام الوحيد المنشأ
var mathUtils = (function() {
// خاص -- غير متاح خارج هذا IIFE
var PI = 3.14159265359;
var E = 2.71828182846;
function circleArea(radius) {
return PI * radius * radius;
}
function circleCircumference(radius) {
return 2 * PI * radius;
}
// واجهة عامة -- فقط هذه متاحة
return {
circleArea: circleArea,
circleCircumference: circleCircumference
};
})();
console.log(mathUtils.circleArea(5)); // 78.5398...
console.log(mathUtils.circleCircumference(5)); // 31.4159...
console.log(typeof PI); // "undefined" -- خاص
كانت IIFEs تحسينا لكنها لا تزال تتطلب إدارة يدوية لترتيب التحميل والتبعيات. كما اعتمدت على إرفاق الوحدات بالنطاق العام من خلال اصطلاحات التسمية، وهو أمر هش.
CommonJS: وحدات لـ Node.js
عندما أنشئ Node.js في عام 2009، اعتمد نظام وحدات CommonJS. يستخدم CommonJS دالة require() لاستيراد الوحدات وmodule.exports لتصدير القيم. كل ملف يعامل كوحدة منفصلة بنطاقها الخاص. تحمل وحدات CommonJS بشكل متزامن، وهو ما يعمل جيدا على الخادم حيث تقرأ الملفات من القرص المحلي لكنه مشكلة في المتصفحات حيث يجب جلب الملفات عبر الشبكة.
مثال: وحدات CommonJS (في Node.js)
// mathUtils.js -- تصدير CommonJS
const PI = 3.14159265359;
function circleArea(radius) {
return PI * radius * radius;
}
function circleCircumference(radius) {
return 2 * PI * radius;
}
module.exports = {
circleArea,
circleCircumference
};
// app.js -- استيراد CommonJS
const { circleArea, circleCircumference } = require('./mathUtils');
console.log(circleArea(10)); // 314.159...
console.log(circleCircumference(10)); // 62.831...
require() وmodule.exports في عدد لا يحصى من حزم npm. ومع ذلك، يدعم Node.js الآن وحدات ES أصليا والنظام البيئي يتحول تدريجيا نحوها.وحدات ES: المعيار الرسمي
أدخلت وحدات ES (ESM) في ES2015 (ES6) كمعيار الوحدات الرسمي لجافاسكريبت. على عكس CommonJS، تستخدم وحدات ES عبارات import وexport القابلة للتحليل الثابت -- يمكن للمحرك تحديد جميع الاستيرادات والتصديرات في وقت التحليل دون تنفيذ الكود. هذا يمكن من تحسينات قوية مثل إزالة الكود الميت (tree shaking)، حيث تزيل أدوات التجميع التصديرات غير المستخدمة من الحزمة النهائية. تحمل وحدات ES أيضا بشكل غير متزامن، مما يجعلها مناسبة لكل من المتصفحات والخوادم.
الطبيعة الثابتة لوحدات ES تعني أنه لا يمكنك استخدام import داخل عبارة if أو بناء مسار الوحدة ديناميكيا بربط السلاسل النصية (رغم أن import() الديناميكي يتعامل مع هذه الحالات، كما سنرى لاحقا). هذا القيد مقصود -- يسمح للأدوات بتحليل رسم التبعيات في وقت البناء.
التصديرات والاستيرادات المسماة
تسمح لك التصديرات المسماة بتصدير عدة قيم من وحدة. كل قيمة مصدرة لها اسم محدد، ويجب على كود الاستيراد استخدام ذلك الاسم بالضبط (أو إعادة تسميته صراحة). هذا ينشئ تبعيات واضحة وموثقة ذاتيا.
مثال: التصديرات المسماة
// validators.js
// تصدير التعريفات الفردية مباشرة
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function validateEmail(email) {
return EMAIL_REGEX.test(email);
}
export function validatePassword(password) {
if (password.length < 8) {
return { valid: false, message: 'يجب أن تكون كلمة المرور 8 أحرف على الأقل' };
}
if (!/[A-Z]/.test(password)) {
return { valid: false, message: 'يجب أن تحتوي كلمة المرور على حرف كبير' };
}
if (!/[0-9]/.test(password)) {
return { valid: false, message: 'يجب أن تحتوي كلمة المرور على رقم' };
}
return { valid: true, message: 'كلمة المرور صالحة' };
}
export function validateUsername(username) {
if (username.length < 3 || username.length > 20) {
return { valid: false, message: 'يجب أن يكون اسم المستخدم 3-20 حرفا' };
}
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
return { valid: false, message: 'يمكن أن يحتوي اسم المستخدم فقط على حروف وأرقام وشرطات سفلية' };
}
return { valid: true, message: 'اسم المستخدم صالح' };
}
مثال: استيراد التصديرات المسماة
// form.js
// استيراد الدوال التي تحتاجها فقط
import { validateEmail, validatePassword, validateUsername } from './validators.js';
function handleFormSubmit(formData) {
const emailResult = validateEmail(formData.email);
const passwordResult = validatePassword(formData.password);
const usernameResult = validateUsername(formData.username);
if (!emailResult) {
console.error('عنوان بريد إلكتروني غير صالح');
return false;
}
if (!passwordResult.valid) {
console.error(passwordResult.message);
return false;
}
if (!usernameResult.valid) {
console.error(usernameResult.message);
return false;
}
console.log('نجحت جميع عمليات التحقق!');
return true;
}
يمكنك أيضا تجميع كل التصديرات في نهاية الملف باستخدام قائمة تصدير، وهو ما يفضله بعض الفرق لأنه يجعل واجهة API العامة للوحدة مرئية فورا:
مثال: قائمة التصدير في النهاية
// helpers.js
const TAX_RATE = 0.08;
function calculateTax(amount) {
return amount * TAX_RATE;
}
function calculateTotal(amount) {
return amount + calculateTax(amount);
}
function formatCurrency(amount) {
return '$' + amount.toFixed(2);
}
// عبارة تصدير واحدة في النهاية
export { TAX_RATE, calculateTax, calculateTotal, formatCurrency };
التصديرات والاستيرادات الافتراضية
يمكن لكل وحدة أن تحتوي على تصدير افتراضي واحد على الأكثر. التصديرات الافتراضية مفيدة عندما تحتوي الوحدة على قيمة أساسية واحدة -- فئة أو كائن تكوين أو دالة رئيسية أو مكون. يمكن لكود الاستيراد اختيار أي اسم للاستيراد الافتراضي بدون استخدام الأقواس المعقوفة.
مثال: التصدير الافتراضي
// Logger.js
class Logger {
constructor(prefix) {
this.prefix = prefix;
this.logs = [];
}
info(message) {
const entry = `[INFO] ${this.prefix}: ${message}`;
this.logs.push(entry);
console.log(entry);
}
warn(message) {
const entry = `[WARN] ${this.prefix}: ${message}`;
this.logs.push(entry);
console.warn(entry);
}
error(message) {
const entry = `[ERROR] ${this.prefix}: ${message}`;
this.logs.push(entry);
console.error(entry);
}
getHistory() {
return [...this.logs];
}
}
export default Logger;
مثال: استيراد تصدير افتراضي
// app.js
// يمكنك تسمية الاستيراد الافتراضي بأي اسم تريده
import Logger from './Logger.js';
const appLogger = new Logger('App');
appLogger.info('بدأ التطبيق');
appLogger.warn('ملف التكوين غير موجود، استخدام الإعدادات الافتراضية');
// يمكنك أيضا تسميته بشكل مختلف
import AppLogger from './Logger.js'; // نفس الشيء، اسم مختلف
خلط التصديرات الافتراضية والمسماة
يمكن للوحدة أن تحتوي على تصدير افتراضي وتصديرات مسماة معا. هذا شائع في المكتبات حيث تكون الوظيفة الرئيسية هي التصدير الافتراضي بينما دوال المساعدة أو الثوابت هي تصديرات مسماة.
مثال: التصديرات المختلطة
// httpClient.js
// تصديرات مسماة للتكوين والمساعدات
export const DEFAULT_TIMEOUT = 5000;
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
export function buildQueryString(params) {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
// تصدير افتراضي للفئة الرئيسية
class HttpClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.timeout = options.timeout || DEFAULT_TIMEOUT;
this.headers = options.headers || {};
}
async get(path, params = {}) {
const query = buildQueryString(params);
const url = query ? `${this.baseURL}${path}?${query}` : `${this.baseURL}${path}`;
const response = await fetch(url, {
method: 'GET',
headers: this.headers
});
return response.json();
}
async post(path, body) {
const response = await fetch(`${this.baseURL}${path}`, {
method: 'POST',
headers: { ...this.headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return response.json();
}
}
export default HttpClient;
مثال: استيراد التصديرات المختلطة
// api.js
// استيراد الافتراضي والمسمى في عبارة واحدة
import HttpClient, { DEFAULT_TIMEOUT, buildQueryString } from './httpClient.js';
const client = new HttpClient('https://api.example.com', {
timeout: DEFAULT_TIMEOUT * 2,
headers: { 'Authorization': 'Bearer token123' }
});
// استخدام التصدير المسمى مباشرة
const query = buildQueryString({ page: 1, limit: 20 });
console.log(query); // "page=1&limit=20"
إعادة تسمية الاستيرادات والتصديرات باستخدام كلمة as
عند الاستيراد من عدة وحدات تصدر قيما بنفس الاسم، أو عندما تريد توفير اسم أكثر سياقية، استخدم كلمة as لإعادة تسمية الاستيرادات. يمكنك أيضا إعادة تسمية التصديرات عند التصدير.
مثال: إعادة تسمية الاستيرادات
// كلتا الوحدتين تصدران دالة تسمى "validate"
import { validate as validateEmail } from './emailValidator.js';
import { validate as validatePhone } from './phoneValidator.js';
// الآن يمكنك استخدام كليهما بدون تعارض
validateEmail('user@example.com');
validatePhone('+1-555-0123');
مثال: إعادة تسمية التصديرات
// internalUtils.js
function _parseDate(str) {
// التنفيذ الداخلي
return new Date(str);
}
function _formatDate(date) {
return date.toISOString().split('T')[0];
}
// التصدير بأسماء عامة الواجهة
export {
_parseDate as parseDate,
_formatDate as formatDate
};
استيراد النطاق: استيراد كل شيء
أحيانا تريد استيراد جميع التصديرات المسماة من وحدة ككائن واحد. هذا يسمى استيراد النطاق ويستخدم صيغة * as. هذا يتجنب قوائم الاستيراد الطويلة ويوفر تسمية واضحة في كودك.
مثال: استيراد النطاق
// استيراد كل شيء من وحدة المدققين
import * as validators from './validators.js';
// الوصول إلى التصديرات كخصائص لكائن النطاق
console.log(validators.validateEmail('test@example.com'));
console.log(validators.validatePassword('Secure123'));
console.log(validators.EMAIL_REGEX);
إعادة التصدير: إنشاء ملفات البرميل
مع نمو المشاريع، غالبا ما تنظم الوحدات المرتبطة في مجلدات. ملف البرميل (يسمى عادة index.js) يعيد تصدير القيم من عدة وحدات في مجلد، مما يوفر نقطة دخول واحدة للاستيراد. هذا يبسط مسارات الاستيراد وينشئ واجهة API عامة نظيفة لكل مجلد.
مثال: بنية المشروع مع ملفات البرميل
src/
utils/
stringUtils.js
dateUtils.js
arrayUtils.js
index.js <-- ملف البرميل
validators/
emailValidator.js
passwordValidator.js
index.js <-- ملف البرميل
services/
apiService.js
authService.js
index.js <-- ملف البرميل
مثال: ملف البرميل (إعادة التصدير)
// utils/stringUtils.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function truncate(str, maxLength) {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + '...';
}
// utils/dateUtils.js
export function formatDate(date) {
return date.toLocaleDateString('ar-SA', {
year: 'numeric', month: 'long', day: 'numeric'
});
}
export function daysFromNow(days) {
const date = new Date();
date.setDate(date.getDate() + days);
return date;
}
// utils/arrayUtils.js
export function unique(arr) {
return [...new Set(arr)];
}
export function chunk(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
// utils/index.js -- ملف البرميل
export { capitalize, truncate } from './stringUtils.js';
export { formatDate, daysFromNow } from './dateUtils.js';
export { unique, chunk } from './arrayUtils.js';
مثال: استيرادات نظيفة مع ملفات البرميل
// بدون ملف البرميل -- مطول، عدة أسطر استيراد
import { capitalize, truncate } from './utils/stringUtils.js';
import { formatDate } from './utils/dateUtils.js';
import { unique } from './utils/arrayUtils.js';
// مع ملف البرميل -- استيراد واحد نظيف
import { capitalize, truncate, formatDate, unique } from './utils/index.js';
يمكنك أيضا إعادة تصدير التصديرات الافتراضية وإعادة تسمية إعادة التصديرات:
مثال: إعادة التصدير المتقدمة
// services/index.js
// إعادة تصدير الافتراضي كتصدير مسمى
export { default as ApiService } from './apiService.js';
export { default as AuthService } from './authService.js';
// إعادة تصدير جميع التصديرات المسماة من وحدة
export * from './constants.js';
// إعادة التصدير مع إعادة التسمية
export { fetchUser as getUser } from './apiService.js';
export * from. إذا صدرت وحدتان قيمة بنفس الاسم، ستحصل على خطأ أو ستحجب إحداهما الأخرى بصمت حسب البيئة. فضل إعادة التصديرات الصريحة للحفاظ على قابلية التنبؤ بملف البرميل.import() الديناميكي لتقسيم الكود
عبارات import الثابتة تحمل الوحدات قبل تنفيذ كودك، مما يعني أن كل الكود المستورد يضمن في الحزمة الأولية. للتطبيقات الكبيرة، هذا يمكن أن يعني تحميل ميغابايتات من جافاسكريبت قد لا يحتاجها المستخدم أبدا. import() الديناميكي يحل هذا بتحميل الوحدات عند الطلب في وقت التشغيل. يرجع وعدا (Promise) يتحقق بكائن الوحدة.
مثال: الاستيراد الديناميكي لتقسيم الكود
// تحميل مكتبة رسوم بيانية ثقيلة فقط عند حاجة المستخدم
async function showDashboard() {
const statusElement = document.getElementById('status');
statusElement.textContent = 'جاري تحميل لوحة المعلومات...';
try {
// هذه الوحدة تحمل فقط عند استدعاء هذه الدالة
const { default: ChartLibrary } = await import('./charts/ChartLibrary.js');
const chart = new ChartLibrary('#dashboard-container');
chart.render(dashboardData);
statusElement.textContent = 'تم تحميل لوحة المعلومات!';
} catch (error) {
statusElement.textContent = 'فشل تحميل لوحة المعلومات';
console.error('فشل الاستيراد الديناميكي:', error);
}
}
// مكتبة الرسوم البيانية لا تحمل حتى يتم النقر على الزر
document.getElementById('show-dashboard-btn')
.addEventListener('click', showDashboard);
مثال: استيرادات مشروطة بناء على إجراء المستخدم
// تحميل وحدات مختلفة بناء على تفضيل المستخدم
async function loadTheme(themeName) {
try {
const themeModule = await import(`./themes/${themeName}.js`);
themeModule.apply();
console.log(`تم تطبيق السمة "${themeName}" بنجاح`);
} catch (error) {
console.error(`السمة "${themeName}" غير موجودة، استخدام الافتراضية`);
const defaultTheme = await import('./themes/default.js');
defaultTheme.apply();
}
}
// تحميل التنسيق الخاص بالمنطقة عند الطلب
async function loadLocaleFormatter(locale) {
const formatter = await import(`./locales/${locale}.js`);
return formatter;
}
import() الديناميكي هو الأساس لتقسيم الكود في أدوات التجميع الحديثة مثل Webpack و Rollup و Vite. عندما تصادف أداة التجميع استيرادا ديناميكيا، تنشئ تلقائيا جزءا منفصلا (ملفا) يحمل فقط عند الحاجة. هذا يمكن أن يحسن بشكل كبير وقت تحميل الصفحة الأولي للتطبيقات الكبيرة.نطاق الوحدة والوضع الصارم
لوحدات ES نطاقها الخاص -- المتغيرات المعرفة في وحدة لا تضاف إلى الكائن العام. كل وحدة لها قيمة this خاصة بها في المستوى الأعلى وهي undefined (وليس window كما في السكريبتات العادية). بالإضافة إلى ذلك، تعمل وحدات ES دائما في الوضع الصارم تلقائيا حتى بدون توجيه "use strict". هذا يعني أن سلوكيات جافاسكريبت المتساهلة محظورة في الوحدات.
مثال: اختلافات نطاق الوحدة
// regularScript.js (محمل بـ <script src="...">)
var globalVar = 'أنا متغير عام';
console.log(window.globalVar); // "أنا متغير عام"
console.log(this === window); // true
// esModule.js (محمل بـ <script type="module" src="...">)
var moduleVar = 'أنا محدود النطاق بهذه الوحدة';
console.log(window.moduleVar); // undefined -- ليس على window
console.log(this); // undefined -- ليس window
// الوضع الصارم تلقائي في الوحدات
// هذه ستطلق جميعها أخطاء في وحدة:
// undeclaredVar = 42; // ReferenceError
// delete Object.prototype; // TypeError
// function f(a, a) {} // SyntaxError -- معاملات مكررة
تحميل الوحدات في المتصفح
لاستخدام وحدات ES مباشرة في المتصفح، أضف type="module" إلى وسم السكريبت. سكريبتات الوحدات مؤجلة تلقائيا، مما يعني أنها لا تحجب تحليل HTML وتنفذ بعد تحليل DOM بالكامل (يعادل سمة defer على السكريبتات العادية). سكريبتات الوحدات تنفذ أيضا مرة واحدة فقط حتى لو استوردت نفس الوحدة عدة مرات عبر ملفات مختلفة.
مثال: استخدام الوحدات في HTML
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>وحدات ES في المتصفح</title>
</head>
<body>
<h1>عرض الوحدات</h1>
<div id="output"></div>
<!-- سكريبت عادي -- ينفذ فورا، يحجب التحليل -->
<script src="legacy.js"></script>
<!-- سكريبت وحدة -- مؤجل تلقائيا، له نطاقه الخاص -->
<script type="module" src="app.js"></script>
<!-- سكريبت وحدة مضمن -->
<script type="module">
import { formatDate } from './utils/dateUtils.js';
document.getElementById('output').textContent = formatDate(new Date());
</script>
<!-- بديل للمتصفحات التي لا تدعم الوحدات -->
<script nomodule src="fallback-bundle.js"></script>
</body>
</html>
nomodule هي آلية بديلة ذكية. المتصفحات التي تفهم type="module" ستتجاهل السكريبتات ذات nomodule. المتصفحات الأقدم التي لا تفهم type="module" ستتخطى سكريبت الوحدة (معاملة إياه كنوع غير معروف) وتنفذ سكريبت nomodule بدلا من ذلك. هذا يتيح لك تقديم كود حديث قائم على الوحدات للمتصفحات الحديثة وحزمة بديلة للمتصفحات الأقدم.التبعيات الدائرية
تحدث التبعية الدائرية عندما تستورد الوحدة أ من الوحدة ب، والوحدة ب تستورد أيضا من الوحدة أ. بينما يمكن لوحدات ES التعامل مع التبعيات الدائرية (على عكس CommonJS الذي يمكن أن ينتج وحدات محملة جزئيا)، فإنها غالبا تشير إلى مشكلة في التصميم ويجب إعادة هيكلتها عند الإمكان.
مثال: مشكلة التبعية الدائرية
// user.js
import { createDefaultSettings } from './settings.js';
export class User {
constructor(name) {
this.name = name;
this.settings = createDefaultSettings(this);
}
}
// settings.js
import { User } from './user.js';
export function createDefaultSettings(user) {
return {
theme: 'light',
language: 'ar',
notifications: true,
displayName: user.name
};
}
// هذه دائرية: user.js -> settings.js -> user.js
// وحدات ES تتعامل مع هذا لكنه يمكن أن يسبب مشاكل دقيقة
// إذا كان توقيت توفر التصديرات مهما
مثال: حل التبعيات الدائرية
// الحل: استخراج المنطق المشترك إلى وحدة ثالثة
// أو إعادة الهيكلة بحيث تتدفق التبعية باتجاه واحد
// types.js -- مشتركة، بدون تبعيات دائرية
export function createDefaultSettings(displayName) {
return {
theme: 'light',
language: 'ar',
notifications: true,
displayName
};
}
// user.js -- يستورد من types.js فقط
import { createDefaultSettings } from './types.js';
export class User {
constructor(name) {
this.name = name;
this.settings = createDefaultSettings(this.name);
}
}
// settings.js -- يستورد من types.js فقط، بدون تبعية دائرية
import { createDefaultSettings } from './types.js';
export { createDefaultSettings };
تنظيم مشروع حقيقي باستخدام الوحدات
في مشروع جيد التنظيم، كل وحدة لها مسؤولية واحدة. جمع الوحدات المرتبطة في مجلدات واستخدم ملفات البرميل لاستيرادات نظيفة وحافظ على تدفق رسم التبعيات في اتجاه واحد (من الوحدات عالية المستوى إلى المساعدات منخفضة المستوى). إليك مثال عملي لتطبيق قائمة مهام منظم بالوحدات:
مثال: بنية وحدات تطبيق المهام
src/
models/
Todo.js // فئة المهمة مع التحقق
TodoList.js // منطق المجموعة
index.js // برميل: export { Todo } from './Todo.js'; ...
services/
storageService.js // حفظ/تحميل من localStorage
apiService.js // مزامنة مع الخادم
index.js // ملف البرميل
ui/
renderer.js // معالجة DOM
eventHandlers.js // معالجات النقر والإدخال
index.js // ملف البرميل
utils/
dateUtils.js // مساعدات تنسيق التاريخ
idGenerator.js // توليد معرف فريد
index.js // ملف البرميل
app.js // نقطة الدخول -- يستورد من البراميل
مثال: نقطة الدخول باستخدام استيرادات البرميل
// app.js -- نقطة الدخول
import { Todo, TodoList } from './models/index.js';
import { storageService } from './services/index.js';
import { renderer, eventHandlers } from './ui/index.js';
// تهيئة التطبيق
function init() {
const savedTodos = storageService.load('todos');
const todoList = new TodoList(savedTodos);
renderer.renderTodoList(todoList.getAll());
eventHandlers.onAddTodo((text) => {
const todo = new Todo(text);
todoList.add(todo);
storageService.save('todos', todoList.getAll());
renderer.renderTodoList(todoList.getAll());
});
eventHandlers.onToggleTodo((id) => {
todoList.toggle(id);
storageService.save('todos', todoList.getAll());
renderer.renderTodoList(todoList.getAll());
});
eventHandlers.onDeleteTodo((id) => {
todoList.remove(id);
storageService.save('todos', todoList.getAll());
renderer.renderTodoList(todoList.getAll());
});
}
init();
ملخص أفضل ممارسات الوحدات
اتباع هذه الإرشادات سيساعدك على كتابة كود جافاسكريبت قائم على الوحدات نظيف وقابل للصيانة:
- وحدة واحدة، مسؤولية واحدة -- كل ملف يجب أن يقوم بشيء واحد جيدا. إذا نمت الوحدة لأكثر من 200-300 سطر، فكر في تقسيمها.
- فضل التصديرات المسماة -- أسهل في إعادة الهيكلة والاستيراد التلقائي والاكتشاف. استخدم التصديرات الافتراضية أساسا للفئة أو المكون الرئيسي للملف.
- استخدم ملفات البرميل لاستيرادات نظيفة -- أنشئ
index.jsفي كل مجلد يعيد تصدير واجهة API العامة. أبق الوحدات الداخلية خاصة بعدم إعادة تصديرها. - تجنب التبعيات الدائرية -- إذا احتاجت وحدتان إلى بعضهما، استخرج المنطق المشترك إلى وحدة ثالثة.
- استخدم
import()الديناميكي للميزات الكبيرة -- لوحات الإدارة ومكتبات الرسوم البيانية والميزات الثقيلة الأخرى يجب أن تحمل عند الطلب. - أبق عبارات الاستيراد في الأعلى -- الاستيرادات الثابتة يجب أن تكون في المستوى الأعلى للوحدة. إبقاؤها مجمعة في الأعلى يحسن المقروئية.
- ضمن دائما امتداد الملف -- في بيئات المتصفح ووضع ESM في Node.js، يجب تضمين
.jsفي مسارات الاستيراد. قد تسمح أدوات التجميع بحذفه لكن الوضوح أكثر أمانا وقابلية للنقل.
تمرين عملي
ابنِ تطبيق آلة حاسبة قائم على الوحدات بالبنية التالية. أنشئ مجلد math/ يحتوي على أربع وحدات: basic.js (يصدر add وsubtract وmultiply وdivide)، وadvanced.js (يصدر power وsquareRoot وfactorial)، وconverters.js (يصدر celsiusToFahrenheit وfahrenheitToCelsius وkmToMiles وmilesToKm)، وindex.js كملف برميل يعيد تصدير كل شيء. أنشئ وحدة history.js تصدر افتراضيا فئة CalculationHistory مع التوابع add(entry) وgetAll() وclear() وgetLast(n). أنشئ ملف calculator.js رئيسي يستورد من ملف البرميل ووحدة التاريخ وينفذ خمس عمليات حسابية مختلفة على الأقل ويسجل كل نتيجة مع وحدة التاريخ ويطبع سجل الحسابات في النهاية. إضافة: استخدم import() الديناميكي لتحميل وحدة converters.js فقط عند استدعاء دالة تحويل. اختبر وحداتك في المتصفح باستخدام وسوم سكريبت type="module" وتحقق من أن متغيرات وحدة واحدة غير متاحة في نطاق وحدة أخرى.