Внешний модуль TypeScript предоставляют много возможностей и удобства. Здесь мы обсудим его возможности и некоторые паттерны используемые на практике.
Прежде всего нам нужно прояснить (ужасную) несогласованность систем модулей. Я просто дам вам свою текущую рекомендацию и уберу шум, т.е. скрою некоторые возможные варианты работы.
Из одного и того же TypeScript вы можете генерировать разный код JavaScript в зависимости от опции module
. Вот вещи, которые вы можете игнорировать (я не заинтересован в объяснении устаревших технологий):
- AMD: Не используйте. Использовались только в браузерах.
- SystemJS: Был неплохим вариантом. Заменен ES модулями.
- ES Модули: Еще не готовы.
Теперь это всего лишь варианты генерации JavaScript. Вместо этих опций используйте module:commonjs
То, как вы пишете модули TypeScript, также немного запутанно. Опять же, вот как лучше не делать сегодня:
import foo = require('foo')
. т.е.import/require
. Вместо этого используйте синтаксис модуля ES.
Окей, вместе с тем давайте посмотрим на синтаксис модуля ES.
Резюме: используйте
module:commonjs
и используйте синтаксис модуля ES для импорта / экспорта / создания модулей.
- Экспортировать переменную (или тип) можно просто добавив ключевое слово
export
, например
// файл `foo.ts`
export let someVar = 123;
export type SomeType = {
foo: string;
};
- Экспорт переменной или типа в отдельной
export
инструкции, например
// файл `foo.ts`
let someVar = 123;
type SomeType = {
foo: string;
};
export {
someVar,
SomeType
};
- Экспорт переменной или типа в отдельной
export
инструкции с переименованием, например
// файл `foo.ts`
let someVar = 123;
export { someVar as aDifferentName };
- Импортируйте переменную или тип, используя
import
, например
// файл `bar.ts`
import { someVar, SomeType } from './foo';
- Импортировать переменную или тип, используя
import
с переименованием, например
// файл `bar.ts`
import { someVar as aDifferentName } from './foo';
- Импортируйте все из модуля под новым именем с помощью
import * as
, например
// файл `bar.ts`
import * as foo from './foo';
// вы можете использовать `foo.someVar` и` foo.SomeType` и все остальное, что foo может экспортировать
- Импорт файла для общего доступа с помощью одной только инструкции импорта:
import 'core-js'; // библиотека основных полифиллов
- Реэкспорт всех переменных из другого модуля
export * from './foo';
- Реэкспорт только некоторых переменных из другого модуля
export { someVar } from './foo';
- Реэкспорт только некоторых переменных из другого модуля с переименованием
export { someVar as aDifferentName } from './foo';
Как вы узнаете позже, я не фанат экспорта по умолчанию. Тем не менее есть синтаксис для экспорта и использования экспорта по умолчанию
- Экспорт с использованием
export по умолчанию
- перед переменной (не требуется
let / const / var
) - перед функцией
- перед классом
- перед переменной (не требуется
// какая-то переменная
export default someVar = 123;
// ИЛИ какая-то функция
export default function someFunction() { }
// ИЛИ какой-то класс
export default class SomeClass { }
- Импорт с использованием
import someName from "someModule"
(вы можете назвать импорт как угодно), например
import someLocalNameForThisFile from "../foo";
Я собираюсь предложить использовать
moduleResolution: commonjs
. Эта опция должна быть в вашей конфигурации TypeScript. А эта настройкаmodule:commonjs
подразумевается автоматически.
Есть два разных вида модулей. Различие обусловлено секцией пути в операторе импорта (например, import foo from 'ЭТО_СЕКЦИЯ_ПУТИ'
).
- Модули с относительным путем (где путь начинается с
.
, например./someFile
или../../someFolder/someFile
и т.д.) - Другие модули с динамической подстановкой (например,
'core-js'
или'typestyle'
или'react'
или даже'react/core'
и т.д.)
Основное отличие состоит в том, как модуль находится в файловой системе.
Я буду использовать концептуальный термин место, который я объясню после упоминания шаблона поиска.
Это легко, просто следуй по пути :) например
- если файл
bar.ts
выполняетimport * as foo from './foo';
, то файлfoo
должен существовать в той же папке. - если файл
bar.ts
выполняетimport * as foo from '../foo';
, то файлfoo
должен существовать в папке выше. - если файл
bar.ts
выполняетimport * as foo from '../someFolder/foo';
в папке выше должна быть папкаsomeFolder
с местомfoo
Или любой другой относительный путь, который вы можете придумать :)
Когда путь импорта не относительный, поиск определяется node style resolution. Здесь я приведу только простой пример:
-
У вас есть
import * as foo from 'foo'
, ниже перечислены места, которые проверяются по порядку./node_modules/foo
../node_modules/foo
../../node_modules/foo
- И далее до корня файловой системы
-
У вас есть
import * as foo from 'something/foo'
, ниже перечислены места, которые проверяются по порядку./node_modules/something/foo
../node_modules/something/foo
../../node_modules/something/foo
- И далее до корня файловой системы
Когда я говорю проверенные места, я имею в виду, что в этом месте проверяются следующие вещи. например для места foo
:
- Если место представляет собой файл, например,
foo.ts
, завершено! - иначе, если место - это папка, а есть файл
foo/index.ts
, завершено! - в противном случае, если местом является папка и существует
foo/package.json
с указанным и существующим в ключеtypes
файлом, тогда завершено! - в противном случае, если местом является папка и существует package.json с указанным и существующим в ключе
main
файлом, то завершено!
Под файлом я имею в виду .ts
/ .d.ts
и .js
.
И это все. Теперь вы являетесь экспертами по поиску модулей (не маленький подвиг!).
Вы можете объявить модуль глобально для вашего проекта, используя declare module 'somePath'
, и тогда импорт исполнит волшебным образом этот путь
например
// global.d.ts
declare module 'foo' {
// Некоторые объявления переменных
export var bar: number; /*пример*/
}
и потом:
// anyOtherTsFileInYourProject.ts
import * as foo from 'foo';
// TypeScript предполагает (без каки-либо подстановок), что
// foo это {bar:number}
Следующая инструкция:
import foo = require('foo');
на самом деле делает две вещи:
- Импортирует информацию о типах модуля foo.
- Определяет необходимые зависимости в среде выполнения для модуля foo.
Вы можете выбрать, чтобы загружалась только информация о типе и избежать загрузки зависимостей. Прежде чем продолжить, вы можете вспомнить раздел области объявлений этой книги.
Если вы не используете импортированное имя в области объявления переменных, то импорт полностью удаляется из сгенерированного JavaScript. Это лучше всего объяснить на примерах. Как только вы поймете это, мы представим вам варианты использования.
import foo = require('foo');
сгенерирует следующий JavaScript:
Всё в порядке. Пустой файл так как foo не используется.
import foo = require('foo');
var bar: foo;
сгенерирует следующий JavaScript:
var bar;
Это потому, что foo
(или любое из его свойств, например, foo.bas
) никогда не используется в качестве переменной.
import foo = require('foo');
var bar = foo;
сгенерирует следующий JavaScript (при условии использования commonjs параметра конфига):
var foo = require('foo');
var bar = foo;
Это потому что foo
используется как переменная.
Вывод типа должен быть сделан предварительно. Это означает, что если вы хотите использовать какой-либо тип из файла foo
в файле bar
, вам придется сделать следующее:
import foo = require('foo');
var bar: foo.SomeType;
Однако вы можете захотеть загружать файл foo
только во время выполнения при определенных условиях. В таких случаях вы должны использовать импортированное
значение только в описаниях типа, но не в качестве переменной. Это удаляет любой предварительный код зависимостей в среде выполнения, внедренный TypeScript. Затем вручную импортируйте реальный модуль, используя код, необходимый для вашего загрузчика модулей.
В качестве примера рассмотрим следующий код, основанный на commonjs
, где мы загружаем модуль 'foo'
только при вызове определенной функции:
import foo = require('foo');
export function loadFoo() {
// Это отложенная загрузка `foo` и использование исходного модуля *только* в качестве описания типа
var _foo: typeof foo = require('foo');
// Теперь используем `_foo` в качестве переменной вместо `foo`.
}
Подобный пример в amd
(используя requirejs) будет выглядеть так:
import foo = require('foo');
export function loadFoo() {
// Это отложенная загрузка `foo` и использование исходного модуля *только* в качестве описания типа
require(['foo'], (_foo: typeof foo) => {
// Теперь используем `_foo` в качестве переменной вместо `foo`.
});
}
Этот шаблон обычно используется:
- в веб-приложениях, где вы загружаете определенный JavaScript на определенных роутах,
- в nodejs приложениях, где вы загружаете только определенные модули, если это необходимо для ускорения загрузки приложений.
Как и в случае с отложенной загрузкой, некоторые загрузчики модулей (commonjs/node и amd/requirejs) плохо работают с циклическими зависимостями. В таких случаях полезно иметь отложенную загрузку кода в одном направлении и загружать модули предварительно в противоположном направлении.
Иногда вы хотите загрузить файл только для побочного эффекта (например, для регистрации в какой-то библиотеке CodeMirror addons и т.д.). Тем не менее, если вы просто выполните import/require
, транспиленный JavaScript не будет содержать зависимости от модуля, и ваш загрузчик модулей (например, webpack) может полностью игнорировать импорт. В таких случаях вы можете использовать переменную ensureImport
, чтобы гарантировать, что скомпилированный JavaScript принимает зависимость от модуля, например:
import foo = require('./foo');
import bar = require('./bar');
import bas = require('./bas');
const ensureImport: any =
foo
&& bar
&& bas;