如何在TypeScript外部模块中使用名称空间?

我有一些代码:

baseTypes.ts

export module Living.Things { export class Animal { move() { /* ... */ } } export class Plant { photosynthesize() { /* ... */ } } } 

dog.ts

 import b = require('./baseTypes'); export module Living.Things { // Error, can't find name 'Animal', ?? export class Dog extends Animal { woof() { } } } 

tree.ts

 // Error, can't use the same name twice, ?? import b = require('./baseTypes'); import b = require('./dogs'); module Living.Things { // Why do I have to write b.Living.Things.Plant instead of b.Plant?? class Tree extends b.Living.Things.Plant { } } 

这一切都很混乱。 我希望有一大堆外部模块可以将相同的名字空间贡献出来, Living.Things 。 看起来这根本行不通 – 我不能在dogs.ts看到Animal 。 我必须在tree.ts编写完整的命名空间名称tree.ts 。 在跨文件的同一名称空间中组合多个对象是行不通的。 我如何做到这一点?

糖杯比喻

版本1:每个糖果的杯子

假设你写了这样的代码:

Mod1.ts

 export namespace A { export class Twix { ... } } 

Mod2.ts

 export namespace A { export class PeanutButterCup { ... } } 

Mod3.ts

 export namespace A { export class KitKat { ... } } 

你已经创build了这个设置: 在这里输入图像描述

每个模块(纸)都有自己的杯子 A 这没用 – 你实际上没有在这里组织你的糖果,你只是在你和糖果之间增加一个步骤(把它从杯子里拿出来)。


版本2:全球范围内的一杯

如果你不使用模块,你可能会写这样的代码(注意缺lessexport声明):

global1.ts

 namespace A { export class Twix { ... } } 

global2.ts

 namespace A { export class PeanutButterCup { ... } } 

global3.ts

 namespace A { export class KitKat { ... } } 

代码在全局范围内创build一个合并的名称空间A

在这里输入图像描述

这种设置是有用的,但不适用于模块(因为模块不会污染全局范围)。


版本3:无所事事

回到原来的例子,杯子AAA没有任何好处。 相反,你可以编写代码:

Mod1.ts

 export class Twix { ... } 

Mod2.ts

 export class PeanutButterCup { ... } 

Mod3.ts

 export class KitKat { ... } 

创build一个看起来像这样的图片:

在这里输入图像描述

好多了!

现在,如果你仍然想着你真的想在模块中使用命名空间,请继续阅读…


这些不是你正在寻找的概念

我们需要回顾起名字空间为什么存在的原因,并检查这些原因是否对外部模块有意义。

组织 :命名空间可方便地将逻辑相关的对象和types组合在一起。 例如,在C#中,您将在System.Collections查找所有的集合types。 通过将我们的types组织到分层名称空间中,我们为这些types的用户提供了一个很好的“发现”体验。

名称冲突 :命名空间对于避免命名冲突很重要。 例如,您可能有My.Application.Customer.AddFormMy.Application.Order.AddForm – 两个具有相同名称的types,但名称空间不同。 在所有标识符都存在于同一个根作用域中并且所有程序集加载所有types的语言中,将所有内容都放在一个名称空间中至关重要。

这些原因在外部模块中是否有意义?

组织 :外部模块已经存在于文件系统中。 我们必须通过path和文件名来解决它们,所以有一个合理的组织scheme供我们使用。 我们可以在其中包含一个带有list模块的/collections/generic/文件夹。

名称冲突 :这在外部模块中根本不适用。 一个模块中,有两个同名的对象没有合理的理由。 从消费方面来说,任何给定模块的用户都可以select他们将用来引用模块的名称,这样意外的命名冲突是不可能的。


即使你不相信这些原因已经被模块的工作原理所充分解决了,试图在外部模块中使用命名空间的“解决scheme”也不能工作。

在箱子的箱子的箱子

一个故事:

你的朋友鲍勃打电话给你。 “我家里有一个很好的新组织scheme”,他说,“来看看吧!” 整洁的,让我们来看看鲍勃已经提出了什么。

你从厨房开始,打开食品室。 有60个不同的盒子,每个都标有“食品室”。 你随机挑选一个盒子并打开它。 里面是标有“谷物”的单个框。 你打开“谷物”盒子,find一个标有“面食”的盒子。 你打开“面食”盒子,find一个标有“Penne”的盒子。 你打开盒子,如你所料,find一袋通心粉。

有点困惑,你拿起一个相邻的盒子,也被称为“茶水间”。 里面是一个单独的盒子,又被标记为“谷物”。 你打开“谷物”框,并再次find一个标有“面食”的框。 你打开“意大利面”盒子,find一个盒子,这个盒子被标为“Rigatoni”。 你打开这个盒子,find一袋通心粉意大利面。

“这很棒!” 鲍勃说。 “一切都在一个名字空间!”。

“但鲍勃……”你回答。 “你的组织scheme是没有用的,你必须打开一堆箱子才能find任何东西,如果你把所有东西都放在一个盒子里而不是三个盒子里面,食品室已经按货架分类,根本不需要包装箱,为什么不把货架放在架子上,在需要的时候拿起呢?

“你不明白 – 我需要确保没有其他人放置不属于'Pantry'命名空间的东西,而且我已经安全地将所有的面食组织到了Pantry.Grains.Pasta命名空间中,所以我可以很容易地find它“

鲍勃是一个非常困惑的人。

模块是他们自己的盒子

你可能在现实生活中发生过类似的事情:你在亚马逊上订购了几件东西,每件商品都出现在自己的盒子里面,里面装着一个小盒子,你的东西包装在自己的包装里。 即使内部的箱子是相似的,货物也不是有用的“组合”。

与箱子类比,关键的观察是外部模块是他们自己的盒子 。 这可能是一个非常复杂的项目,具有很多function,但是任何给定的外部模块都是它自己的盒子。


外部模块指南

现在我们已经知道我们不需要使用“命名空间”,我们应该如何组织我们的模块? 一些指导原则和例子如下。

尽可能靠近顶层导出

  • 如果您只导出一个类或函数,请使用export default

MyClass.ts

 export default class SomeType { constructor() { ... } } 

MyFunc.ts

 function getThing() { return 'thing'; } export default getThing; 

消费

 import t from './MyClass'; import f from './MyFunc'; var x = new t(); console.log(f()); 

这对消费者来说是最佳的。 他们可以根据自己的需要命名你的types(在这种情况下),而不必做任何多余的点击来find你的对象。

  • 如果您要导出多个对象,请将它们全部放在顶层:

MyThings.ts

 export class SomeType { /* ... */ } export function someFunc() { /* ... */ } 

消费

 import * as m from './MyThings'; var x = new m.SomeType(); var y = m.someFunc(); 
  • 如果你正在导出大量的东西,那么你只能使用module / namespace关键字:

MyLargeModule.ts

 export namespace Animals { export class Dog { ... } export class Cat { ... } } export namepsace Plants { export class Tree { ... } } 

消费

 import { Animals, Plants} from './MyLargeModule'; var x = new Animals.Dog(); 

红旗

以下所有内容都是模块结构的红旗。 仔细检查你是不是试图命名空间你的外部模块,如果这些适用于您的文件:

  • 只有顶层声明是export module Foo { ... } (移除Foo并将所有内容都移动到一个级别)
  • 具有单个export classexport function的文件不是export default
  • 多个文件具有相同的export module Foo {在顶层(不要以为这些将会合并成一个Foo !)

Ryan的答案没有什么错,但是对于那些来这里寻找如何保持一个一级文件结构,同时仍然正确使用ES6命名空间的人请参考这个来自Microsoft的有用资源。

阅读文档后,我不清楚的一件事是:如何导入整个(合并)模块与一个单一的 import

编辑回圈来更新这个答案。 TS中出现了一些名称空间的方法。

所有模块类在一个文件中。

 export namespace Shapes { export class Triangle {} export class Square {} } 

将文件导入名称空间并重新分配

 import { Triangle as _Triangle } from './triangle'; import { Square as _Square } from './square'; export namespace Shapes { export const Triangle = _Triangle; export const Square = _Square; } 

 // ./shapes/index.ts export { Triangle } from './triangle'; export { Square } from './square'; // in importing file: import * as Shapes from './shapes/index.ts'; let myTriangle = new Shapes.Triangle(); 

最后的考虑。 你可以命名空间每个文件

 // triangle.js export namespace Shapes { export class Triangle {} } 

但是从一个名字空间导入两个类时,TS会抱怨有一个重复的标识符。 这次唯一的解决scheme是别名命名空间。

 import { Shapes } from './square'; import { Shapes as _Shapes } from 'triangle'; // ugh let myTriangle = new _Shapes.Shapes.Triangle(); 

这种别名是绝对可恶的,所以不要这样做。 你最好用上面的方法。 就个人而言,我更喜欢“桶”。

尝试按文件夹整理:

baseTypes.ts

 export class Animal { move() { /* ... */ } } export class Plant { photosynthesize() { /* ... */ } } 

dog.ts

 import b = require('./baseTypes'); export class Dog extends b.Animal { woof() { } } 

tree.ts

 import b = require('./baseTypes'); class Tree extends b.Plant { } 

LivingThings.ts

 import dog = require('./dog') import tree = require('./tree') export = { dog: dog, tree: tree } 

main.ts

 import LivingThings = require('./LivingThings'); console.log(LivingThings.Tree) console.log(LivingThings.Dog) 

这个想法是,你的模块本身不应该关心/知道他们正在参与一个命名空间,但是这将以一种紧凑的,明智的方式将你的API暴露给消费者,这与你为这个项目使用哪种types的模块系统是不可知的。

Albinofrenchy的小动作回答:

base.ts

 export class Animal { move() { /* ... */ } } export class Plant { photosynthesize() { /* ... */ } } 

dog.ts

 import * as b from './base'; export class Dog extends b.Animal { woof() { } } 

things.ts

 import { Dog } from './dog' namespace things { export const dog = Dog; } export = things; 

main.ts

 import * as things from './things'; console.log(things.dog); 

OP我和你在一起。 这个答案也没有问题,但是我的意见是:

  1. 将课堂单独放入舒适温暖的自己的文件有什么问题? 我的意思是这会让事情看起来好多了吧? (或者就像所有模型的1000行文件一样)

  2. 那么,如果第一个将会实现,我们必须在每个模型文件(如man,srsly,模型文件,.d.ts文件)中导入import import … import,为什么会有这么多*在那里? 它应该是简单,整洁,就是这样。 为什么我需要import? 为什么? C#有名字空间的原因。

  3. 到那时,你真的使用“filenames.ts”作为标识符。 作为标识符…现在到2017年,我们仍然这样做? Ima回到火星再睡1000年。

遗憾的是,我的答案是:nop,如果不使用所有这些导入或使用这些文件名作为标识符(我认为这很愚蠢),则不能使“名称空间”function正常工作。 另一种select是:将所有这些依赖关系放入名为filenameasidentifier.ts的框中并使用

 export namespace(or module) boxInBox {} . 

包装它们,所以他们不会尝试访问其他具有相同名称的类时,他们只是试图从类坐在上面的引用。

dog.ts

 import b = require('./baseTypes'); export module Living.Things { // Error, can't find name 'Animal', ?? // Solved: can find, if properly referenced; exporting modules is useless, anyhow export class Dog extends b.Living.Things.Animal { public woof(): void { return; } } } 

tree.ts

 // Error, can't use the same name twice, ?? // Solved: cannot declare let or const variable twice in same scope either: just use a different name import b = require('./baseTypes'); import d = require('./dog'); module Living.Things { // Why do I have to write b.Living.Things.Plant instead of b.Plant?? class Tree extends b.Living.Things.Plant { } } 

我在这个主题上看到的几个问题/评论听起来像是在使用Namespace ,他们的意思是“模块别名”。 Ryan Cavanaugh在他的一个评论中提到,你可以用一个“Wrapper”模块重新导出几个模块。

如果你真的想从相同的模块名称/别名中导入它,请在你的tsconfig.json一个包装模块与path映射结合起来。

例:

./path/to/CompanyName.Products/Foo.ts

 export class Foo { ... } 

./path/to/CompanyName.Products/Bar.ts

 export class Bar { ... } 

./path/to/CompanyName.Products/index.ts

 export { Foo } from './Foo'; export { Bar } from './Bar'; 

tsconfig.json

 { "compilerOptions": { ... paths: { ... "CompanyName.Products": ["./path/to/CompanyName.Products/index"], ... } ... } ... } 

main.ts

 import { Foo, Bar } from 'CompanyName.Products' 

注意 :输出.js文件中的模块分辨率需要以某种方式处理,比如使用https://github.com/tleunen/babel-plugin-module-resolver

例如.babelrc来处理别名parsing:

 { "plugins": [ [ "module-resolver", { "cwd": "babelrc", "alias": { "CompanyName.Products": "./path/to/typescript/build/output/CompanyName.Products/index.js" } }], ... other plugins ... ] } 

Necromancing。
如果我理解正确的话,你会问如何把所有的类放在一个单独的文件中,同时为它们保留一个单一的命名空间。

由于没有人似乎有一个好的解决scheme – 这是一个简单的解决scheme,甚至不涉及打字稿的想法:这个解决scheme被称为Gulp

把你需要在一个命名空间中的所有类放到同一个文件夹中(对于代码组织非常有用)。 然后添加一个吞噬任务,将此目录中的所有文件合并成一个文件(gulp-concat)。 然后,添加一个名称与顶层目录相同的名称空间,然后添加连接的文件,然后添加一个右大括号,并保存到一个文件中。
完成。
然后添加一个吞吐任务,监视在同一目录中的更改(和添加/删除)。 在更改/添加时,触发连接function。

现在,您将所有类都放在一个文件中,并将一个文件包含在一个名称空间中的所有类。
示例代码 – 大致如下:

 gulp.task("js:min:all", function () { return gulp.src(["./wwwroot/app/**/*.js", "!" + "./wwwroot/app/**/*.min.js" , "./wwwroot/GeneratedScripts/**/*.js", "!" + "./wwwroot/GeneratedScripts/**/*.min.js"], { base: "." }) .pipe(concat("./wwwroot/js/myscripts.min.js")) .pipe(uglify()) .pipe(gulp.dest(".")); }); gulp.task('watch:js', function () { gulp.watch('js/**/*.js', ['js:min:all']); }); 

这里有一个gulp append-prepend模块: https : //www.npmjs.com/package/gulp-append-prepend

 var gap = require('gulp-append-prepend'); gulp.task('myawesometask', function(){ gulp.src('index.html') .pipe(gap.prependFile('header.html')) .pipe(gap.prependText('<!-- HEADER -->')) .pipe(gap.appendText('<!-- FOOTER -->')) .pipe(gap.appendFile('footer.html')) .pipe(gulp.dest('www/')); }); 

最后,设置观察者开始解决scheme负载,你就完成了。