4. Модули

Модули

Код TypeScript может быть организован в модули разными способами. Рассмотрим внутренние и внешние модули, для чего они нужны и как их использовать.

Первые шаги

Рассмотрим на примере несколько простых валидаторов строк, которые могут использоваться при проверке пользовательского ввода на веб-форме или при проверке данных из файла.
Валидаторы расположены в одном файле
interface StringValidator {
    isAcceptable(s: string): boolean;
}

var lettersRegexp = /^[A-Za-z]+$/;
var 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);
    }
}

// Тестовые строки
var strings = ['Hello', '98052', '101'];
// Использование валидаторов
var validators: { [s: string]: StringValidator; } = {};
validators['ZIP code'] = new ZipCodeValidator();
validators['Letters only'] = new LettersOnlyValidator();
// Демонстрация как каждая строка проходит каждый валидатор
strings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' успешно ' : ' неуспешно ') + name);
    }
});

Добавление модульности

Если валидаторов будет очень много, для удобства лучше будет каким-то образом организовать код. Вместо того, чтобы вставлять большой количество имен в глобальное пространство имен объединим объекты в модуль.

Добавим все валидаторы в модуль Validation. Для того, чтобы интерфейсы и классы были видны извне, обозначим их словом export. Переменные lettersRegexp и numberRegexp не экспортируемые и будут недоступны извне модуля. Теперь необходимо указывать имя типа, когда модуль используется извне, например, Validation.LettersOnlyValidator.
Валидаторы, объединенные в модуль
module Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    var lettersRegexp = /^[A-Za-z]+$/;
    var 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);
        }
    }
}

// Тестовые строки
var strings = ['Hello', '98052', '101'];
// Использование валидаторов
var validators: { [s: string]: Validation.StringValidator; } = {};
validators['ZIP code'] = new Validation.ZipCodeValidator();
validators['Letters only'] = new Validation.LettersOnlyValidator();
// Демонстрация как каждая строка проходит каждый валидатор
strings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' успешно ' : ' неуспешно ') + name);
    }
});

Разделение на файлы

Когда приложение растет, код можно разбить на файлы, для большего удобства.
Разобьем модуль валидации на несколько файлов. Даже когда используются отдельные файлы, они могут представлять из себя один модуль так, как будто они в одном файле. Т.к. используется несколько файлов необходимо добавить ссылку /// <reference path="FileName.ts" />, чтобы сообщить компилятору, что используется несколько файлов. Тестовый код остается неизменным.

Модуль в нескольких файлах

Validation.ts
module Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}
LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
module Validation {
    var lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}
ZipCodeValidator.ts
/// <reference path="Validation.ts" />
module Validation {
    var 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" />

// Тестовые строки
var strings = ['Hello', '98052', '101'];
// Использование валидаторов
var validators: { [s: string]: Validation.StringValidator; } = {};
validators['ZIP code'] = new Validation.ZipCodeValidator();
validators['Letters only'] = new Validation.LettersOnlyValidator();
// Демонстрация как каждая строка проходит каждый валидаторstrings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
    }
});

Для объединения всех TypeScript файлов в один JavaScript файл необходимо указать компилятору параметр --out:
tsc --out sample.js Test.ts

Компилятор автоматически сформирует результирующий файл в зависимости от ссылок reference в исходных файлах. Однако можно указать исходные файлы вручную:
tsc --out sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

Если формируются отдельные 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" />

Внешние модули

TypeScript также имеет концепцию внешних модулей. Внешние модули используются в двух случаях: node.js и require.js. Приложениям, не использующим node.js или require.js, нет необходимости использовать внешние модули и они могут быть лучше организованы, использую внутренние модули, описанные ранее.

Во внешних модулях отношения между файлами определяются импортом и экспортом на уровне файлов. В TypeScript файл содержащий высокоуровневый import или export считается внешним модулем.

Изменим наш пример для использования внешних модулей. Мы больше не используем ключевое слово module - файлы сами организуют модуль и идентифицируются по их именам.

Тег reference заменяется на import, определяющий связи между модулями. Тег import имеет две части: имя будущего модуля конструкцию require, указывающую путь к модулю:

