Spec-Zone.ru › TypeScript 5.1

Разрешение модулей

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

Разрешение модулей — это процесс, который использует компилятор, чтобы определить, к чему относится импорт. Рассмотрим инструкцию импорта, такую как import { a } from "moduleA"; чтобы проверить любое использование a, компилятору необходимо точно знать, что оно представляет, и ему потребуется проверить его определение moduleA.

На этом этапе компилятор спросит: «Какова структура moduleA?» Хотя это звучит просто, moduleA может быть определено в одном из ваших собственных файлов .ts/.tsx, или в .d.ts, от которого зависит ваш код.

Сначала компилятор попытается найти файл, который представляет импортированный модуль. Для этого компилятор использует одну из двух стратегий: Классическая или Node. Эти стратегии сообщают компилятору, где искать moduleA.

Если это не сработало, и имя модуля не относительно (а в случае "moduleA", оно таковым является), тогда компилятор попытается найти объявление модуля ambient module declaration. Далее мы рассмотрим импорты, которые не относительны.

Наконец, если компилятор не смог разрешить модуль, он выведет ошибку. В этом случае сообщение об ошибке будет примерно таким: error TS2307: Cannot find module 'moduleA'.

Относительные и неотносительные импорты модулей

Импорты модулей разрешаются по-разному в зависимости от того, является ли ссылка на модуль относительной или неотносительной.

Относительный импорт — это импорт, который начинается с /, ./ или ../. Вот некоторые примеры:

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

Любой другой импорт считается неотносительным. Вот некоторые примеры:

  • import * as $ from "jquery";
  • import { Component } from "@angular/core";

Относительный импорт разрешается относительно файла, импортирующего его, и не может ссылаться на объявление модуля ambient. Вы должны использовать относительные импорты для собственных модулей, местоположение которых относительно гарантировано во время выполнения.

Неотносительный импорт может быть разрешен относительно baseUrl или с помощью сопоставления путей, о котором мы поговорим ниже. Они также могут ссылаться на объявления модулей ambient. Используйте неотносительные пути при импорте любых внешних зависимостей.

Стратегии разрешения модулей

Существуют две возможные стратегии разрешения модулей: Node и Классическая. Вы можете использовать опцию moduleResolution, чтобы указать стратегию разрешения модулей. Если не указано, по умолчанию используется стратегия Node для --module commonjs, и Классическая в противном случае (в том числе, когда module установлено в amd, system, umd, es2015, esnext, и т.д.).

Примечание: стратегия разрешения модулей Node — наиболее часто используемая в сообществе TypeScript и рекомендуется для большинства проектов. Если у вас возникают проблемы с разрешением import и export в TypeScript, попробуйте установить moduleResolution: "node", чтобы проверить, решит ли это проблему.

Классическая

Эта стратегия ранее была по умолчанию в TypeScript. В наши дни она в основном используется для обратной совместимости.

Относительный импорт будет разрешен относительно импортирующего файла. Таким образом, import { b } from "./moduleB" в файле исходного кода /root/src/folder/A.ts приведет к следующим поискам:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

Однако для импортов модулей, которые не являются относительными, компилятор проходит вверх по дереву каталогов, начиная с каталога, содержащего импортирующий файл, пытаясь найти соответствующий файл определения.

Например:

Неотносительный импорт в moduleB такого как import { b } from "moduleB", в файле исходного кода /root/src/folder/A.ts, приведет к попытке найти "moduleB" в следующих местах:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

Node

Эта стратегия разрешения пытается имитировать механизм разрешения модулей Node.js во время выполнения. Полный алгоритм разрешения модулей Node.js описан в документации по модулям Node.js.

Как Node.js разрешает модули

Чтобы понять шаги, которые выполнит компилятор TS, важно прояснить работу модулей Node.js. Традиционно импорт в Node.js выполняется вызовом функции с именем require. Поведение Node.js будет отличаться в зависимости от того, передается ли require относительный путь или неотносительный путь.

Относительные пути довольно просты. В качестве примера рассмотрим файл, расположенный по адресу /root/src/moduleA.js, который содержит импорт var x = require("./moduleB"); Node.js разрешает этот импорт в следующем порядке:

  1. Спрашивает файл с именем /root/src/moduleB.js, существует ли он.

  2. Спрашивает папку /root/src/moduleB содержит ли она файл с именем package.json, который определяет модуль "main". В нашем примере, если Node.js нашел файл /root/src/moduleB/package.json содержащий { "main": "lib/mainModule.js" }, то Node.js обратится к /root/src/moduleB/lib/mainModule.js.

  3. Спрашивает папку /root/src/moduleB содержит ли она файл с именем index.js. Этот файл неявно считается «главным» модулем папки.

