مجالات الأسماء والوحدات في TypeScript

من موسوعة حسوب


ملاحظة حول المصطلحات

تغيّرت طريقة استخدام المصطلحات في النسخة TypeScript 1.5، إذ كانت مجالات الأسماء (namespaces) تُسمّى قديمًا بمصطلح "الوحدات الداخليّة (internal modules)"، وما كان يُسمّى بالوحدات الخارجيّة (External modules) أصبح يُسمّى ببساطة بمصطلح "الوحدات (modules)" وهو ما سنغطّيه في هذه الصفحة. ويجب استخدام الكلمة المفتاحية ‎namespace‎ في الأماكن التي كانت تُستخدَم فيها الكلمة المفتاحية ‎module‎ للتصريح عن وحدة داخليّة في النسخ التي سبقت TypeScript 1.5، أي أنّ عليك استخدام ‎namespace X {‎ عوضًا عن ‎module X {‎ (انظر صفحة مجالات الأسماء للاستزادة)، وذلك تجنّبًا لإرباك المستخدمين الجدد.

مقدمة

تُوضِّح هذه الصّفحة الأساليب التي يُمكن بها تنظيم الشيفرة باستخدام مجالات الأسماء والوحدات في TypeScript. سنغطّي هنا بعض المواضيع المتقدّمة في آليّة استخدام مجالات الأسماء والوحدات، وسنتطرّق إلى بعض الأخطار الشائعة التي يقع فيها من يستخدم هاذين الأسلوبين في TypeScript.

انظر توثيق الوحدات للمزيد من المعلومات حولها، وانظر توثيق مجالات الأسماء كذلك لمزيدٍ من المعلومات عنها.

استخدام مجالات الأسماء

مجالاتُ الأسماءِ مُجرّدُ كائنات JavaScript مُسمّاةٍ (named JavaScript objects) في مجال الأسماء العام (global namespace). ما يجعل مجالات الأسماء بنيةً سهلة الاستخدام. يُمكن تقسيمها على عدّة ملفّات، ويُمكن ربطها وجمعها باستخدام خيار المترجم ‎‎--outFile‎‎. يُمكن لمجالات الأسماء أن تكون طريقة جيّدة لهيكلة شيفرتك في تطبيق ويب (Web Application)، إذ يُمكن وضع جميع الاعتماديّات داخل وسوم ‎<script>‎ في صفحة HTML.

لكن مجالات الأسماء تأتي بضريبة تلويث مجال الأسماء العام، إذ قد تُصبح البرمجيات معقّدةً لدرجةٍ تُصعّب التعرّف على اعتماديات المكونات، خاصّة في التطبيقات الضّخمة.

استخدام الوحدات

كما هي الحال مع مجالات الأسماء، يُمكن للوحدات احتواء كل من الشيفرة (code) والتصريحات (declarations) كذلك. الفرق الرئيسيّ هو أنّ الوحدات تُصرّح عن الاعتماديات التي تعتمد عليها.

تعتمد الوحدات على مُحمِّل وحدات (module loader) كذلك مثل CommonJs أو Require.js. وقد لا يكون هذا جيّدًا في تطبيقات JavaScript الصغيرة، لكنّ دفع ثمن الاعتماد على مُحمّل وحدات يأتي بفوائد عديدة في التطبيقات الكبيرة، خاصّةً فائدةُ قابليّة الصيانة والحفاظ على الأداء، إضافةً للتقسيم الفعّال. تسمح الوحدات للمبرمج بإعادة استخدامٍ أفضلَ للشيفرة، وفصلٍ أقوى بين وظائف البرنامج، إضافة إلى توفير أدواتٍ أفضلَ لدعم التحزيم (bundling).

يجدر الذكر إلى أنّ تطبيقات Node.js تعتمد على الوحدات افتراضيًّا وهي الأسلوب المنصوح به لتنظيم شيفرتك.

بدءًا من نسخة ECMAScript 2015، أصبحت الوحدات جزءًا أصيلًا (native part) من اللغة، ومن المفترض أن تدعمها جميع تطبيقات المُحرّكات (engine implementations) المتوافقة (أي كل ما يُشغّل شيفرات JavaScript مثل المتصفحات وغيرها). لذا فالوحدات هي الطريقة المنصوح بها لتنظيم الشيفرة في المشاريع الجديدة.

أخطار مجالات الأسماء والوحدات

سَنَصِفُ في هذا القسم بعض الأخطار الشائعة التي قد يقع فيها مستخدم مجالات الأسماء والوحدات وكيفيّة تجنّبها.

إحالة وحدة بالبنية ‎/// <reference>

استخدام بنية ‎/// <reference ... />‎ للإحالة إلى ملفّ وحدةٍ ما عوضًا عن استدعائها بالجملة ‎import‎ من الأخطاء الشائعة. ولفهم الفرق نحتاج أولًا إلى فهم كيفيّة بحثِ المُترجم عن معلومات الأنواع لوحدةٍ اعتمادًا على مسار الاستيراد (المقطعُ ‎...‎ في ‎import x from "...";‎ و‎import x = require("...");‎ وما إلى ذلك).

سيحاول المترجم العثور على ملف ذي الامتداد ‎.ts‎، أو ‎.tsx‎، وبعد ذلك مُحاولة العثور على ملف ذي الامتداد ‎.d.ts‎ ذو المسار المناسب. إذا لم يُعثَر على ملفٍّ معيّن، فسيبحث المترجم عندئذٍ عن تصريحِ وحدةٍ مُحيطة (ambient module declaration)، والتي يُصرَّح عنها في ملفٍّ ذي الامتداد .d.ts.

- ‎myModules.d.ts

// ضع هذه الشيفرة في ملفّ
// .d.ts
// أو ملفّ
// .ts (على شرط ألّا يكون وحدةً)

declare module "SomeModule" {
    export function fn(): string;
}

- ‎myOtherModule.ts

/// <reference path="myModules.d.ts" />
import * as m from "SomeModule";

يسمح لنا وسمُ الإحالة هنا بالعثور على ملفّ التصريح (declaration file) الذي يحتوي على التصريح عن الوحدة المحيطة. وهكذا يُستخدَم ملفّ ‎node.d.ts‎ والعديد من عيّنات TypeScript الأخرى.

استخدام مجالات الأسماء بغير حاجة

عند تحويلك لبرنامجٍ من الاعتماد على مجالات الأسماء إلى التنظيم بالوحدات، فمن السهل أن تصل إلى ملفٍّ مشابه لما يلي:

- ‎shapes.ts

export namespace Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
}

وحدة ‎Shapes‎ الموجودة على المستوى الأعلى (top-level module) هنا تُحيط بكلّ من ‎Triangle‎ و‎Square‎ بدون أي سبب. هذا مُربكٌ ومزعج لمستخدمي وحدتِك:

- ‎shapeConsumer.ts

import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes? هذا غير معقول

استحالةُ أن تُساهم وحدتان بأسمائهما في نفس المجال من مزايا الوحدات في TypeScript (أي أن الأسماء في الوحدة A لن تُشارَك أبدًا في نفس مجال الوحدة B، والعكس صحيح). ولأنّ مستخدم الوحدة يقرّر الاسم الذي سيُسمّي به الوحدة عند استيرادها، فلا حاجة لإحاطة الرموز المصدّرة (exported symbols) داخل مجال أسماء.

تجنَّبْ إحاطة محتوى وحدتِك بمجال أسماء لأنّ الهدف العامّ من مجالات الأسماء هو توفير تجميع البنيات منطقيًّا ولتفادي اصطدامات الأسماء (أي المشاكل التي قد تنشب بسبب وجود كائنين بنفس الاسم). ولأنّ ملفّ الوحدة نفسه تجميع منطقيّ أصلًا، ولمّا كان الاسم على المستوى الأعلى (top-level name) مُعرَّفًا من طرف الشيفرة التي تستورد الوحدة، فلا حاجة لاستخدام طبقة وحدة إضافيّة للكائنات المصدَّرة.

هذا مثال مُراجَعٌ مُنقَّح:

- ‎shapes.ts

export class Triangle { /* ... */ }
export class Square { /* ... */ }

- ‎shapeConsumer.ts

import * as shapes from "./shapes";
let t = new shapes.Triangle();

مساوئ الوحدات

كما يوجد انسجامُ واحدٍ لواحدٍ (one-to-one) بين ملفّات JavaScript والوحدات، فهناك كذلك في لغة TypeScript انسجامٌ بين ملفات الوحدات المصدريّة وملفّات JavaScript المولَّدة لها. أحد تأثيرات هذا الأمر هو أنّه لا يُمكن تجميع عدّة ملفّات وحدات مصدريّة حسب نظام الوحدات الذي يستهدفه المبرمج. على سبيل المثال، لا يُمكن استخدام خيار ‎outFile‎ عند استهداف نظام ‎commonjs‎ أو نظام ‎umd‎، لكن مع النسخة TypeScript 1.8 وما تلاها، فقد أصبح بالإمكان استخدام الخيار ‎outFile‎ عند استهداف ‎نظام ‎amd‎ أو نظام ‎system‎.

مصادر