import someMod = require('someModule');

Для указания, какие объекты будут видны вне модуля, используется ключевое слово export, так же, как export определял видимые объекты во внутренних модулях.

При компиляции необходимо указать модуль в командной строке. Для node.js: --module commonjs; для require.js: --module amd. Например:
tsc --module commonjs Test.ts

При компиляции каждый внешний модуль станет отдельным .js файлом.
Validation.ts
export interface StringValidator {
    isAcceptable(s: string): boolean;
}
LettersOnlyValidator.ts
import validation = require('./Validation');
var lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}
ZipCodeValidator.ts
import validation = require('./Validation');
var numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
Test.ts
import validation = require('./Validation');
import zip = require('./ZipCodeValidator');
import letters = require('./LettersOnlyValidator');

// Тестовые строки
var strings = ['Hello', '98052', '101'];
// Использование валидаторов
var validators: { [s: string]: validation.StringValidator; } = {};
validators['ZIP code'] = new zip.ZipCodeValidator();
validators['Letters only'] = new letters.LettersOnlyValidator();
// Демонстрация как каждая строка проходит каждый валидаторstrings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
    }
});

Генерация кода для внешних модулей

В зависимости от указанного при компиляции модуля, компилятор генерирует код, соответствующий системе загрузки модулей или для node.js (commonjs) или для require.js (AMD). Как это работает показано на следующем примере.
SimpleModule.ts
import m = require('mod');
export var t = m.something + 1;
AMD / RequireJS SimpleModule.js:
define(["require", "exports", 'mod'], function(require, exports, m) {
    exports.t = m.something + 1;
});
CommonJS / Node SimpleModule.js:
var m = require('mod');
exports.t = m.something + 1;

Export =

В предыдущем примере каждый модуль экспортировал только один объект. 

Конструкция export = указывает объект, экспортируемый из модуля. Это может быть класс, интерфейс, модуль, функция или перечисление. При импорте необязательно указывать имя экспортируемего объекта напрямую.

Ниже улучшена реализация Validator для экспорта только одного объекта из каждого модуля используя конструкцию export =.Это упрощает код – вместо 'zip.ZipCodeValidator' можно использовать 'zipValidator'.
Validation.ts
export interface StringValidator {
    isAcceptable(s: string): boolean;
}
LettersOnlyValidator.ts
import validation = require('./Validation');
var lettersRegexp = /^[A-Za-z]+$/;
class LettersOnlyValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}
export = LettersOnlyValidator;
ZipCodeValidator.ts
import validation = require('./Validation');
var numberRegexp = /^[0-9]+$/;
class ZipCodeValidator implements validation.StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;
Test.ts
import validation = require('./Validation');
import zipValidator = require('./ZipCodeValidator');
import lettersValidator = require('./LettersOnlyValidator');

// Some samples to try
var strings = ['Hello', '98052', '101'];
// Validators to use
var validators: { [s: string]: validation.StringValidator; } = {};
validators['ZIP code'] = new zipValidator();
validators['Letters only'] = new lettersValidator();
// Show whether each string passed each validator
strings.forEach(s => {
    for (var name in validators) {
        console.log('"' + s + '" ' + (validators[name].isAcceptable(s) ? ' matches ' : ' does not match ') + name);
    }
});

Алиасы

Другой способ упростить работу с модулями, это использовать import q = x.y.z чтобы создать короткие ссылки на часто используемые объекты. Не надо путать с конструкцией import x = require('name') используемой для загрузки внешних модулей. Данный вид импорта (чаще называемый алиасами) можно использовать для любого идентификатора, включая объекты во внешних модулях.
Использование алиасов
module Shapes {
    export module Polygons {
        export class Triangle { }
        export class Square { }
    }
}

import polygons = Shapes.Polygons;
var sq = new polygons.Square(); // Same as 'new Shapes.Polygons.Square()'

Обратите внимание, что не используется ключевое слово require, а имя имортируемого объекта указывается напрямую. Это похоже на использование var, но также работает на типах и пространствах имен. Важно иметь в виду, что import это отдельная ссылка на объект, и изменение переменной алиаса не будет отражено на оригинальной переменной.