Дополнительную информацию можно найти в документации Node.js по файловым модулям и папкам как модулям.

Однако разрешение для неотносительного имени модуля выполняется по-другому. Node будет искать ваши модули в специальных папках с именем node_modules. Папка node_modules может находиться на одном уровне с текущим файлом или выше в цепочке каталогов. Node будет подниматься по цепочке каталогов, проверяя каждую node_modules до тех пор, пока не найдет модуль, который вы пытались загрузить.

Продолжая наш пример выше, предположим, что /root/src/moduleA.js вместо этого использовал неотносительный путь и имел импорт var x = require("moduleB");. Node затем попытался бы разрешить moduleB в каждом из расположений до тех пор, пока одно не сработало.

  1. /root/src/node_modules/moduleB.js
  2. /root/src/node_modules/moduleB/package.json (если он указывает свойство "main")
  3. /root/src/node_modules/moduleB/index.js
  4. /root/node_modules/moduleB.js
  5. /root/node_modules/moduleB/package.json (если он указывает свойство "main")
  6. /root/node_modules/moduleB/index.js
  7. /node_modules/moduleB.js
  8. /node_modules/moduleB/package.json (если он указывает свойство "main")
  9. /node_modules/moduleB/index.js

Обратите внимание, что Node.js переходил на один уровень вверх в шагах (4) и (7).

Подробнее об этом процессе можно прочитать в документации Node.js по загрузке модулей из node_modules.

Как TypeScript разрешает модули

TypeScript будет имитировать стратегию разрешения Node.js во время выполнения, чтобы найти файлы определения модулей во время компиляции. Для этого TypeScript накладывает расширения файлов исходного кода TypeScript (.ts, .tsx, и .d.ts ) поверх логики разрешения Node. TypeScript также будет использовать поле в package.json с именем types, чтобы отразить назначение "main" — компилятор будет использовать его, чтобы найти «главный» файл определения, который следует проконсультироваться.

Например, инструкция импорта import { b } from "./moduleB" в /root/src/moduleA.ts приведет к попытке найти "./moduleB" в следующих местах:

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (если он указывает свойство types)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

Вспомните, что Node.js искал файл с именем moduleB.js, затем соответствующую папку package.json, и затем index.js.

Аналогично, неотносительный импорт будет следовать логике разрешения Node.js, сначала выполняя поиск файла, а затем соответствующей папки. Таким образом, import { b } from "moduleB" в файле исходного кода /root/src/moduleA.ts приведет к следующим поискам:

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json (если он указывает свойство types)
  5. /root/src/node_modules/@types/moduleB.d.ts
  6. /root/src/node_modules/moduleB/index.ts
  7. /root/src/node_modules/moduleB/index.tsx
  8. /root/src/node_modules/moduleB/index.d.ts
  9. /root/node_modules/moduleB.ts
  10. /root/node_modules/moduleB.tsx
  11. /root/node_modules/moduleB.d.ts
  12. /root/node_modules/moduleB/package.json (если он указывает свойство types)
  13. /root/node_modules/@types/moduleB.d.ts
  14. /root/node_modules/moduleB/index.ts
  15. /root/node_modules/moduleB/index.tsx
  16. /root/node_modules/moduleB/index.d.ts
  17. /node_modules/moduleB.ts
  18. /node_modules/moduleB.tsx
  19. /node_modules/moduleB.d.ts
  20. /node_modules/moduleB/package.json (если он указывает свойство types)
  21. /node_modules/@types/moduleB.d.ts
  22. /node_modules/moduleB/index.ts
  23. /node_modules/moduleB/index.tsx
  24. /node_modules/moduleB/index.d.ts

Не пугайтесь большого количества шагов — TypeScript всё ещё переходит на один уровень вверх дважды в шагах (9) и (17). Это не сложнее, чем то, что делает сам Node.js.

Дополнительные флаги разрешения модулей

Макет проекта иногда не совпадает с макетом выходных данных. Обычно набор шагов сборки приводит к генерации окончательных выходных данных. Они включают компиляцию .ts файлов в .js, и копирование зависимостей из разных источников в одно место вывода. В результате модули во время выполнения могут иметь другие имена, чем исходные файлы, содержащие их определения. Или пути к модулям в окончательных выходных данных могут не совпадать с соответствующими путями к исходным файлам во время компиляции.

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

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

Базовый URL

