مجالات الأسماء في TypeScript

من موسوعة حسوب
< TypeScript
مراجعة 13:10، 18 سبتمبر 2018 بواسطة عبد-الهادي-الديوري (نقاش | مساهمات) (←‏مصادر)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى التنقل اذهب إلى البحث


مقدمة

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

بداية

لنبدأ بالتعرف على البرنامج الذي سنستخدمه في مثالنا على هذه الصفحة. كتبنا مجموعة صغيرة من مُصادقات السلاسل النصيّة (string validators)، كما قد يكتبها من يريد التحقّق من أنّ مُدخلات المستخدم على حقل في صفحة ويب صالحةٌ أو لا، أو التحقّق من صحّة صيغة ملفّ بيانات وُفِّر خارجيًّا (كملفٍّ مرفوع من طرف المستخدم مثلًا).

وضع المصادقات في ملفّ واحد

interface StringValidator {
    isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// بعض العيّنات لتجربتها
let strings = ["Hello", "98052", "101"];

// المُصادقات المرغوب استخدامها
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// اعرض ما إذا كانت السلسلة النّصيّة مُصادقَةً أو لا من طرف كل مُصادِق
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
    }
}

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

عند إضافة المزيد من المُصادِقات، سنحتاج إلى تنظيم الشيفرة بشكلٍ ما لتنظيم الأنواع وتجنّب اصطدامات الأسماء بين الكائنات (أي المشاكل التي قد تنشب بسبب وجود كائنين بنفس الاسم). وعوضًا عن إنشاء العديد من الأسماء في مجال الأسماء العام (global namespace)، لنُحِط (wrap) الكائنات في مجال أسماء واحد.

في هذا المثال، سننقُل جميع الكائنات المتعلّقة بمُصادقة البيانات إلى مجال أسماء سنُسمّيه ‎Validation‎. ولأنّنا نريد للأصناف والواجهات في هذا المجال أن تكون مرئيّةً خارج مجال الأسماء، فسنُصدّرها بالكلمة المفتاحية المحجوزةِ ‎export‎. والمتغيّران ‎lettersRegexp‎ و‎numberRegexp‎ في المقابل مُجرَّد تفاصيلِ تطبيقٍ (implementation details) أي أنّ الأصناف الأخرى تعتمد عليهما فقط ولا يُعتمَد عليهما في غير ذلك ولا نحتاج لاستخدامهما مباشرةً، لذا سنتركهما دون تصدير ولن يكونا مرئيّين خارج مجال الأسماء. في شيفرة الاختبار في أسفل هذا الملفّ، سنحتاج إلى تعديل أسماء الأنواع عند استخدامها خارج مجال الأسماء (الاسمُ ‎Validation.LettersOnlyValidator‎ عوضًا عن الاسمِ ‎LettersOnlyValidator‎ على سبيل المثال).

جمع المصادقات في مجال أسماء واحد

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// بعض العيّنات لتجربتها
let strings = ["Hello", "98052", "101"];

// المُصادقات المرغوب استخدامها
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// اعرض ما إذا كانت السلسلة النّصيّة مُصادقَةً أو لا من طرف كل مُصادِق
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

تقسيم الشيفرة على عدة ملفات

سنحتاج مع نموّ تطبيقنا إلى تقسيم الشيفرة على عدّة ملفّات لتسهيل صيانتها وتبسيطها.

استخدام أسماء المجالات في عدّة ملفّات

هنا نقسم مجال الأسماء ‎Validation‎ الخاص بنا على عدّة ملفّات. ورغم أنّ الملفّات منقسمة، إلّا أنّها جميعًا تُساهم في نفس مجال الأسماء ويُمكن استخدامها وكأنّها مُعرَّفة في مكان واحد. ولأنّ هناك اعتمادياتٍ بين الملفّات، فسنُضيف وسومَ إحالةٍ (reference tags) لإخبار المترجم عن العلاقات بين الملفّات (ركّز على جمل ‎reference‎ في الأمثلة أدناه). أمّا بقيّة شيفرة الاختبار فستبقى كما هي دون تغيير.

(الملفّ ‎Validation.ts‎)

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}

(الملفّ ‎ZipCodeValidator.ts‎)

/// <reference path="Validation.ts" />
namespace Validation {
    const numberRegexp = /^[0-9]+$/;
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}


(الملفّ ‎Test.ts‎)

/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// بعض العيّنات لتجربتها
let strings = ["Hello", "98052", "101"];

// المُصادقات المرغوب استخدامها
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// اعرض ما إذا كانت السلسلة النّصيّة مُصادقَةً أو لا من طرف كل مُصادِق
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

حالما يكون لدينا عدّة ملفّات، فسنحتاج إلى التأكّد من أنّ الشيفرة المُترجَمة ستُحمَّل بنجاح. هناك طريقتان للقيام بهذا.