Выборочная загрузка модулей и прочие варианты загрузки

Иногда необходимо загрузить модуль только при определенных условиях. Для этого в TypeScript можно использовать схему указанную выше, так и другие варианты загрузки, чтобы напрямую вызывать загрузку модулей.

Смысл в том, что import id = require('...') дает доступ к типам внешнего модуля. Загрузка модулей происходит динамически, как показано ниже. Модуль загружается по требованию. Важно чтобы алиасы определенные через import использовались только в качестве типа.
Для безопасности типов необходимо использовать ключевое слово typeof.
Dynamic Module Loading in node.js
declare var require;
import Zip = require('./ZipCodeValidator');
if (needZipValidation) {
    var x: typeof Zip = require('./ZipCodeValidator');
    if (x.isAcceptable('.....')) { /* ... */ }
}
Sample: Dynamic Module Loading in require.js
declare var require;
import Zip = require('./ZipCodeValidator');
if (needZipValidation) {
    require(['./ZipCodeValidator'], (x: typeof Zip) => {
        if (x.isAcceptable('...')) { /* ... */ }
    });
}

Работа с другими JavaScript библиотеками

Для использования внешней библиотеки необходимо описать её API. И модули - это хороший способ сделать это. Это похоже на .h файлы в C/C++. Рассмотрим на примере внутренних и внешних модулей.

Пример внутреннего модуля

Популярная библиотека D3 определеяет её функциональность в глобальном объекте 'D3'. Так как библиотека загружается через тег script (а не через загрузчик модулей), её описание реализуется через внутренний модуль. Чтобы компилятор TypeScript увидел её функциональность используется внутренний модуль. Например:
D3.d.ts (simplified excerpt)
declare module 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;

Пример внешнего модуля

В node.js большинство задач решается загрузкой одного или нескольких модулей. И более удобно описать их как один большой файл .d.ts. Для этого мы указываем имя модуля в кавычках, и далее он будет доступен для импорта. Например:
node.d.ts (simplified excerpt)
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;
}

Теперь можно сослаться на модуль /// <reference> node.d.ts и затем загрузить его используя import url = require('url');.

///<reference path="node.d.ts"/>
import url = require("url");
var myUrl = url.parse("http://www.typescriptlang.org");

Ловушки модулей

Рассмотрим как избегать типичных ошибок при использовании внутренних и внешних модулей.

/// <reference> на внешний модуль

Распространенной ошибкой является попытка использовать синтаксиса /// <ссылка> для обозначения внешнего файла модуля, а не с помощью импорта. Чтобы понять различие, мы должны сначала понять три пути того, как компилятор может найти информацию о типе внешнего модуля.

Первый состоит в поиске ts-файла с именем при import x = require(...);. Этот файл должен иметь реализации высокоуровневого импорта или экспорта.

Второй аналогичен первому, но находится не файл с реализацией функциональности, а файл ее описания. 

И наконец третий - это поиска файла описания внешнего модуля, где объявлен модуль через имя в кавычках.
myModules.d.ts
// In a .d.ts file or .ts file that is not an external module:
declare module "SomeModule" {
    export function fn(): string;
}
myOtherModule.ts
/// <reference path="myModules.d.ts" />
import m = require("SomeModule");

Тег reference позволяет найти фал описания внешнего модуля. 

Излишнее использование пространств имен

При преобразовании программы из внутренних модулей во внешние, легко может получиться такой файл:
shapes.ts
export module Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
}

Здесь высокоуровневый модуль Shapes содержит Triangle и Square. И это создает неудобство использования модуля:
shapeConsumer.ts
import shapes = require('./shapes');
var t = new shapes.Shapes.Triangle(); // shapes.Shapes?

Ключевая особенность внешних модулей в том, что два различных модуля никогда не используют имена в одной и той же области видимости. Потому что потребитель внешнего модуля решает, какое имя назначить внешнему модулю.

Пример:
shapes.ts
export class Triangle { /* ... */ }
export class Square { /* ... */ }
shapeConsumer.ts
import shapes = require('./shapes');
var t = new shapes.Triangle(); 

Комментариев нет :

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.