Использование baseUrl является распространенной практикой в приложениях, использующих загрузчики модулей AMD, где модули «развернуты» в одну папку во время выполнения. Источники этих модулей могут находиться в разных каталогах, но скрипт сборки соберет их вместе.

Установка baseUrl сообщает компилятору, где найти модули. Все импорты модулей с именами, не являющимися относительными, предполагаются относительными к baseUrl.

Значение baseUrl определяется как:

  • значение аргумента командной строки baseUrl (если указанный путь является относительным, он вычисляется на основе текущего каталога)
  • значение свойства baseUrl в tsconfig.json (если указанный путь является относительным, он вычисляется на основе расположения tsconfig.json)

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

Дополнительную документацию по baseUrl можно найти в документации RequireJS и SystemJS.

Сопоставление путей

Иногда модули не находятся непосредственно под baseUrl. Например, импорт модуля "jquery" будет переведен во время выполнения в "node_modules/jquery/dist/jquery.slim.min.js". Загрузчики используют конфигурацию сопоставления для сопоставления имен модулей с файлами во время выполнения, см. документацию RequireJs и документацию SystemJS.

Компилятор TypeScript поддерживает объявление таких сопоставлений, используя свойство paths в файлах tsconfig.json. Вот пример того, как указать свойство paths для jquery.

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
    }
  }
}

Обратите внимание, что paths разрешаются относительно baseUrl. При установке baseUrl на значение, отличное от ".", т. е. каталога tsconfig.json, сопоставления необходимо изменить соответствующим образом. Скажем, вы установили "baseUrl": "./src" в приведенном выше примере, тогда jquery должно быть сопоставлено с "../node_modules/jquery/dist/jquery".

Использование paths также позволяет выполнять более сложные сопоставления, включая несколько резервных мест. Рассмотрим конфигурацию проекта, где некоторые модули доступны в одном месте, а остальные — в другом. Шаг сборки объединит их все в одном месте. Макет проекта может выглядеть так:

projectRoot
├── folder1
│   ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│   └── file2.ts
├── generated
│   ├── folder1
│   └── folder2
│       └── file3.ts
└── tsconfig.json

