
4.3 模块
1.全局模块
在默认情况下,当你开始在一个新的TypeScript文件中写下代码时,它处于全局命名空间中。如在foo.ts里的以下代码。

如果你在相同的项目里创建了一个新的文件bar.ts,TypeScript类型系统将会允许你使用变量foo,就像它在全局中可用一样。

毋庸置疑,使用全局命名空间是危险的,因为它会与文件内的代码命名相冲突。我们推荐使用下文中将要提到的文件模块。
2.文件模块
文件模块也被称为外部模块。如果在TypeScript文件的根级别位置含有import或export,那么它会在这个文件中创建一个本地的作用域。因此,我们需要把上文中的foo.ts改成如下方式(注意export的用法)。

在全局命名空间里,我们不再有foo,这可以通过创建一个新文件bar.ts来证明。

如果你想在bar.ts里使用来自foo.ts的内容,你必须显式地导入它,更新后的bar.ts如下所示。

在bar.ts文件里使用import时,它不仅允许你使用从其他文件导入的内容,还会将此文件bar.ts标记为一个模块,在文件内定义的声明也不会“污染”全局命名空间。
从带有外部模块的TypeScript文件中,生成什么样的JavaScript文件,是由编译选项module决定的。
文件模块(外部模块)拥有强大的功能和较强的可用性。下面我们来讨论它的功能及一些用法。
1)澄清:CommonJS、AMD、ES模块,以及其他
首先,我们需要澄清这些模块系统的不一致性。我将给你一些建议,并消除一些你的顾虑。
你可以根据不同的module选项来把TypeScript编译成不同的JavaScript模块类型,下面是一些你可以忽略的东西。
● AMD:不要使用它,它只能在浏览器上工作。
● SystemJS:这是一个好的实验,已经被ES模块代替。
● ES模块:它并没有准备好。
使用module:commonjs选项来代替这些生成JavaScript的选项,将会是一个好主意。
怎么书写TypeScript模块呢?这也是一件让人困惑的事。今天我们应该这么做:使用ES模块语法代替import foo=require('foo')即import/require语法。
这很酷,接下来,让我们看看ES模块语法。
总结:使用module:commonjs选项和ES模块语法来导入、导出、编写模块。
2)ES模块语法
使用export关键字导出一个变量或类型。

export的写法除了上面这种,还有另外一种。

你也可以用重命名变量的方式导出。

使用import关键字导入一个变量或一个类型。

通过重命名的方式导入变量或类型。

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,将所有输出值都加载到这个对象上面。

只导入模块。

从其他模块导入后整体导出。

从其他模块导入后,部分导出。

通过重命名,部分导出从另一个模块导入的项目。

默认导入/导出
我并不喜欢用默认导出,虽然有默认导出的语法。
● 使用默认导出(export default)。
在一个变量之前(不需要使用let/const/var)。
在一个函数之前。
在一个类之前。

● 使用import someName from'someModule'语法导入(可以根据需要为导入的内容命名)。

3)模块路径
我只是想确认,在你的TypeScript编译选项中应该包含moduleResolution:commonjs选项,module:commonjs选项自动包含此设置。
这里存在两种截然不同的模块,它们是由导入语句时不同的路径写法所引起的,例如,import foo from'模块路径'。模块路径的写法主要有以下两种。
● 相对模块路径,以.开头,例如./someFile,或者../../someFolder/someFile等。
● 其他动态查找模块,如core-js、typestyle、react或react/core等。
它们的主要区别来自系统如何解析模块。
在提及查找模式后,我将使用一个概念性术语——place来解释它。
相对模块路径
这很简单,只要按照相对路径来写就可以了。
● 如果文件bar.ts中含有import*as foo from'./foo',那么foo文件必须与bar.ts文件存在于相同的文件夹下。
● 如果文件bar.ts中含有import*as foo from'../foo',那么foo文件所存在的地方必须是bar.ts文件的上一级目录。
● 如果文件bar.ts中含有import*as foo from'../someFolder/foo',那么foo文件所在的文件夹someFolder必须与bar.ts所在的文件夹在相同的目录下。
你还可以去思考一下使用相对路径导入的其他情景。
动态查找
当导入路径不是相对路径时,模块解析将会模仿Node模块解析策略(详见参考资料[7]),下面我将给出一个简单的例子。
● 当你使用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.
一直查到系统的根目录。
什么是place
当我提及被检查的place时,我想表达的是在这个place上,TypeScript将会检查以下内容(例如一个foo的place)。
● 如果这个place表示一个文件,如foo.ts。
● 或者,这个place是一个文件夹,并且存在一个文件foo/index.ts。
● 或者,这个place是一个文件夹,并且存在一个foo/package.json文件,在该文件中指定types的文件存在。
● 也或者,这个place是一个文件夹,并且存在一个package.json文件,在该文件中指定main的文件存在。
从文件类型上来说,我实际上是指.ts、.d.ts或.js
就是这样,现在你已经是一个模块查找专家了,这并不是一个小小的成功。
重写类型的动态查找
在你的项目里,你可以通过使用declare module'somePath'声明一个全局模块的方式,来解决查找模块路径的问题。

