كن مطوّر واجهات أمامية محترفا
5 دقيقة قراءة

وحدات ES والأدوات الحديثة: من السكربتات إلى التطبيقات

دليل عملي لوحدات JavaScript وسلسلة أدوات البناء — التصديرات المسمّاة والافتراضية، وصياغة import، ولماذا للوحدات نطاقها الخاص، والاستيراد الساكن مقابل الديناميكي، ورسم الوحدات البياني، وnpm وpackage.json، والمُجمِّعات مثل Vite، وكيف يعمل البناء الحديث — مع تمارين عملية وحلولها.

القفزة من "بضعة وسوم <script>" إلى "تطبيق حقيقي" تدور غالبًا حول الوحدات والأدوات. وحدات ES تتيح تقسيم الشيفرة إلى ملفّات بنطاقها الخاص وتبعياتها الصريحة؛ وnpm والمُجمِّعات تحوّل كومة الملفّات تلك إلى شيء سريع يعمل في متصفح. فهم هذه الطبقة هو ما يتيح قراءة أي قاعدة شيفرة حديثة وإعداد قاعدتك. (يبني على مقدّمة الوحدات في أساسيات JavaScript.)

الوحدة ملفّ بـ نطاق خاص — لا شيء يتسرّب للخارج إلا إن صدّرته بـ export، ولا شيء يدخل إلا إن استوردته بـ import. تلك الصراحة هي الهدف كلّه: تصير التبعيات مرئيّة ويتوقّف فضاء الأسماء العام عن كونه مكبًّا.

التصديرات: المسمّاة والافتراضية

تكشف الوحدة قيمًا بـ export. هناك نكهتان، وغالبًا تُخلَطان:

// math.js

// تصديرات مسمّاة — أي عدد، تُستورَد بأسمائها الدقيقة
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export class Vector { /* ... */ }

// تصدير افتراضي — واحد على الأكثر لكل وحدة، "الشيء الرئيسي"
export default function multiply(a, b) { return a * b; }

استخدم التصديرات المسمّاة لوحدة تقدّم عدّة أشياء (ملفّ أدوات)؛ واستخدم تصديرًا افتراضيًّا لوحدة مهمّتها شيء واحد (مكوّن أو صنف واحد). يمكن أن يكون كلاهما في ملفّ واحد.

الاستيرادات

الاستيراد يعكس التصدير. الاستيرادات المسمّاة تستخدم أقواسًا ويجب أن تطابق الأسماء المصدَّرة؛ والافتراضي يُستورَد بلا أقواس بأي اسم تختاره:

// app.js
import multiply, { PI, add } from "./math.js";  // افتراضي + مسمّى معًا
import { add as sum } from "./math.js";          // أعد تسمية استيراد مسمّى
import * as math from "./math.js";               // فضاء أسماء: math.PI, math.add
import "./styles.css";                            // استيراد أثر جانبي (يشغّل الملفّ)

بضع قواعد: المسار يحتاج الامتداد في المتصفح (./math.js)، و./ أو ../ يعلّم ملفًّا محليًّا (اسم مجرّد مثل "react" حزمة)، وأسماء الاستيراد ارتباطات حيّة للقراءة فقط — ترى القيمة الحالية للوحدة المصدِّرة لكن لا تستطيع إعادة إسنادها.

نطاق الوحدة والوضع الصارم

هذا الفرق السلوكيّ عن السكربتات القديمة. لكل وحدة نطاقها العلويّ الخاصconst x في قمّة وحدة خاصّ بذلك الملفّ، لا عام:

// a.js
const secret = 42;   // ليس عامًّا — غير مرئيّ لملفّات أخرى
export const shared = "مرئيّ فقط إن استُورد";

تعمل الوحدات أيضًا في الوضع الصارم تلقائيًّا، وهي مؤجّلة (deferred) افتراضيًّا (لا تحجب تحليل HTML). في المتصفح تختار ذلك بـ type="module":

<script type="module" src="/app.js"></script>

الاستيراد الساكن مقابل الديناميكي

عبارة import أعلاه ساكنة — تُحَلّ قبل تشغيل الشيفرة، فتستطيع المُجمِّعات رؤية رسم التبعيات كاملًا. حين تحتاج تحميل وحدة شرطيًّا أو كسولًا (فقط عند استخدام ميزة)، استخدم import() الديناميكي، الذي يُرجِع وعدًا:

button.addEventListener("click", async () => {
  const { Chart } = await import("./chart.js");  // يُحمَّل عند الطلب فقط
  new Chart(/* ... */);
});

الاستيراد الديناميكي هو كيف يعمل تقسيم الشيفرة (code splitting): تُنزَّل الميزات الثقيلة فقط عند الحاجة، مُبقيةً الحزمة الأولية صغيرة. الاستيرادات الساكنة هي الافتراضي؛ والجأ للديناميكي للتحميل الكسول.

رسم الوحدات البياني

تطبيقك رسم بياني: ملفّ دخول يستورد آخرين، يستوردون آخرين، نزولًا إلى الأوراق. يبدأ المُجمِّع من الدخول، ويتبع كل import، ويجمّع الرسم كاملًا. وهنا أيضًا يحدث هزّ الشجرة (tree-shaking) — يستطيع المُجمِّع إسقاط دوال مصدَّرة لا تستوردها أبدًا، فلا تشحن مكتبة أدوات كبيرة سوى الأجزاء التي تستخدمها (سبب أهمّية التصديرات المسمّاة لحجم الحزمة).