Соответствующий tsconfig.json будет выглядеть так:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": ["*", "generated/*"]
    }
  }
}

Это сообщает компилятору для любого импорта модуля, соответствующего шаблону "*" (т. е. для всех значений), искать в двух местах:

  1. "*": что означает то же самое имя без изменений, так что сопоставьте <moduleName> => <baseUrl>/<moduleName>
  2. "generated/*" означающее имя модуля с добавленным префиксом «generated», так что сопоставьте <moduleName> => <baseUrl>/generated/<moduleName>

Следуя этой логике, компилятор попытается разрешить два импорта следующим образом:

import ‘folder1/file2’:

  1. шаблон ’*’ соответствует, и подстановочный символ захватывает все имя модуля
  2. сначала попробовать первую подстановку в списке: ’*’ -> folder1/file2
  3. результат подстановки — имя, не являющееся относительным — объединить его с baseUrl -> projectRoot/folder1/file2.ts.
  4. Файл существует. Закончено.

import ‘folder2/file3’:

  1. шаблон ’*’ соответствует, и подстановочный символ захватывает все имя модуля
  2. сначала попробовать первую подстановку в списке: ’*’ -> folder2/file3
  3. результат подстановки — имя, не являющееся относительным — объединить его с baseUrl -> projectRoot/folder2/file3.ts.
  4. Файл не существует, перейти ко второй подстановке
  5. вторая подстановка ‘generated/*’ -> generated/folder2/file3
  6. результат подстановки — имя, не являющееся относительным — объединить его с baseUrl -> projectRoot/generated/folder2/file3.ts.
  7. Файл существует. Закончено.

Виртуальные каталоги с rootDirs

Иногда исходные файлы проекта из нескольких каталогов во время компиляции объединяются для генерации одного выходного каталога. Это можно рассматривать как набор исходных каталогов, создающих «виртуальный» каталог.

Используя rootDirs, вы можете сообщить компилятору о корнях, составляющих этот «виртуальный» каталог; и таким образом компилятор может разрешать относительные импорты модулей внутри этих «виртуальных» каталогов как если бы они были объединены в один каталог.

Например, рассмотрим следующую структуру проекта:

 src
 └── views
     └── view1.ts (imports './template1')
     └── view2.ts

 generated
 └── templates
         └── views
             └── template1.ts (imports './view2')

Файлы в src/views — это пользовательский код для некоторых элементов управления интерфейсом пользователя. Файлы в generated/templates — это код привязки шаблонов пользовательского интерфейса, автоматически сгенерированный генератором шаблонов в рамках сборки. Шаг сборки скопирует файлы в /src/views и /generated/templates/views в один и тот же каталог в выводе. Во время выполнения виджет может ожидать, что его шаблон существует рядом с ним, и поэтому должен импортировать его с помощью относительного имени, как "./template".

Чтобы указать эту связь компилятору, используйте rootDirs. rootDirs задают список корней, содержимое которых ожидается объединить во время выполнения. Итак, следуя нашему примеру, файл tsconfig.json должен выглядеть так:

{
  "compilerOptions": {
    "rootDirs": ["src/views", "generated/templates/views"]
  }
}

Каждый раз, когда компилятор видит относительный импорт модуля в подкаталоге одного из rootDirs, он будет пытаться найти этот импорт в каждой из записей rootDirs.

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

Рассмотрим сценарий локализации, где инструмент сборки автоматически генерирует языковые пакеты, интерполируя специальный маркер пути, например #{locale}, в качестве части относительного пути модуля, например ./#{locale}/messages. В этой гипотетической настройке инструмент перечисляет поддерживаемые языковые версии, сопоставляя абстрактный путь с ./zh/messages, ./de/messages, и так далее.

Предположим, что каждый из этих модулей экспортирует массив строк. Например, ./zh/messages может содержать:

export default ["您好吗", "很高兴认识你"];

Используя rootDirs, мы можем сообщить компилятору об этом сопоставлении и, таким образом, позволить ему безопасно разрешить ./#{locale}/messages, даже если каталог никогда не будет существовать. Например, с помощью следующего tsconfig.json:

{
  "compilerOptions": {
    "rootDirs": ["src/zh", "src/de", "src/#{locale}"]
  }
}

Теперь компилятор разрешит import messages from './#{locale}/messages' в import messages from './zh/messages' для целей инструментария, что позволит разрабатывать без учета языка без ущерба для поддержки на этапе проектирования.

Отслеживание разрешения модулей

Как обсуждалось ранее, компилятор может обращаться к файлам за пределами текущей папки при разрешении модуля. Это может быть сложно при диагностике причин, по которым модуль не разрешен или разрешен неправильно. Включение отслеживания разрешения модуля компилятора с помощью traceResolution обеспечивает понимание того, что произошло во время процесса разрешения модуля.

Допустим, у нас есть примерное приложение, которое использует модуль typescript. app.ts имеет импорт, подобный import * as ts from "typescript".

│   tsconfig.json
├───node_modules
│   └───typescript
│       └───lib
│               typescript.d.ts
└───src
        app.ts

Вызов компилятора с traceResolution

tsc --traceResolution

Результатом будет вывод, похожий на:

======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

Что следует учитывать

  • Имя и расположение импорта

======== Разрешение модуля ‘typescript’ из ‘src/app.ts’. ========

  • Стратегия, которую использует компилятор

Тип разрешения модуля не указан, используется ‘NodeJs’.

  • Загрузка типов из пакетов npm

В ‘package.json’ есть поле ‘types’ ‘./lib/typescript.d.ts’, которое ссылается на ‘node_modules/typescript/lib/typescript.d.ts’.

  • Конечный результат

======== Имя модуля ‘typescript’ было успешно разрешено до ‘node_modules/typescript/lib/typescript.d.ts’. ========

Использование --noResolve

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

Параметр компилятора noResolve указывает компилятору не добавлять в компиляцию файлы, которые не были переданы в командной строке. Он всё ещё будет пытаться разрешить модуль до файлов, но если файл не указан, он не будет включён.

Например:

app.ts

import * as A from "moduleA"; // OK, 'moduleA' passed on the command-line
import * as B from "moduleB"; // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve

Компиляция app.ts с использованием noResolve должна привести к:

  • Корректной находке moduleA, поскольку он был передан в командной строке.
  • Ошибка при поиске moduleB, так как он не был передан.

Общие вопросы

Почему модуль в списке исключений всё ещё подхватывается компилятором?

tsconfig.json превращает папку в «проект». Без указания каких-либо “exclude” или “files” записей, все файлы в папке, содержащей tsconfig.json и всех её подпапок, включаются в компиляцию. Если вы хотите исключить некоторые файлы, используйте “exclude”, а если хотите указать все файлы вместо того, чтобы позволять компилятору их искать, используйте “files”.

Это была tsconfig.json автоматическая включённость. Это не включает разрешение модулей, как обсуждалось выше. Если компилятор определил файл как цель импорта модуля, он будет включён в компиляцию независимо от того, был ли он исключён на предыдущих этапах.

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

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/module-resolution.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API