我不明白我们如何对它们进行分类?
命名空间用于组织/封装您的代码。外部模块用于组织/封装您的代码并在运行时定位您的代码。在实践中,您在运行时有两种选择:1) 将所有转译的代码合并到一个文件中,或者 2) 使用外部模块并拥有多个文件,并且需要一些其他机制来获取这些文件。
何时导出类、命名空间或包?
要使类型或值在其所在文件之外可见,如果它位于命名空间内,则必须将其导出。无论是在顶层还是在命名空间中导出它,都将决定它现在是否在外部模块中。
如果我们导出包/命名空间,其中的所有类都被导出或需要显式导出
命名空间中的类总是需要显式导出,以便该类在编译时在定义它的文件之外可见。
如何导入/需要它们中的每一个?
这取决于您是否使用外部模块。始终需要导入外部模块才能“使用”它。导入不在外部模块中的命名空间实际上只是为命名空间提供别名——您仍然必须在类型/任何内容前面加上别名(这就是为什么您通常不希望将命名空间与外部模块一起使用;这样做意味着您在引用外部模块提供的任何内容时始终必须使用前缀。)不在外部模块中的命名空间可以跨越文件,因此如果您在同一个命名空间中,则可以引用由命名空间,无需任何类型的导入。
要真正理解上述内容,您需要一些背景知识。理解引用/命名空间/外部模块的关键是这些结构在编译时做什么以及它们在运行时做什么。
在编译时使用引用指令来定位类型信息。您的来源中有一个特定的符号。 TypeScript 编译器如何定位该符号的定义?引用指令在很大程度上已被 tsconfig.json 机制所包含——使用 tsconfig.json,您可以告诉编译器您的所有源代码在哪里。
命名空间可以包含类型定义和/或实现。如果命名空间只包含类型信息,那么它根本没有运行时表现——您可以通过查看 JS 输出并找到一个空的 JS 文件来检查这一点。如果命名空间有实现代码,那么代码会包装在一个闭包中,该闭包分配给一个与命名空间同名的全局变量。使用嵌套命名空间,根命名空间将有一个全局变量。再次检查 JS 输出。从历史上看,命名空间是 JS 客户端库试图避免命名冲突问题的方式。这个想法是将整个库包装到一个闭包中,然后尽可能少地公开全局占用空间——只有一个引用闭包的全局变量。好吧,问题仍然是您在全局空间中声明了一个名称。如果你想要一个库的两个版本怎么办? TypeScript 命名空间仍然存在如何定位命名空间源的问题。也就是说,引用 A.B 的源代码仍然存在告诉编译器如何定位 A.B 的问题——通过使用引用指令或使用 tsconfig.json。或者将命名空间放入外部模块,然后导入外部模块。
外部模块源自服务器端 JS。外部模块与文件系统上的文件是一一对应的。您可以使用文件系统目录结构将外部模块组织成嵌套结构。导入外部模块通常会引入对该外部模块的运行时依赖(例外情况是当您导入外部模块但在值位置不使用其任何导出 - 也就是说,您只导入外部模块获取其类型信息)。外部模块隐含在闭包中,这是关键:模块的用户可以将闭包分配给他们想要的任何局部变量。 TypeScript/ES6 围绕将外部模块的导出映射到本地名称添加了额外的语法,但这只是一个细节。在服务器端,定位外部模块相对简单:只需在本地文件系统上定位代表外部模块的文件即可。如果您想在客户端使用外部模块,在浏览器中,它会变得更加复杂,因为没有等效于具有可加载模块的文件系统。所以现在在客户端,你需要一种方法将所有这些文件打包成一个可以在浏览器中远程使用的表单——这就是像 Webpack 这样的模块打包器(尽管 Webpack 比打包模块做的要多得多)和Browserify 开始发挥作用。模块捆绑器允许在浏览器中对外部模块进行运行时解析。
真实世界场景:AngularJS。假装外部模块不存在,使用单个命名空间来限制全局空间的污染(在下面的示例中,单个变量 MyApp 就是全局空间中的全部),仅导出接口,并使用 AngularJS 依赖注入进行实现可供使用。将所有类放在一个目录根目录下,将 tsconfig.json 添加到根目录,在同一目录根目录下安装 angularjs Typings,以便 tsconfig.json 也可以选择它,将所有输出合并到一个 JS 文件中。如果代码重用不是什么大问题,这对于大多数项目来说都可以正常工作。
MyService.ts:
namespace MyApp {
// without an export the interface is not visible outside of MyService.ts
export interface MyService {
....
}
// class is not exported; AngularJS DI will wire up the implementation
class MyServiceImpl implements MyService {
}
angular.module("MyApp").service("myService", MyServiceImpl);
}
MyController.ts:
namespace MyApp {
class MyController {
// No import of MyService is needed as we are spanning
// one namespace with multiple files.
// MyService is only used at compile time for type checking.
// AngularJS DI is done on the name of the variable.
constructor(private myService: MyService) {
}
}
angular.module("MyApp").controller("myController", MyController);
}
使用 IIFE 避免污染全局运行时范围。在此示例中,根本没有创建全局变量。 (假定为 tsconfig.json。)
Foo.ts:
namespace Foo {
// without an export IFoo is not visible. No JS is generated here
// as we are only defining a type.
export interface IFoo {
x: string;
}
}
interface ITopLevel {
z: string;
}
(function(){
// export required above to make IFoo visible as we are not in the Foo namespace
class Foo1 implements Foo.IFoo {
x: string = "abc";
}
// do something with Foo1 like register it with a DI system
})();
Bar.ts:
// alias import; no external module created
import IFoo = Foo.IFoo;
(function() {
// Namespace Foo is always visible as it was defined at
// top level (outside of any other namespace).
class Bar1 implements Foo.IFoo {
x: string;
}
// equivalent to above
class Bar2 implements IFoo {
x: string;
}
// IToplevel is visible here for the same reason namespace Foo is visible
class MyToplevel implements ITopLevel {
z: string;
}
})();
使用 IIFE,您可以避免在第一个示例中将 MyApp 作为全局变量引入。
MyService.ts:
interface MyService {
....
}
(function() {
class MyServiceImpl implements MyService {
}
angular.module("MyApp").service("myService", MyServiceImpl);
})();
MyController.ts:
(function() {
class MyController {
constructor(private myService: MyService) {
}
}
angular.module("MyApp").controller("myController", MyController);
})();