EduFlow
أساسيات JavaScript
⁦0%⁩

الوحدة 5 · كتابة برامج حقيقية

الوحدات (Modules): import وexport

15 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

تقسّم برنامجًا متناميًا إلى وحدات (modules) باستخدام export وimport، وتحمّلها في المتصفّح بـ type=“module” من خادم تطوير، وتنظّم Pocket في ملفات لكل منها مهمة واضحة.

  1. 1الوحدة ملف له نطاقه الخاص
  2. 2تحميل الوحدات في المتصفّح
  3. 3الوحدات في Node.js
  4. 4تنظيم Pocket

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

صار لدى Pocket دوال مساعدة للمال، وتحقّق من المدخلات، وتخزين، وعرض، ومعالجات أحداث، وكود شبكة. وفي ملف واحد يعني هذا مئات الأسطر، تستطيع فيها كل دالة رؤية كل متغيّر، وتغيير جزء واحد يعني التمرير متجاوزًا كل الأجزاء الأخرى. البرامج الحقيقية تُقسَّم إلى ملفات، لكل منها مهمة واحدة، وطريقة JavaScript في ذلك هي الوحدات (modules).

الوحدة ملف له نطاقه الخاص

في الوحدة، تكون المتغيّرات والدوال في المستوى الأعلى خاصة بذلك الملف. ولتشارك شيئًا، تُصدّره بـ export. ولتستخدم شيئًا من وحدة أخرى، تستورده بـ import:

src/money.js
export function toCents(dollars) {
  return Math.round(dollars * 100);
}

export function formatMoney(cents, symbol = "$") {
  return `${symbol}${(cents / 100).toFixed(2)}`;
}

const SECRET_ROUNDING_NOTE = "not exported, so only this file can see it";
src/main.js
import { toCents, formatMoney } from "./money.js";

console.log(formatMoney(toCents(12.2)));

الأقواس المعقوفة تسرد التصديرات المسمّاة (named exports) التي تريدها. والمسار يبدأ بـ ./، ومعناه “نسبةً إلى هذا الملف”، وفي المتصفّح يتضمّن الامتداد .js. أما SECRET_ROUNDING_NOTE فلا يمكن استيرادها أصلًا؛ وهذا هو المقصود. كل وحدة تقرّر ما تقدّمه، وكل ما عدا ذلك مخفي، مثل الـ closures في القسم 2 تمامًا، لكن على مستوى الملف.

صيغ أخرى ستراها:

src/main.js
import { formatMoney as fmt } from "./money.js";   // rename on import
import * as money from "./money.js";                // everything, as money.toCents etc.

وهناك أيضًا التصدير الافتراضي (default export)، واحد لكل ملف، يُستورد دون أقواس معقوفة وبأي اسم تحبّه: export default function renderApp() {} ثم import renderApp from "./ui.js". كثير من الفرق تفضّل التصديرات المسمّاة، وكذلك هذه الدورة: فالتصدير المسمّى يحمل الاسم نفسه في كل ملف يستخدمه، لذلك يعمل البحث وإعادة التسمية التلقائية في محرّرك بشكل موثوق.

تحميل الوحدات في المتصفّح

يُحمَّل ملف الدخول بـ type="module"، وهو يستورد كل ما عداه:

index.html
<script type="module" src="./src/main.js"></script>

تتصرّف سكربتات الوحدات بشكل مختلف قليلًا عن السكربتات الكلاسيكية التي استخدمتها حتى الآن. فهي مؤجَّلة تلقائيًا، فيكون الـ DOM جاهزًا حين تُنفَّذ. وتعمل في الوضع الصارم (strict mode)، الذي يحوّل بعض الأخطاء الصامتة إلى أخطاء ظاهرة. وكل وحدة تُنفَّذ مرة واحدة، مهما كان عدد الملفات التي تستوردها، لذلك يكون المتغيّر على مستوى الوحدة مشتركًا بين كل من يستوردها.

عمليًا، تستخدم معظم المشاريع أداة بناء مثل Vite. إنها تقدّم وحداتك أثناء التطوير، وتتيح لك استيراد الحزم من npm بأسمائها (import { something } from "some-package")، وعند البناء تحزم كل شيء في ملفات قليلة محسّنة. وصياغة import وexport التي تكتبها هي نفسها تمامًا.

الوحدات في Node.js

