TypeScript 模块
任何包含 import 或 export 语句的文件
TypeScript 模块
简介
任何包含 import
或 export
语句的文件,就是一个模块(module)。相应地,如果文件不包含 export
语句,就是一个全局的脚本文件。
模块本身就是一个作用域,不属于全局作用域。模块内部的变量、函数、类只在内部可见,对于模块外部是不可见的。暴露给外部的接口,必须用 export
命令声明;如果其他文件要使用模块的接口,必须用 import
命令来输入。
如果一个文件不包含 export
语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句。
上面这行语句不产生任何实际作用,但会让当前文件被当作模块处理,所有它的代码都变成了内部代码。
ES 模块的详细介绍,请参考 ES6 教程,这里就不重复了。本章主要介绍 TypeScript 的模块处理。
TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。
上面示例中,当前脚本输出一个类型别名Bool
。这行语句把类型定义和接口输出写在一行,也可以写成两行。
假定上面的模块文件为a.ts
,另一个文件b.ts
就可以使用 import
语句,输入这个类型。
上面示例中,import
语句加载的是一个类型。注意,加载文件写成./a
,没有写脚本文件的后缀名。TypeScript 允许加载模块时,省略模块文件的后缀名,它会自动定位,将./a
定位到./a.ts
。
编译时,可以两个脚本同时编译。
上面命令会将a.ts
和b.ts
分别编译成a.js
和b.js
。
也可以只编译b.ts
,因为它是入口脚本,tsc 会自动编译它依赖的所有脚本。
上面命令发现b.ts
依赖a.ts
,就会自动寻找a.ts
,也将其同时编译,因此编译产物还是a.js
和b.js
两个文件。
import type 语句
import
在一条语句中,可以同时输入类型和正常接口。
上面示例中,文件a.ts
的 export
语句输出了一个类型A
和一个正常接口a
,另一个文件b.ts
则在同一条语句中输入了类型和正常接口。
这样很不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。
第一个方法是在 import
语句输入的类型前面加上type
关键字。
上面示例中,import
语句输入的类型A
前面有type
关键字,表示这是一个类型。
第二个方法是使用 import
type 语句,这个语句只用来输入类型,不用来输入正常接口。
上面示例中,import
type 输入类型A
是正确的,可以把A
当作类型使用。但是,输入正常接口a
,并把a
当作一个值使用,就会报错。这就是说,看到import
type,你就知道它输入的肯定是类型。
import
type 语句也可以输入默认类型。
import
type 在一个名称空间下,输入所有类型的写法如下。
同样的,export
语句也有两种方法,表示输出的是类型。
上面示例中,方法一是使用type
关键字作为前缀,表示输出的是类型;方法二是使用 export
type 语句,表示整行输出的都是类型。
下面是 export
type 将一个类作为类型输出的例子。
上面示例中,由于使用了 export
type 语句,输出的并不是 Point 这个类,而是 Point 代表的实例类型。输入时,只能作为类型输入。
上面示例中,Point
只能作为类型输入,不能当作正常接口使用。
importsNotUsedAsValues 编译设置
TypeScript 特有的输入类型(type)的 import
语句,编译成 JavaScript 时怎么处理呢?
TypeScript 提供了importsNotUsedAsValues
编译设置项,有三个可能的值。
(1)remove
:这是默认值,自动删除输入类型的 import
语句。
(2)preserve
:保留输入类型的 import
语句。
(3)error
:保留输入类型的 import
语句(与preserve
相同),但是必须写成import
type的形式,否则报错。
请看示例,下面是一个输入类型的 import
语句。
上面示例中,TypeA
是一个类型。
remove
的编译结果会将该语句删掉。
preserve
的编译结果会保留该语句,但会删掉其中涉及类型的部分。
上面就是preserve
的编译结果,可以看到编译后的import
语句不从a.js
输入任何接口(包括类型),但是会引发a.js
的执行,因此会保留a.js
里面的副作用。
error
的编译结果与preserve
相同,但在编译过程中会报错,因为它要求输入类型的import
语句必须写成import
type 的形式。原始语句改成下面的形式,就不会报错。
CommonJS 模块
CommonJS 是 Node.js 的专用模块格式,与 ES 模块格式不兼容。
import = 语句
TypeScript 使用import
=语句输入 CommonJS 模块。
上面示例中,使用import
=语句和require()
命令输入了一个 CommonJS 模块。模块本身的用法跟 Node.js 是一样的。
除了使用import =
语句,TypeScript 还允许使用import * as [接口名] from "模块文件"
输入 CommonJS 模块。
export = 语句
TypeScript 使用export =
语句,输出 CommonJS 模块的对象,等同于 CommonJS 的module.exports
对象。
export =
语句输出的对象,只能使用import =
语句加载。
模块定位
模块定位(module resolution)指的是一种算法,用来确定 import
语句和 export
语句里面的模块文件位置。
上面示例中,TypeScript 怎么确定./a
或jquery
到底是指哪一个模块,具体位置在哪里,用到的算法就叫做“模块定位”。
编译参数moduleResolution
,用来指定具体使用哪一种定位算法。常用的算法有两种:Classic
和Node
。
如果没有指定moduleResolution
,它的默认值与编译参数module
有关。module
设为commonjs
时(项目脚本采用 CommonJS 模块格式),moduleResolution
的默认值为Node
,即采用 Node.js 的模块定位算法。其他情况下(module
设为 es2015、 esnext、amd, system, umd 等等),就采用Classic
定位算法。
相对模块,非相对模块
加载模块时,目标模块分为相对模块(relative import
)和非相对模块两种(non-relative import
)。
相对模块指的是路径以/
、./
、../
开头的模块。下面 import
语句加载的模块,都是相对模块。
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
相对模块的定位,是根据当前脚本的位置进行计算的,一般用于保存在当前项目目录结构中的模块脚本。
非相对模块指的是不带有路径信息的模块。下面 import
语句加载的模块,都是非相对模块。
import * as $ from "jquery";
import { Component } from "@angular/core";
非相对模块的定位,是由baseUrl
属性或模块映射而确定的,通常用于加载外部模块。
Classic 方法
Classic 方法以当前脚本的路径作为“基准路径”,计算相对模块的位置。比如,脚本a.ts
里面有一行代码import { b } from "./b"
,那么 TypeScript 就会在a.ts
所在的目录,查找b.ts
和b.d.ts
。
至于非相对模块,也是以当前脚本的路径作为起点,一层层查找上级目录。比如,脚本a.ts
里面有一行代码import { b } from "b"
,那么就会依次在每一级上层目录里面,查找b.ts
和b.d.ts
。
Node 方法
Node 方法就是模拟 Node.js 的模块加载方法,也就是require()
的实现方法。
相对模块依然是以当前脚本的路径作为“基准路径”。比如,脚本文件a.ts
里面有一行代码let x = require("./b");
,TypeScript 按照以下顺序查找。
- 当前目录是否包含
b.ts
、b.tsx
、b.d.ts
。如果不存在就执行下一步。 - 当前目录是否存在子目录
b
,该子目录里面的package.json
文件是否有types
字段指定了模块入口文件。如果不存在就执行下一步。 - 当前目录的子目录
b
是否包含index.ts
、index.tsx
、index.d.ts
。如果不存在就报错。
非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录node_modules
。比如,脚本文件a.js
有一行let x = require("b");
,TypeScript 按照以下顺序进行查找。
- 当前目录的子目录
node_modules
是否包含b.ts
、b.tsx
、b.d.ts
。 - 当前目录的子目录
node_modules
,是否存在文件package.json
,该文件的types
字段是否指定了入口文件,如果是的就加载该文件。 - 当前目录的子目录
node_modules
里面,是否包含子目录@types
,在该目录中查找文件b.d.ts
。 - 当前目录的子目录
node_modules
里面,是否包含子目录b
,在该目录中查找index.ts
、index.tsx
、index.d.ts
。 - 进入上一层目录,重复上面4步,直到找到为止。
路径映射
TypeScript 允许开发者在tsconfig.json
文件里面,手动指定脚本模块的路径。
(1)baseUrl
baseUrl
字段可以手动指定脚本模块的基准目录。
上面示例中,baseUrl
是一个点,表示基准目录就是tsconfig.json
所在的目录。
(2)paths
paths
字段指定非相对路径的模块与实际脚本的映射。
上面示例中,加载模块jquery
时,实际加载的脚本是node_modules/jquery/dist/jquery
,它的位置要根据baseUrl
字段计算得到。
注意,上例的jquery
属性的值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。
(3)rootDirs
rootDirs
字段指定模块定位时必须查找的其他目录。
上面示例中,rootDirs
指定了模块定位时,需要查找的不同的国际化目录。
tsc 的--traceResolution
参数
由于模块定位的过程很复杂,tsc 命令有一个--traceResolution
参数,能够在编译时在命令行显示模块定位的每一步。
上面示例中,traceResolution
会输出模块定位的判断过程。
tsc 的--noResolve
参数
tsc 命令的--noResolve
参数,表示模块定位时,只考虑在命令行传入的模块。
举例来说,app.ts
包含如下两行代码。
使用下面的命令进行编译。
上面命令使用--noResolve
参数,因此可以定位到moduleA.ts
,因为它从命令行传入了;无法定位到moduleB
,因为它没有传入,因此会报错。