أولًا، يُمكننا استخدام شيفرةٍ متسلسلة مربوطة في ملفّ واحد بالخيار ‎--outFile‎ لترجمة جميع الملفّات المُدخلة (input files) إلى ملفّ JavaScript مُخرجٍ واحد (output file):

tsc --outFile sample.js Test.ts

سيُرتِّب المترجم الملفّ المُخرَج تلقائيًّا حسب وسوم الإحالة الموجودة في الملفّات. ويُمكنك كذلك تحديد كلّ ملفّ على حدة:

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

وكبديل لهذا، يُمكننا استخدام ترجمة لكل ملفّ (وهو السلوك الافتراضيّ) لتوليد ملفّ JavaScript لكلّ ملفّ مُدخَل. إذا وُلِّدت عدّة ملفّات JavaScript، فسنحتاج إلى استخدام وسوم ‎<script>‎ على صفحة الويب الخاصة بنا لتحميل كلّ ملفّ بالترتيب الملائم، مثلًا:

(مقطع من الملفّ ‎MyTestPage.html‎)

    <script src="Validation.js" type="text/javascript" />
    <script src="LettersOnlyValidator.js" type="text/javascript" />
    <script src="ZipCodeValidator.js" type="text/javascript" />
    <script src="Test.js" type="text/javascript" />

الأسماء البديلة (Aliases)

يُمكن استخدام الأسماء البديلة لتبسيط العمل مع مجالات الأسماء، وتكون باستخدام البنية ‎import q = x.y.z‎‎ لإنشاء أسماءٍ مُختصَرة للكائنات التي يكثُر الاعتماد عليها. لكن احذر من خلطها مع بنية ‎‎import x = require("name")‎‎ التي تُحمِّل الوحدات (انظر صفحة الوحدات)، تُنشئ هذه البنية اسمًا بديلًا للرّمز (symbol) المُعطى. يُمكنك استخدام هذا النّوع من الاستيرادات (تُعرَف كذلك بالأسماء البديلة) لأيّ نوعٍ من المُعرِّفات (identifier)، ما يشمل الكائنات التي تُنشؤها استيراداتُ الوحدات (module imports).

namespace Shapes {
    export namespace Polygons {
        export class Triangle { }
        export class Square { }
    }
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // هو نفسُه الاستدعاءُ 'new Shapes.Polygons.Square()'

لاحظ أنّنا لا نستخدم الكلمة المفتاحية ‎require‎؛ بل نُعيِّن مباشرةً من الاسم الجديد للرّمز الذي استوردنا. هذا مُشابه لاستخدام متغيّرٍ عاديّ، لكنّه يعمل كذلك على دلالات النوع ومجال الأسماء الخاصّة بالرّمز المُستورَد. وتجدر الإشارة كذلك إلى أنّ جملة ‎import‎ مرجع مختلفٌ عن الرّمز الأصليّ، لذا فتعديل متغيّرٍ بديل لن ينعكِس على المتغيّر الأصليّ.

العمل مع مكتبات JavaScript أخرى

لوصف شكل المكتبات التي لم تُكتَب بلغة TypeScript، سنحتاج إلى التصريح عن الواجهة البرمجيّة التي تُوفِّرها المكتبة. لأنّ معظم مكتبات JavaScript تُوفِّر عددًا قليلًا من الكائنات فقط على المستوى الأعلى (top-level)، مجالات الأسماء (namespaces) طريقة جيّدة لتمثيلها.

نُسمِّي التصريحات التي لا تُعرِّف تطبيقًا (implementation) بالتصريحات المُحيطة (ambient). عادةً ما تُعرَّف هذه التصريحات في ملفّات ‎.d.ts‎. إذا ألِفت استخدام لغة C أو C++‎ فيُمكنك التفكير فيها وكأنّها ملفّات ‎.h‎. لِنُلق نظرةً على بعض الأمثلة.

مجالات الأسماء المحيطة (Ambient Namespaces)

على سبيل المثال، تُعرِّف المكتبةُ المشهورة D3 وظيفتها في كائنٍ عامٍّ (global object) يُسمّى ‎d3‎. ولأنّ هذه المكتبة تُحمَّل عبر وسم ‎<script>‎ (عوضًا عن مُحمّل وحدات [module loader])، فتصريحُها يعتمد على مجالات الأسماء لتعريف شكلها. ولكي يرى مُتَرجمُ TypeScript هذا الشّكل، فسنستخدم تصريح مجال أسماءٍ مُحيطٍ. على سبيل المثال، يُمكننا أن نبدأ بكتابة التّصريح كما يلي:

(جزء مُبسّطٌ من الملفّ ‎D3.d.ts‎)

declare namespace D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        };
    }

    export interface Event {
        x: number;
        y: number;
    }

    export interface Base extends Selectors {
        event: Event;
    }
}

declare var d3: D3.Base;

مصادر