يدعم Node.js الصياغة نفسها. يُعامَل الملف على أنه وحدة ES حين يحتوي أقرب package.json على "type": "module"، أو حين ينتهي اسم الملف بـ .mjs. (والإصدارات الحديثة من Node.js تكتشف أيضًا صياغة import في ملف .js عادي، لكن ضبط "type" صراحةً أوضح.) وستصادف أيضًا نظام Node.js الأقدم، CommonJS، الذي يستخدم require() وmodule.exports؛ وما زال شائعًا في المشاريع القائمة، لكن الكود الجديد يستخدم import وexport.

تنظيم Pocket

التقسيم الجيد يتبع المهام، ويُبقي الأجزاء التي تتعامل مع العالم الخارجي منفصلة عن المنطق النقي:

يستورد main.js من ui.js وstorage.js وexpenses.js. ويستورد ui.js من money.js. أما expenses.js وmoney.js فمنطق نقي لا يمسّ الصفحة ولا التخزين أبدًا. main.js ui.js storage.js expenses.js money.js الأخضر: منطق نقي، بلا DOM ولا تخزين
وحدات Pocket ومن يستورد ماذا
  • money.js: toCents وformatMoney. نقية.
  • expenses.js: addExpense وremoveExpense وtotalCents وtotalsByCategory وvalidateExpense. نقية: بيانات تدخل، وبيانات تخرج.
  • storage.js: loadExpenses وsaveExpenses. الملف الوحيد الذي يعرف بوجود localStorage.
  • ui.js: renderExpenses وغيرها من كود الـ DOM.
  • main.js: نقطة الدخول. تحمّل البيانات، وتربط مستمعي الأحداث، وتستدعي البقية.

والقاعدة وراء هذا التقسيم: الاعتماديات تتّجه من الخارج إلى الداخل. main.js يعرف كل شيء؛ وexpenses.js لا يعرف شيئًا سوى البيانات. وهذا يعني أنك تستطيع اختبار المنطق الأساسي، أو إعادة استخدامه في Node.js، دون متصفّح، وتستطيع لاحقًا استبدال localStorage بخادم بتغيير ملف واحد.

بيئة التجربة في هذه الدورة تشغّل ملفًا واحدًا في كل مرة، لذلك يدور التمرين أدناه حول قراءة كود الوحدات. وفي الدرس التالي ستتعرّف إلى طريقة أخرى لتنظيم الكود، هي الأصناف (classes)، التي تجمع البيانات مع الدوال التي تعمل عليها.

أهم النقاط

  • الوحدة (module) ملف له نطاقه الخاص؛ ولا شيء فيه مرئي للملفات الأخرى ما لم يُصدَّر.
  • فضّل التصدير المسمّى (export function formatMoney)، فهو يحتفظ بالاسم نفسه في كل مكان ويسهل البحث عنه.
  • في المتصفّح، حمّل ملف الدخول بـ <script type="module">، واستخدم مسارات نسبية مع الامتداد .js، وقدّم الملفات عبر http.
  • في Node.js، تكون الملفات وحدات ES حين يحتوي أقرب package.json على "type": "module" أو حين ينتهي اسم الملف بـ .mjs.
  • نظّم حسب المهمة: المنطق النقي في وحدات لا تمسّ الصفحة أبدًا، وملف دخول رفيع يربط المنطق والتخزين والـ DOM معًا.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

يقول console المتصفّح: Uncaught SyntaxError: Cannot use import statement outside a module. ما الحل؟

السؤال 2 من 3

يصرّح money.js بـ function toCents(d) { … } دون export. ماذا يحدث حين ينفّذ main.js السطر import { toCents } from "./money.js";؟

السؤال 3 من 3

تفتح index.html بالنقر المزدوج عليه، فيفشل سكربت الوحدة في التحميل بخطأ CORS. لماذا؟

تدريب

اكتشف الخطأ

لماذا لا تُحمَّل وحدات Pocket؟

قُسّم Pocket إلى وحدتين، والآن لا تعرض الصفحة شيئًا ويُبلغ الـ console عن أخطاء. ملف HTML يحمّل src/main.js بشكل صحيح بـ type="module" من خادم ملفات ثابتة بسيط (دون أداة بناء مثل Vite). اختر السطرين اللذين يمنعان هاتين الوحدتين من العمل في المتصفّح.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.