الوحدات في TypeScript

من موسوعة حسوب
< TypeScript
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى التنقل اذهب إلى البحث

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

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

مقدمة

أضافت نسخة ECMAScript 2015 مبدأ الوحدات (modules) إلى لغة JavaScript، وهذا المبدأ موجود كذلك في لغة TypeScript.

تُنفَّذ الوحدات داخل مجالها (scope) الخاص وليس في المجال العام (global scope)؛ هذا يعني بأنّ المتغيّرات، والدوال، والأصناف وغيرها من الكائنات المعرفة في وحدةٍ لا تكون مرئيّةً خارج الوحدة إلا في حالة صُدِّرَت صراحةً وبوضوح (explicitly exported) باستعمال أحد أشكال التعبير البرمجيّ ‎export‎. في المقابل، إذا أردت استخدام متغير أو دالة أو صنف أو واجهة أو غير ذلك ممّا صُدِّر من طرف وحدة أخرى، فلا بد لك أن تَسْتَورِدَها بأحد أشكال التعبير البرمجيّ ‎import‎.

الوحدات تصريحيّة (declarative)؛ أي أن العلاقة بين الوحدات تُحدَّد بالاستيرادات والتصديرات على مستوى الملف.

تستورد الوحدات بعضها البعض باستعمال مُحمِّل وحداتٍ (module loader). يكون محمِّل الوحدات أثناء التنفيذ مسؤولًا عن البحث عن جميع اعتماديات الوحدة وتنفيذها قبل تنفيذ الوحدة نفسها. بعض محمّلات الوحدات المعروفة في JavaScript تشمل: CommonJS لمنصّة Node.js، وrequire.js لتطبيقات الويب.

إذا احتوى أي ملفٍّ (في لغة TypeScript، كما هي الحال في نسخة ECMAScript 2015) على تعبير ‎import‎ أو ‎export‎ في المستوى الأعلى (top-level)، فسيُعَدّ هذا الملفُّ وحدةً. في المقابل، إذا لم يكن لملفٍّ أي تصريحات ‎import‎ أو تصريحات ‎export‎ فسيُعَدّ سكربتًا (script) محتوياته موجودة في المجال العام (أي أنّ محتوياته ستظهر للوحدات كذلك).

التصدير (Export)

تصدير تصريح (Exporting a declaration)

يُمكن تصدير أي تصريح (مثل التصريح عن متغيّرٍ، أو دالةٍ، أو صنفٍ، أو اسم نوعٍ بديلٍ، أو واجهةٍ) عبر إضافة الكلمة المفتاحيّة ‎export‎ كما يلي:

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

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

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

export const numberRegexp = /^[0-9]+$/;

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

جمل التصدير (Export statements)

جمل التصدير مفيدة عند الرغبة في إعادة تسميّة التصديرات لتُسمّى باسم مُغاير عند استيرادها، يُمكن مثلا كتابة المثال أعلاه على الشّكل:

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

لاحظ أنّ جملة التصدير ‎export { ZipCodeValidator as mainValidator };‎ ستُصدِّر الصنف ‎ZipCodeValidator‎ باسم ‎mainValidator‎ بدلًا من ‎ZipCodeValidator‎.

إعادة التصدير (Re-exports)

تُوسِّع الوحداتُ أحيانًا وحداتٍ أخرى (أي تُضيف إليها مكونات جديدة)، وتعرض بعضًا من ميّزات الوحدات الموسَّعة جزئيًّا. إعادة تصدير ما وُسِّع لا يستورد الوحدة محليًّا (أي داخل الوحدة التي أعيد فيها التصدير)، ولا تُضيف أي متغيرات محليّة جديدة:

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

export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// تصدير الصنف الأصلي مع إعادة تسميّته
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

يُمكن لوحدةٍ إحاطة (wrap) وحدة أخرى أو أكثر من وحدة واحدة ومزج جميع تصديراتها اختياريًّا باستخدام البنية ‎export * from "module"‎:

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

// تصدير الواجهة
// 'StringValidator'
export * from "./StringValidator";

// تصدير الصنف 
// 'LettersOnlyValidator'
export * from "./LettersOnlyValidator";

// تصدير الصنف
// 'ZipCodeValidator'
export * from "./ZipCodeValidator";

الاستيراد (Import)

الاستيراد من وحدة ما سهلٌ ومشابه للتصدير. يُستورد تصريح مُصدَّر (exported declaration) عبر استخدام أحد أشكال الجملة البرمجية ‎import‎ التاليّة:

