我不明白我们如何分类?
命名空间用于组织/封装您的代码。外部模块用于组织/封装您的代码并在运行时定位您的代码。实际上,您在运行时有两种选择: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 类型,以便 tsconfig.json 也选择它,将所有输出合并到一个 JS 文件中。如果代码重用不是很重要的话,这对于大多数项目来说都可以正常工作。
我的服务.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);
}
我的控制器.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 作为全局变量的情况。
我的服务.ts:
interface MyService {
....
}
(function() {
class MyServiceImpl implements MyService {
}
angular.module("MyApp").service("myService", MyServiceImpl);
})();
我的控制器.ts:
(function() {
class MyController {
constructor(private myService: MyService) {
}
}
angular.module("MyApp").controller("myController", MyController);
})();