시작하며
[모듈화 메커니즘](/articles/모듈-typescript 노트 13/) 을 통해 코드를 여러 모듈 (파일) 로 분할할 수 있습니다. 컴파일 시에는 의존 모듈의 정확한 타입을 알아야 하므로, 먼저 그것을 찾아야 합니다 (모듈 이름에서 모듈 파일 경로로의 매핑 확립)
실제로, TypeScript 에서 하나의 모듈 이름은 하나의.ts/.tsx또는.d.ts파일에 해당할 수 있습니다 (--allowJs 를 켜면.js/.jsx 파일에 해당할 수도 있습니다)
기본적인思路는 다음과 같습니다:
- 먼저 모듈에 해당하는 파일 (
.ts/.tsx) 을 찾는 것을 시도 - 찾지 못했고, 상대 모듈 도입 (
non-relative) 이 아닌 경우, 외부 모듈 선언 (ambient module declaration), 즉d.ts를 찾는 것을 시도 - 그래도 찾지 못하면, 오류
Cannot find module 'ModuleA'.보고
一。상대와 비상대 모듈 도입
상대 모듈 도입 (relative import) 은/, ./ 또는../ 로 시작합니다. 예를 들어:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
주의, 상대 모듈 도입은 상대 경로 도입과 동치가 아닙니다 (예를 들어/mod)
다른 형식은 모두 비상대 모듈 도입 (non-relative import) 입니다. 예를 들어:
import * as $ from "jquery";
import { Component } from " @angular/core";
둘의 차이는 다음과 같습니다:
-
상대 모듈 도입: 도입하는 파일에 상대적으로 모듈을 찾으며, 외부 모듈 선언으로 해석되지 않습니다. (런타임에서 상대 위치를 유지할 수 있는) 커스텀 모듈을 도입하는 데 사용
-
비상대 모듈 도입: baseUrl 에 상대적으로 또는 패스 매핑 에 따라 모듈을 찾으며, 외부 모듈 선언으로 해석될 수 있습니다. 외부 의존 모듈을 도입하는 데 사용
二。모듈 해석 전략
구체적으로, 2 가지 모듈 해석 전략이 있습니다:
이 2 가지 전략은--moduleResolution 컴파일 옵션을 통해 지정할 수 있으며, 기본적으로 타겟 모듈 형식에 따라 결정됩니다 (module === "AMD" or "System" or "ES6" ? "Classic" : "Node")
Classic
Classic 전략 하에서, 상대 모듈 도입은 도입하는 파일에 상대적으로 해석됩니다. 예를 들어:
// 소스 파일 /root/src/folder/A.ts
import { b } from "./moduleB"
다음을 찾는 것을 시도합니다:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
비상대 모듈 도입의 경우, 도입하는 파일을 포함한 디렉토리에서 시작하여 디렉토리 트리를 위로 올라가며 일치하는 정의 파일을 찾습니다. 예를 들어:
// 소스 파일 /root/src/folder/A.ts
import { b } from "moduleB"
다음 파일들을 찾는 것을 시도합니다:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
Node
NodeJS 모듈 해석
NodeJS 에서require 를 통해 모듈을 도입하며, 모듈 해석의 구체적인 동작은 파라미터가 상대 경로인지 비상대 경로인지에 따라 달라집니다
상대 경로의 처리 전략은 매우 간단합니다. 다음과 같은 경우:
// 소스 파일 /root/src/moduleA.js
var x = require("./moduleB");
매치 순서는 다음과 같습니다:
-
/root/src/moduleB.js매치를 시도 -
다음으로
/root/src/moduleB/package.json매치를 시도하고, 다음으로 주 모듈을 찾음 (예를 들어{ "main": "lib/mainModule.js" }가 지정된 경우,/root/src/moduleB/lib/mainModule.js를 도입) -
否则
/root/src/moduleB/index.js매치를 시도.index.js는 암묵적으로该 디렉토리 하의 주 모듈로 간주되기 때문
P.S. 자세한 내용은 NodeJS 문서 참조: File Modules 와 Folders as Modules
비상대 모듈 도입은node_modules 에서 찾습니다 (node_modules 는 현재 파일의 평급 디렉토리에 위치할 수도 있고, 조상 디렉토리에 있을 수도 있습니다). NodeJS 는 각node_modules 를 위로查找하며, 도입할 모듈을 찾습니다. 예를 들어:
// 소스 파일 /root/src/moduleA.js
var x = require("moduleB");
NodeJS 는 순차적으로 다음 매치를 시도합니다:
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json
/node_modules/moduleB/index.js
P.S.package.json 에 대해서는, 실제로는 그main 필드가 가리키는 모듈을 로드합니다
P.S. NodeJS 가 어떻게node_modules 에서 모듈을 로드하는지에 대한 자세한 정보는, Loading from node_modules Folders 참조
TypeScript 의 NodeJS 전략 모방
(모듈 해석 전략이"Node"인 경우) TypeScript 도 NodeJS 런타임의 모듈 해석 메커니즘 을 시뮬레이션하여, 컴파일 시에 모듈의 정의 파일을 찾습니다
구체적으로, TypeScript 소스 파일의 접미사 이름을 NodeJS 의 모듈 해석 로직에 추가하고, package.json 중의types 필드를 통해 선언 파일을 찾습니다 (NodeJS 의main 필드를 시뮬레이션하는 것과 동등). 예를 들어:
// 소스 파일 /root/src/moduleA.ts
import { b } from "./moduleB"
다음 매치를 시도합니다:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
P.S.package.json 에 대해서는, TypeScript 는 그types 필드가 가리키는 모듈을 로드합니다
이 프로세스는 NodeJS 와 매우 유사합니다 (먼저moduleB.js, 다음으로package.json, 마지막으로index.js). TypeScript 의 소스 파일 접미사 이름으로 교체되었을 뿐입니다
유사하게, 비상대 모듈 도입도 동일하게 NodeJS 의 해석 로직을 따르며, 먼저 파일을 찾고, 다음으로 적용 가능한 폴더를 찾습니다:
// 소스 파일 /root/src/moduleA.ts
import { b } from "moduleB
모듈查找 순서는 다음과 같습니다:
/root/src/node_modules/moduleB.ts|tsx|d.ts
/root/src/node_modules/moduleB/package.json
/root/src/node_modules/ @types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts|tsx|d.ts
/root/node_modules/moduleB.ts|tsx|d.ts
/root/node_modules/moduleB/package.json
/root/node_modules/ @types/moduleB.d.ts
/root/node_modules/moduleB/index.ts|tsx|d.ts
/node_modules/moduleB.ts|tsx|d.ts
/node_modules/moduleB/package.json
/node_modules/ @types/moduleB.d.ts
/node_modules/moduleB/index.ts|tsx|d.ts
NodeJS 查找 로직과 거의 일치하지만, node_modules/ @types 에서d.ts 선언 파일을 찾는 것이 추가됩니다
三。추가 모듈 해석 마커
빌드 시에는.ts 를.js 로 컴파일하고, 서로 다른 소스 위치에서 의존성을 하나의 출력 위치로 복사합니다. 따라서, 런타임에는 모듈이 소스 파일과 다른 명칭을 가질 수 있거나, 컴파일 시에 마지막으로 출력되는 모듈 경로가 해당하는 소스 파일과 일치하지 않을 수 있습니다
이러한 문제들에 대해, TypeScript 는 일련의 마커를 제공하여 컴파일러에게 소스 경로상에서 발생할 것으로 예상되는 변환을 알리고, 최종 출력을 생성합니다
P.S. 주의, 컴파일러는 어떠한 변환도 수행하지 않으며, 이 정보만을 사용하여 모듈 도입에서 그 정의 파일로의 프로세스를 해석하는 것을 지도합니다
Base URL
baseUrl 는AMD 모듈을 따르는 애플리케이션에서 매우 흔하며, 모듈의 소스 파일은 서로 다른 디렉토리에 위치하고, 빌드 스크립트에 의해 함께 배치됩니다. 런타임에는 이러한 모듈들이 단일 디렉토리에 "배포"됩니다
TypeScript 에서는baseUrl 를 설정하여 컴파일러에게 모듈을 어디서 찾는지 알리며, 모든 비상대 모듈 도입은baseUrl 에 상대적으로 있습니다. 두 가지 지정 방법이 있습니다:
-
커맨드라인 파라미터
--baseUrl(상대 경로를 지정하는 경우, 현재 디렉토리를 기준으로 계산) -
tsconfig.json중의baseUrl필드 (상대 경로인 경우,tsconfig.json이所在하는 디렉토리를 기준으로 계산)
주의, 상대 모듈 도입은 baseUrl 의 영향을 받지 않습니다. 항상 도입하는 파일에 상대적으로 해석되기 때문입니다
패스 매핑
일부 모듈은baseUrl下に 없습니다. 예를 들어jquery 모듈은 런타임에는node_modules/jquery/dist/jquery.slim.min.js 에서 올 수 있습니다. 이 때, 모듈 로더는 패스 매핑을 통해 모듈 이름을 런타임의 파일에 대응시킵니다
TypeScript 도 유사한 매핑 설정 (tsconfig.json 의paths 필드) 을 지원합니다. 예를 들어:
{
"compilerOptions": {
"baseUrl": ".", // "paths" 가 지정된 경우, 이를 지정해야 합니다
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 이 매핑은"baseUrl" 에 상대적으로 있습니다
}
}
}
주의, paths 중의 패스도baseUrl 에 상대적으로 있습니다. baseUrl 가 바뀌면, paths 도 따라 변경해야 합니다
실제로, 더 복잡한 매핑 규칙도 지원하며, 예를 들어 여러 대체 위치 등. 자세한 내용은 Path mapping 참조
rootDirs 가상 디렉토리 지정
컴파일 시, 때로는 여러 디렉토리에서의 프로젝트 소스 코드를 통합하여 단일 출력 디렉토리에 생성합니다. 일련의 소스 디렉토리에서 "가상" 디렉토리를 생성하는 것과 동등합니다
rootDirs 는 컴파일러에게 "가상" 디렉토리를 구성하는那些 "루트" 패스를 알리며, 컴파일러가 "가상" 디렉토리를 가리키는 상대 모듈 도입을 해석할 수 있게 합니다. 마치它们이미 동일 디렉토리에 통합된 것처럼. 예를 들어:
src # 소스 코드
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated # 자동 생성된 템플릿 파일
└── templates
└── views
└── template1.ts (imports './view2')
빌드 도구가它们을 동일 출력 디렉토리에 통합한다고 가정합니다 (즉, 런타임에view1 과template1 은 함께 있음). 따라서./xxx 의 방식으로 도입할 수 있습니다. rootDirs 를 통해 이 관계를 컴파일러에 알리고, 소스 디렉토리를 모두 열거합니다:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
此后, rootDirs 서브디렉토리를 가리키는 상대 모듈 도입을 만나면, rootDirs 의 각 항목에서查找을 시도합니다
실제로, rootDirs 는 매우 유연하며, 배열중에 임의의 수의 디렉토리 이름을 포함할 수 있고, 디렉토리가 실제로 존재하는지 여부와 관계없이. 이를 통해 컴파일러는 타입 안전한 방식으로, 복잡한 빌드/런타임 특성 (조건부 도입이나 프로젝트 고유의 로더 플러그인 등) 을 "포착"할 수 있습니다
예를 들어 국제화 시나리오에서는, 빌드 도구가 특수한 패스 식별자 (예를 들어#{locale}) 를 삽입하여 자동으로 현지 특정 bundle 을 생성합니다. 예를 들어./#{locale}/messages 를 구체적인./zh/messages, ./de/messages 등으로 매핑합니다. rootDirs 를 통해 쉽게 해결할 수 있습니다:
{
"compilerOptions": {
"rootDirs": [
"src/zh",
"src/de",
"src/#{locale}"
]
}
}
로컬에zh 언어 팩이 있는 경우, 컴파일 시에는src/zh 하의 파일이 도입됩니다. 예를 들어import messages from './#{locale}/messages 는import messages from './zh/messages' 로 해석됩니다
四。해석 프로세스 추적
모듈은 현재 디렉토리 밖의 파일을 참조할 수 있으므로, 모듈 해석 관련 문제 (모듈을 찾지 못하거나, 잘못된 것을 찾는 등) 를定位하는 것은 쉽지 않습니다
이 때, --traceResolution 옵션을 켜서 컴파일러 내부의 모듈 해석 프로세스를 추적할 수 있습니다. 예를 들어:
$ tsc --traceResolution
# 도입된 ���듈 이름 및 소재 위치
======== Resolving module './math-lib' from '/proj/src/index.ts'. ========
# 모듈 해석 전략
Explicitly specified module resolution kind: 'NodeJs'.
# 구체적인 프로세스
Loading module as file / folder, candidate module location '/proj/src/math-lib', target file type 'TypeScript'.
File '/proj/src/math-lib.ts' does not exist.
File '/proj/src/math-lib.tsx' does not exist.
File '/proj/src/math-lib.d.ts' exist - use it as a name resolution result.
# 최종 결과
======== Module name './math-lib' was successfully resolved to '/proj/src/math-lib.d.ts'. ========
五。관련 옵션
--noResolve
일반적으로, 컴파일러는 시작 전에 모든 모듈 도입을 해석하려고 시도하며, 모듈 도입을 하나 성공시킬 때마다, 해당하는 파일을 처리할 소스 파일 세트에 추가합니다
--noResolve 컴파일 옵션은 컴파일러가 어떠한 파일도 추가하는 것을 금지합니다 (커맨드라인을 통해传入된 것을 제외). 이 때仍然 모듈에 해당하는 파일의 해석을 시도하지만, 더 이상 추가하지 않습니다. 예를 들어 소스 파일:
// 소스 파일 app.ts
import * as A from "moduleA"
import * as B from "moduleB"
tsc app.ts moduleA.ts --noResolve 는moduleA 를 올바르게 도입할 수 있지만, moduleB 는 찾지 못했다는 오류가 보고됩니다 (--noResolve 가 다른 파일의 추가를 허용하지 않기 때문)
exclude
기본적으로, tsconfig.json 이所在하는 디렉토리가 TypeScript 프로젝트 디렉토리입니다. files 또는exclude 를 지정하지 않는 경우, 该 디렉토리 및 그 자손 디렉토리 하의 모든 파일이 컴파일 프로세스에 추가됩니다. exclude 옵션을 통해 일부 파일을 배제 (블랙리스트) 하거나, files 옵션을 사용하여 컴파일하고 싶은 소스 파일을 지정 (화이트리스트) 할 수 있습니다
게다가, 컴파일 프로세스 중에 도입된 모듈을 만나면, 그것도 추가됩니다. exclude 되어 있는지 여부와 관계없이. 따라서, 컴파일 시에 파일을 완전히 배제하려면, exclude 자체 외에, 그것을 인용하고 있는 모든 파일도 모두 배제해야 합니다
아직 댓글이 없습니다