استيراد تصدير واحد من وحدة ما

import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();

يُمكن إعادة تسمية الاستيرادات كذلك:

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

لاحظ كيف سمَّيْنا ‎ZipCodeValidator‎ بالاسم ‎ZCV‎ في التعبير ‎ZipCodeValidator as ZCV‎.

استيراد كامل الوحدة ووضعها في متغيّر للوصول إلى تصديراتها

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

استيراد وحدة لتأثيراتها الجانبية (side-effects) فقط

التأثيرات الجانبية هي كل ما يُغيِّر من طريقة عمل البرامج ضمنيًّا (مثل تغيير قيمة متغيّرٍ أو تغيير آليّة عمل حلقات ‎for‎، أو تخزين بيانات في ملف معيّن أو ما شابه ذلك).

ورغم أن هذه العمليّة غير منصوح بها، إلا أنها مُفيدة أحيانًا، إذ تُهيّئ الوحدات حالةً عامّةً (global state) تُؤثّر على آلية عمل البرمجيات ويمكن استخدامها من طرف وحدات أخرى. يُمكن لهذه الوحدات ألّا تملك أية تصديرات، أو أن مستوردها ليس في حاجة لأي من تصديراتها. لاستيراد هذه الوحدات، استعمل ما يلي:

import "./my-module.js";

التصديرات الافتراضية (Default exports)

يُمكن لكل وحدة أن تُصدّر تصديرًا افتراضيًا. تُعلَّم التصديرات الافتراضية بالكلمة المفتاحيّة ‎default‎؛ ويُمكن لكل وحدةٍ أن تحتويَ على تصديرٍ افتراضيّ واحدٍ فقط. تُستورَد التصديرات الافتراضية باستخدام شكل آخر من أشكال الاستيراد.

التصديرات الافتراضيّة مفيدة جدًّا. مثلًا، يُمكن لمكتبةٍ مثل مكتبة JQuery أن تحتوي على تصدير افتراضي للكائن ‎jQuery‎ أو الكائن ‎$‎، والذي سيُستَورَد عادةً بالاسم ‎$‎ أو الاسم ‎jQuery‎.

(الملفّ ‎JQuery.d.ts‎)

declare let $: JQuery;
export default $;

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

import $ from "JQuery";

$("button.continue").html( "Next Step..." );

يُمكن كتابة التصريحات عن الأصناف والدوال كتصديراتٍ افتراضيّةٍ مباشرةً. تصدير الأصناف والدوال لا يتطلب تسميتها، بل تسميتها اختياريّة فقط:

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

export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}

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

import validator from "./ZipCodeValidator";

let myValidator = new validator();

أو يُمكن التصدير والاستيراد كما يلي: (الملفّ ‎StaticZipCodeValidator.ts‎)

const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}

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

import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// استخدام الدالة
// validate
// التي صُدرت كتصدير افتراضي واستُوردت مباشرة
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});

يُمكن للتصديرات الافتراضيّة أن تكون مجرّد قيم عاديّة كذلك:

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

export default "123";

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

import num from "./OneTwoThree";

console.log(num); // "123"

الجملة ‎export =‎ والجملة ‎‎import = require()‎

يملك كلّ من محمل الوحدات CommonJS وAMD مبدأ كائنٍ يُسمّى ‎exports‎ يحتوي على جميع تصديرات الوحدة. وتدعم كذلك استبدال الكائن ‎exports‎ بكائنٍ مُخصّص واحد. أُضيفَ مبدأ التصديرات الافتراضية لتكون بديلًا لهذا السلوك؛ لكنهما لا يتوافقان. تدعم TypeScript بنية ‎export =‎ على غرار آلية العمل التقليدية في CommonJS وAMD.

تُحدِّد بنية ‎export =‎ كائنًا واحدًا يُصدَّر من الوحدة. يُمكن لهذا الكائن أن يكون صنفًا، أو واجهةً، أو مجال أسماءٍ، أو دالةً، أو ثابتًا متعدّدًا.

يجب استعمال البنية ‎import module = require("module")‎ الخاصّة بلغة TypeScript عند تصدير وحدةٍ باستخدام ‎export =‎.

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

let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;

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

import zip = require("./ZipCodeValidator");

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

// المُصادقات المرغوب استخدامها
let validator = new zip();


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

توليد الشيفرة للوحدات