npm وpackage.json

npm هو سجلّ الحزم والأداة التي تثبّت التبعيات. package.json هو البيان في جذر كل مشروع:

{
  "name": "my-app",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": { "react": "^19.0.0" },
  "devDependencies": { "vite": "^6.0.0" }
}
  • npm install يقرأ هذا ويُنزّل الحزم إلى node_modules.
  • dependencies تُشحَن في الإنتاج؛ و**devDependencies** للبناء/الاختبار فقط.
  • scripts اختصارات تشغّلها بـ npm run dev.
  • ^ في ^19.0.0 يسمح بتحديثات متوافقة؛ وpackage-lock.json يثبّت الإصدارات المثبّتة بالضبط للتكرارية.

المُجمِّعات: لماذا وماذا

تستطيع المتصفحات تشغيل الوحدات أصليًّا، لكن تحميل مئات الملفّات الصغيرة عبر الشبكة بطيء، وحزم node_modules غالبًا ليست جاهزة للمتصفح. المُجمِّع (Vite، esbuild، webpack، Rollup) يحلّ هذا بـ:

  • تجميع رسم الوحدات في بضعة ملفّات محسّنة،
  • ترجمة (transpiling) الصياغة الحديثة/TypeScript/JSX إلى ما تشغّله المتصفحات،
  • هزّ التصديرات غير المستخدمة وتصغير (minifying) الخرج،
  • تقديم خادم تطوير سريع مع الاستبدال الحارّ للوحدات (تحديثات فورية أثناء التحرير).

Vite الافتراضي الحديث الشائع: بدء تطوير فوريّ (يقدّم وحدات ES أصلية في التطوير) وبناء إنتاج محسّن. نادرًا ما تضبطه يدويًّا لتطبيق قياسيّ — npm create vite@latest يبني هيكله.

الأخطاء الشائعة

  • نسيان امتداد الملفّ (./math مقابل ./math.js) في الوحدات الأصلية للمتصفح.
  • خلط الاستيرادات المسمّاة والافتراضية — import { multiply } بينما كان تصديرًا افتراضيًّا.
  • توقّع أن يكون const علويّ عامًّا — للوحدات نطاق خاص.
  • محاولة await import() متزامنًا — الاستيراد الديناميكي يُرجِع وعدًا.
  • وضع تبعية وقت تشغيل في devDependencies (أو العكس)، فيكسر بناء الإنتاج.
  • إيداع node_modules بدل الاعتماد على package.json + ملفّ القفل.
  • استيراد مكتبة كاملة لدالّة واحدة، فيُبطِل هزّ الشجرة — استورد التصدير المسمّى.

تمارين

جرّب كلًّا منها قبل فتح الحل.

تمرين 1 — صدّر واستورد

أنشئ utils.js يصدّر clamp(n, min, max) مسمّى واستورده في app.js.

إظهار الحل
// utils.js
export function clamp(n, min, max) {
  return Math.min(Math.max(n, min), max);
}

// app.js
import { clamp } from "./utils.js";
clamp(15, 0, 10); // 10

تصدير مسمّى، واستيراد مسمّى بأقواس مطابقة — النمط القياسي لوحدة أدوات.

تمرين 2 — افتراضي + مسمّى

Button.js ينبغي أن يصدّر المكوّن افتراضيًّا ويصدّر أيضًا ثابتًا مسمّى BUTTON_SIZES. استورد كليهما.

إظهار الحل
// Button.js
export const BUTTON_SIZES = ["sm", "md", "lg"];
export default function Button() { /* ... */ }

// app.js
import Button, { BUTTON_SIZES } from "./Button.js";

الافتراضي (بلا أقواس، اسمك من اختيارك) والاستيراد المسمّى (بأقواس) يجتمعان في عبارة واحدة.

تمرين 3 — حمّل كسولًا عند الطلب

حمّل وحدة ./editor.js الثقيلة فقط حين ينقر المستخدم "تحرير".

إظهار الحل
editBtn.addEventListener("click", async () => {
  const { Editor } = await import("./editor.js");
  new Editor();
});

import() الديناميكي يُرجِع وعدًا ويُنزّل الوحدة فقط وقت النقر — تقسيم شيفرة يُبقي التحميل الأولي صغيرًا.

النموذج الذهني الذي تحتفظ به

الوحدة ملفّ بـ نطاق خاص: القيم تعبر الحدّ فقط عبر export/import، ما يجعل التبعيات صريحة ويقتل التلوّث العام. استخدم التصديرات المسمّاة للوحدات متعدّدة الأشياء وافتراضيًّا لأحادية الغرض؛ وتذكّر أن الاستيرادات ارتباطات حيّة للقراءة فقط وأن الوحدات تعمل مؤجّلةً في الوضع الصارم. import الساكن يبني رسم الوحدات الذي يصعده المُجمِّع (ويهزّه)؛ و**import() الديناميكي** يحمّل كسولًا عند الطلب للتقسيم. فوقه تجلس سلسلة الأدوات: npm + package.json يديران التبعيات والسكربتات، ومُجمِّع مثل Vite يترجم ويجمّع ويهزّ ويقدّم خادم تطوير سريعًا. معًا هذه ما يحوّل مجلّد ملفّات .js إلى تطبيق قابل للشحن.