深入理解TypeScript
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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"能让你快速开始。