يُولِّد المترجم الشيفرة الملائمة حسب الوحدة الهدف المُحدّدَة خلال الترجمة، وسيولِّد المترجم شيفرة لأنظمة تحميل الوحدات (module-loading systems) التاليّة: منصة Node.js (CommonJS)، أو require.js (AMD)، أو UMD، أو SystemJS أو وحدات ECMAScript 2015 الأصيلة (النسخة ES6). انظر توثيق محمّل الوحدات للاستزادة حول دور استدعاءات ‎define‎، و‎require‎، و‎register‎ في الشيفرة المولّدة لكل محمّل وحدات.

يعرض هذا المثال البسيط كيف تُترجم الأسماء المستخدمة عند الاستيراد والتّصدير إلى شيفرة تحميل الوحدات:

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

import m = require("mod");
export let t = m.something + 1;

(الملفّ ‎SimpleModule.js‎، نظام AMD / RequireJS)

define(["require", "exports", "./mod"], function (require, exports, mod_1) {
    exports.t = mod_1.something + 1;
});

(الملفّ ‎SimpleModule.js‎، نظام CommonJS / Node)

var mod_1 = require("./mod");
exports.t = mod_1.something + 1;

(الملفّ ‎SimpleModule.js‎، نظام UMD)

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports); if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./mod"], factory);
    }
})(function (require, exports) {
    var mod_1 = require("./mod");
    exports.t = mod_1.something + 1;
});

(الملفّ ‎SimpleModule.js‎، نظام System)

System.register(["./mod"], function(exports_1) {
    var mod_1;
    var t;
    return {
        setters:[
            function (mod_1_1) {
                mod_1 = mod_1_1;
            }],
        execute: function() {
            exports_1("t", t = mod_1.something + 1);
        }
    }
});

(الملفّ ‎SimpleModule.js‎، نظام وحدات ECMAScript 2015 الأصيلة [Native ECMAScript 2015 modules])

import { something } from "./mod";
export var t = something + 1;

مثال بسيط

دمجنا أدناه تطبيقات ‎Validator‎ المُستخدَمة في الأمثلة السابقة لتصديرِ تصديرٍ مُسمًّى (named export) واحدٍ فقط لكل وحدة.

لترجمة الشيفرة نحتاج إلى تحديد هدفِ وحدةٍ (module target) على سطر الأوامر. في منصّة Node.js استخدم الخيار ‎--module commonjs‎، وفي require.js استخدم الخيار ‎--module amd‎. مثلًا:

tsc --module commonjs Test.ts

بعد ترجمة الشيفرة، ستُصبح كل وحدةٍ ملفَّ ‎.js‎ مستقل. وكما الحال مع وسوم المراجع (reference tags) فالمترجم يتبع جمل ‎import‎ لترجمة الملفات الاعتمادية (dependent files).

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

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

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

import { StringValidator } from "./Validation";

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

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

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

import { StringValidator } from "./Validation";

const numberRegexp = /^[0-9]+$/;

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

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

import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";

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

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

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

تحميل الوحدات الاختياري وبعض حالات التحميل المتقدّمة

قد تحتاج في بعض الحالات إلى تحميل وحدةٍ واحدةٍ فقط حسب شروط معيّنة. يُمكننا في TypeScript استعمال النمط الموضّح أدناه لتطبيق هذا وتطبيق حالات التحميل المتقدّمة لاستدعاء مُحمّلات الوحدات مباشرة دون فقدان أمان الأنواع (type safety).

يكتشف المترجم ما إذا استُخدمَت كل وحدة في شيفرة JavaScript المولَّدة. إذا استُخدِمَ مُعرِّف وحدةٍ (module identifier) فقط كجزء من حواشي الأنواع (type annotations) وليس كتعبير، فعندئذ لن يُولَّد أي استدعاء ‎require‎ لهذه الوحدة. حذف المراجع غير المستخدَمة هذا من تحسينات الأداء (performance optimization) الجيّدة، إضافةً إلى أنّه يسمح بتحميل هذه الوحدات اختياريًّا كذلك.

فكرة النمط المركزيّة هي أنّ جملة ‎import id = require("...")‎ تعطينا القدرة على الوصول إلى الأنواع التي تُصدِّرها الوحدة. يُستدعى محمّل الوحدات ديناميكيًّا (عبر ‎require‎) كما هو موضّح في كتل (blocks) شروط ‎if‎ أدناه. هذا يستغل تحسين حذف المراجع غير المستخدَمة لتُحمَّل الوحدة فقط عند الحاجة. ولكي يعمل هذا النّمط، فمن المهمّ أن يُستخدَم الرمز (symbol) المعرَّف عبر جملة ‎import‎ في مواقع الأنواع فقط (ولا يجب أبدًا أن يُستخدَم في موقع قد يُولَّد في شيفرة JavaScript).