接下来的示例如下。

4)import/require只是导入类型
导入的语法如下。

它实际上只做了两件事。
● 导入foo模块的所有类型信息。
● 确定foo模块运行时的依赖关系。
你可以选择只加载类型信息,而没有运行时的依赖关系。在继续之前,你可能需要重新阅读本书的声明空间部分。
如果你没有把导入的名称当作变量声明空间来用,那么在编译成JavaScript时,导入的模块将会被完全移除。这最好用例子来解释,下面我们将会给出一些示例。
例1

这将会被编译成一个不含任何代码的JavaScript文件。并且这是正确的,一个没有被使用的空文件。
例2

这将会被编译成如下所示。

这是因为foo(或其他任何属性,如foo.bas)没有被当作一个变量使用。
例3

这将会被编译成(假设是CommonJS)如下所示。

这是因为foo被当作变量使用了。
使用实例:懒加载
类型推断需要提前完成,这意味着,如果你想在bar文件里使用从其他文件foo导出的类型,你将不得不像下面这样做。

然而,在某些情景下,你可能会想在运行时加载文件foo,此时你应该在类型注解中使用导入的模块名称,而不是把foo当作变量使用。在代码被编译成JavaScript时,这些将会被移除。接着,你可以手动导入所需要的模块。
例如以下基于CommonJS的代码,其中我们只在某个函数内加载foo模块。

一个同样简单的AMD模块(使用RequireJS),如下所示。


这些通常在以下情景中使用。
● 在Web App里,当你在特定路由上加载JavaScript时。
● 在Node应用里,当你只想加载特定模块,用来加快启动速度时。
使用实例:打破循环依赖
类似于懒加载的使用实例,某些模块加载器(CommonJS/Node和AMD/RequireJS)不能很好地处理循环依赖。在这种情况下,我们一方面延迟加载代码,另一方面预先加载模块,这是很有用的。
使用实例:确保导入
有时你加载一个模块,只是想引入其附加的作用,例如该模块可能会注册一些库,如CodeMirroraddons插件等(详见参考资料[8])。然而,如果你只是执行import/require,经过TypeScript编译后,转换后的JavaScript将不包含对模块的依赖关系;而你的模块加载器(如webpack),将会完全忽视它们。在这种情况下,你可以使用一个ensureImport变量,来确保编译后的JavaScript代码依赖于模块,示例如下。

3.globals.d.ts
在上文中,当我们讨论文件模块时,比较了全局变量与文件模块。并且我们推荐使用基于文件的模块,而不是选择“污染”全局命名空间。
然而,如果你的团队里有TypeScript初学者,你可以给他们提供一个globals.d.ts文件,用来将一些接口或类放入全局命名空间里,这些接口和类能在你的所有TypeScript代码中使用。
对于任何需要编译成JavaScript的代码,我们强烈建议把它们放入文件模块里。
● 如果你需要的话,globals.d.ts是一个扩充lib.d.ts的很好的方式。
● 当把JvaScript迁移到TypeScript时,定义declare module"some-library-you-dont-care-to-get-defs-for"能让你快速开始。