يُمكننا استعمال الكلمة المفتاحية ‎typeof‎ للحفاظ على أمان الأنواع. تُنتج الكلمة ‎typeof‎ نوعَ قيمةٍ معيّنة إن استُخدِمَت في موقع أنواع، وفي هذه الحالة فستُنتِج نوع الوحدة.

(تحميل الوحدات ديناميكيًّا في Node.js)

declare function require(moduleName: string): any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
    let validator = new ZipCodeValidator();
    if (validator.isAcceptable("...")) { /* ... */ }
}

(عيِّنة: تحميل الوحدات ديناميكيًّا في require.js)

declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;

import * as Zip from "./ZipCodeValidator";

if (needZipValidation) {
    require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
        let validator = new ZipCodeValidator.ZipCodeValidator();
        if (validator.isAcceptable("...")) { /* ... */ }
    });
}

(عيِّنة: تحميل الوحدات ديناميكيًّا في System.js)

declare const System: any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
        var x = new ZipCodeValidator();
        if (x.isAcceptable("...")) { /* ... */ }
    });
}

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

لوصف شكل المكتبات التي لم تُكتَب بلغة TypeScript، نحتاج إلى التصريح عن الواجهة البرمجية (API) التي تُصدِّرها المكتبة.

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

الوحدات المُحيطة (Ambient Modules)

تُنجَز معظم المهمّات في Node.js عبر تحميل وحدةٍ واحدة أو أكثر. يُمكن تعريف كل وحدة في ملفّ ‎.d.ts‎ خاص به مع تصريحات تصديرات على المستوى الأعلى (top-level)، لكن من الأفضل كتابتها في ملفّ .d.ts كبيرٍ واحد. للقيام بذلك، نستخدم بنية مشابهة لمجالات الأسماء المُحيطة (ambient namespaces)، لكنّنا نستخدم الكلمة المفتاحية ‎module‎ واسم الوحدة الموجود داخل علامتي تنصيص ("") الذي سيتوفَّر لاستيراده في وقت آخر. على سبيل المثال:

(جزء مُبسَّط من الملفّ node.d.ts)

declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export var sep: string;
}

يُمكننا الآن إحالة الملف ‎node.d.ts‎ بالجملة ‎/// <reference> node.d.ts‎ وبعدها تحميل الوحدات باستخدام ‎import url = require("url");‎ أو ‎import * as URL from "url"‎:

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

الوحدات المحيطة بشكل مختصر

إن لم ترغب بكتابة التصريحات قبل استخدام وحدةٍ ما، فيُمكنك استخدام تصريح مختصر:

(الملفّ ‎declarations.d.ts‎)

declare module "hot-new-module";

ستكون جميع الاستيرادات من وحدة مختصرة ذات النوع ‎any‎:

import x, {y} from "hot-new-module";
x(y);

تصريحات الوحدات بأحرف البدل (Wildcard module declarations)

تسمح بعض محمّلات الوحدات (مثل SystemJS وAMD) باستيراد محتوى مغايرٍ لشيفرة JavaScript (مثل محتوًى نصيّ أو محتوى JSON). وتُستَخدم عادةً سابقات (prefix) أو لاحقات (suffix) للإشارة إلى آلية عمل التحميل الخاصّة. يُمكن استخدام تصريحات الوحدات بأحرف البدل لتغطية هذه الحالات.

declare module "*!text" {
    const content: string;
    export default content;
}

// والبعض يستخدمها بالعكس
declare module "json!*" {
    const value: any;
    export default value;
}

يُمكنك الآن استيراد ما يُوافِق النمط ‎"‎*‎!‎text‎"‎‎ أو النمط ‎"json‎!‎*‎"‎‎:

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

وحدات UMD

صُمِّمَت بعض المكتبات لتُستخدَم مع عدّة محمِّلات وحدات، أو دون تحميل الوحدات (المتغيّرات العامّة [global variables]). وهذه الوحدات معروفة بوحدات UMD. يُمكن الوصول إلى هذه المكتبات عبر استيرادها أو عبر متغيّر عام. مثلًا:

(الملفّ ‎math-lib.d.ts‎)

export function isPrime(x: number): boolean;
export as namespace mathLib;

يُمكن بعد ذلك استخدام المكتبة كاستيرادٍ داخل الوحدات:

import { isPrime } from "math-lib";
isPrime(2);

mathLib.isPrime(2); // خطأ، لا يمكن استخدام التعريف العام داخل الوحدة

يُمكن استخدامها كذلك كمتغيّرٍ عام، لكن داخل سكربت فقط. (السكربت هو أي ملفٍّ لا يحتوي على أي استيرادات ولا تصديرات):

mathLib.isPrime(2);

إرشادات هيكلة الوحدات

قرِّب تصديراتك إلى المستوى الأعلى (top-level) ما أمكن

يجب على مستخدمي وحدتِك أن يصلوا إلى ما تُصدِّره مُباشرةً ما أمكن. إضافةُ الكثير من مستويات التداخل (levels of nesting) قد يُعقِّد الأمور، لذا فكّر جيّدًا قبل هيكلة الشيفرة.

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

التوابع الساكنة على صنفٍ مصدَّر تُشكّل مشكلةً مشابهة، الصنف ذاته يُضيف طبقةً غير مباشرة. إلّا إذا زاد ذلك من التعبير وَوَضَّح المُرادَ من الصنف بشكل يُساعد مستخدم الوحدة، وإذا لم يكن الغرض من الصنف توضيحيًّا، فمن الأفضل تصدير دالةٍ مساعدة (helper function) عوضًا عن ذلك.

إذا صدَّرت صنفًا وحدًا فقط أو دالّةً واحدة فقط، فاستخدم ‎export default

وكما يُقلّلُ التصديرُ في أعلى الوحدة أو أقربَ من احتكاك المستخدمين مع الوحدة ومنحهم القدرة على استخدام الكائنات مباشرةً، فكذلك يفعلُ التصدير الافتراضي عبر جملة ‎export default‎. إذ يُسهِّل ذلك من الاستيراد واستخدام الاستيرادات، على سبيل المثال:

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

export default class SomeType {
  constructor() { ... }
}

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

export default function getThing() { return "thing"; }

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

import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());

هذا أفضل لمستخدمي الوحدة. إذ يُمكنهم تسميّة النوع الذي عرّفته في الوحدة كما يشاؤون (‎t‎ في هذه الحالة) ولا يحتاجون إلى إضافة أيّة نقاطٍ إضافيّة للوصول إلى الكائنات في الوحدة.

إذا صدّرت عدّة كائنات، فضعها جميعًا على المستوى الأعلى

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

export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

وعند الاستيراد في المقابل، وضّح الأسماء المُستوردَة بصراحة:

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

import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();

استعمل نمط الاستيراد بمجال الأسماء (namespace import pattern) إذا كنت تستورد عددًا كبيرًا من الكائنات

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

export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }

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

import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();

أعد التصدير (Re-export) لتوسيع الوحدة

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

خذ على سبيل المثال تطبيق آلة حاسبة بسيطة في الوحدة ‎Calculator.ts‎. تُصدِّر الوحدة دالّةً مُساعدةً كذلك لاختبار وظائف الآلة الحاسبة عبر تمرير قائمة من المدخلات النّصيّة وكتابة النتيجة في الأخير.

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

export class Calculator {
    private current = 0;
    private memory = 0;
    private operator: string;

    protected processDigit(digit: string, currentValue: number) {
        if (digit >= "0" && digit <= "9") {
            return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
        }
    }

    protected processOperator(operator: string) {
        if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
            return operator;
        }
    }

    protected evaluateOperator(operator: string, left: number, right: number): number {
        switch (this.operator) {
            case "+": return left + right;
            case "-": return left - right;
            case "*": return left * right;
            case "/": return left / right;
        }
    }

    private evaluate() {
        if (this.operator) {
            this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
        }
        else {
            this.memory = this.current;
        }
        this.current = 0;
    }

    public handleChar(char: string) {
        if (char === "=") {
            this.evaluate();
            return;
        }
        else {
            let value = this.processDigit(char, this.current);
            if (value !== undefined) {
                this.current = value;
                return;
            }
            else {
                let value = this.processOperator(char);
                if (value !== undefined) {
                    this.evaluate();
                    this.operator = value;
                    return;
                }
            }
        }
        throw new Error(`Unsupported input: '${char}'`);
    }

    public getResult() {
        return this.memory;
    }
}

export function test(c: Calculator, input: string) {
    for (let i = 0; i < input.length; i++) {
        c.handleChar(input[i]);
    }

    console.log(`result of '${input}' is '${c.getResult()}'`);
}

هذا اختبار بسيط للآلة الحاسبة باستخدام الدالة ‎test‎ المصدَّرة: (الملفّ ‎TestCalculator.ts‎)

import { Calculator, test } from "./Calculator";


let c = new Calculator();
test(c, "1+2*33/11="); // 9

لنُوسِّع الوحدة الآن لتضيف دعمًا للأعداد ذوات الأساسات غير الأساس 10، لنُنشئ الملفّ ‎ProgrammerCalculator.ts‎:

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

import { Calculator } from "./Calculator";

class ProgrammerCalculator extends Calculator {
    static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];

    constructor(public base: number) {
        super();
        const maxBase = ProgrammerCalculator.digits.length;
        if (base <= 0 || base > maxBase) {
            throw new Error(`base has to be within 0 to ${maxBase} inclusive.`);
        }
    }

    protected processDigit(digit: string, currentValue: number) {
        if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
            return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
        }
    }
}

// صدّر الآلة الحاسبة الموسَّعة جديدًا بالاسم
// Calculator
export { ProgrammerCalculator as Calculator };

// صدّر الدالة المساعدة كذلك
export { test } from "./Calculator";

تُصدِّر الوحدة الجديدة ‎ProgrammerCalculator‎ شكلَ واجهة برمجيّة (API) مشابهٍ لشكل الوحدة ‎Calculator‎ الأصليّة، لكنّها لا تُنمِّي أيّة كائنات في الوحدة الأصليّة. هذا اختبار للصنف ‎ProgrammerCalculator‎:

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

import { Calculator, test } from "./ProgrammerCalculator";

let c = new Calculator(2);

test(c, "001+010="); // 3

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

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

على الجانب التنظيميّ، مجالات الأسماء مفيدة لجمع الكائنات التي تتعلّق ببعضها البعض منطقيًّا ولجمع الأنواع ذات العلاقة في المجال العام. على سبيل المثال، جُمِعَت في لغة C#‎ جميع أنواع المجاميع (collection types) في ‎System.Collections‎. يوفّر تنظيم الأنواع في مجالات أسماءٍ تسلسليّة (hierarchical namespaces) تجربةَ استكشاف جيّدةً لمستخدمي هذه الأنواع (أي أنّ استكشاف الأنواع الموجودة سيسهل على المبرمج). أمّا الوحدات على الجانب الآخر، فهي موجودة أصلًا في نظام ملفّات (file system) ضرورةً. وسيتوجّب علينا البحث عنها عبر مسارها (path) وأسماء ملفّاتها (filename)، لذا فالتنظيم المنطقيّ المُخطَّط موجود ويُمكننا استخدامه بسهولة. ويُمكننا مثلًا امتلاك مجلّدٍ في المسار ‎‎/collections/generic/‎‎ مع قائمة وحدات داخله.

مجالات الأسماء مهمّةٌ كذلك لتجنّب اصطدامات التّسميّة (naming collisions) في المجال العامّ. مثلًا، قد تملك النّوعَ ‎My.Application.Customer.AddForm‎ والنوع ‎My.Application.Order.AddForm‎ (نوعان بنفس الاسم، لكن في مجالي أسماء مختلفة). لكن هذا ليس مشكلة عند العمل مع الوحدات. إذ لا يوجد أي سببٍ لوجود كائنين بنفس الاسم داخل نفس الوحدة. وعلى جانب مستخدم الوحدة، فمستخدم الوحدة هو الذي يختار الاسم الذي يُريد استخدامه للإشارة إلى الوحدة، لذا فصراعات التسميّة بالمصادفة مستحيلة.

للاستزادة، انظر الوحدات ومجالات الأسماء.

إشارات تحذيريّة

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

إذا تحقّقت أي من هذه الشروط في ملفّاتك فأعد النظر فيها:

  • ملفٌ التصريح الوحيد الموجود على المستوى الأعلى فيه هو ‎export namespace Foo { ... }‎ (احذف ‎Foo‎ وانقل كلّ شيء مستوًى واحدًا للأعلى).
  • ملفٌ فيه تصدير ‎export class‎ واحد فقط، أو تصدير ‎export function‎ واحد فقط لا غير (استعمل ‎export default‎).
  • ملفّات متعدّدة تحتوي على نفس التصدير ‎export namespace Foo {‎ على المستوى الأعلى (لن تُمزَج هذه التصديرات إلى تصدير ‎Foo‎ واحد).

مصادر