将TypeScript内部模块重组为外部模块

use*_*757 3 module amd ecmascript-harmony typescript webpack

我有一个使用大型打字稿代码库的网站.所有clases都在他们自己的文件中,并包含一个内部模块,如下所示:

文件BaseClass.ts

module my.module {
  export class BaseClass {
  }
}
Run Code Online (Sandbox Code Playgroud)

文件ChildClass.ts

module my.module {
  export ChildClass extends my.module.BaseClass  {
  }
}
Run Code Online (Sandbox Code Playgroud)

所有文件都以脚本标记全局包含在适当的顺序中(使用ASP.NET Bundling).

我想转向更现代的设置并使用webpack.我希望我的模块语法能够使用新的ECMASCRIPT模块标准.但是有很多代码使用现有的"模块命名空间",所以我想要一个支持这种类型代码的更新路径 -

let x = new my.module.ChildClass();
Run Code Online (Sandbox Code Playgroud)

所以我想我需要这样的东西 -

import * as my.module from ???;
Run Code Online (Sandbox Code Playgroud)

还是使用命名空间?

但是,如果这不是最佳做法,我想坚持最佳做法.内部模块目前非常有助于组织不同的应用程序层和服务......

由于"模块"跨越了许多文件,我将如何实现这一目标?实际上,我想要完成的只是拥有一个名称空间,并远离全局脚本.

Alu*_*dad 6

免责声明(这不是一个全面的指南,而是一个概念性的起点.我希望证明移民的可行性,但最终它需要相当多的努力)

我在一个大型企业项目中完成了这项工作.这不好玩,但它奏效了.

一些技巧:

  1. 只要您需要它们,就只保留全局命名空间对象.

  2. 从源代码的叶子开始,将没有依赖项的文件转换为外部模块.

  3. 虽然这些文件本身依赖于您一直使用的全局命名空间对象,但如果您从外部仔细工作,这将不会成为问题.

假设您有一个全局命名空间utils,它分布在3个文件中,如下所示

// utils/geo.ts
namespace utils {
  export function randomLatLng(): LatLng { return implementation(); };
}

// utils/uuid.ts
namespace utils {
  export function uuid(): string { return implementation(); };
}

// utils/http.ts

/// <reference path="./uuid.ts" />
namespace utils {
  export function createHttpClient (autoCacheBust = false) {
    const appendToUrl = autoCacheBust ? `?cacheBust=${uuid()}` : '';
    get<T>(url, options): Promise<T> {
      return implementation.get(url, {...options}).then(({data}) => data);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

现在假设您只有另一个全局范围的命名空间文件,这次,我们可以轻松地将其分解为适当的模块,因为它不依赖于其自己的命名空间的任何其他成员.例如,我将使用一种服务,使用来自的东西在全球各地的随机位置查询天气信息utils.

// services/weather-service.ts

/// <reference path="../utils/http.ts" />
/// <reference path="../utils/geo.ts" />
namespace services {
  export const weatherService = {
    const http = utils.http.createHttpClient(true);
    getRandom(): Promise<WeatherData> {
      const latLng = utils.geo.randomLatLng();
      return http
        .get<WeatherData>(`${weatherUrl}/api/v1?lat=${latLng.lat}&lng=${latLng.lng}`);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

不,我们将把我们的services.weatherSercice全局命名空间常量转换为适当的外部模块,在这种情况下它将相当容易

// services/weather-service.ts

import "../utils/http"; // es2015 side-effecting import to load the global
import "../utils/geo";  // es2015 side-effecting import to load the global
// namespaces loaded above are now available globally and merged into a single utils object

const http = utils.http.createHttpClient(true);

export default { 
    getRandom(): Promise<WeatherData> {
      const latLng = utils.geo.randomLatLng();
      return http
        .get<WeatherData>(`${weatherUrl}/api/v1?lat=${latLng.lat}&lng=${latLng.lng}`);
  } 
}
Run Code Online (Sandbox Code Playgroud)

常见陷阱和解决方法:

如果我们需要从我们现有的全局命名空间之一引用这个新模块化代码的功能,就会发生障碍

由于我们现在至少在代码的某些部分使用模块,我们有一个模块加载器或捆绑器正在运行(如果你为NodeJS编写,即一个快速应用程序,你可以忽略这个,因为平台集成了一个加载器,但你也可以使用自定义加载器).该模块加载器或捆绑器可以是SystemJS,RequireJS,Webpack,Browserify或更深奥的东西.

最大的,也是最常见的错误就是拥有这样的东西

// app.ts

/// <reference path="./services/weather-service.ts" />
namespace app {
  export async function main() {
    const dataForWeatherWidget = await services.weatherService.getRandom();
  }
}
Run Code Online (Sandbox Code Playgroud)

并且,由于这不再起作用,我们改为编写这个破碎的代码

// app.ts

import weatherService from './services/weather-service';

namespace app {
  export async function main() {
    const dataForWeatherWidget = await weatherService.getRandom();
  }
}
Run Code Online (Sandbox Code Playgroud)

上面的代码被破坏了,因为在我们准备好之前,只需添加一个import... from '...'声明(同样适用于import ... = require(...))我们就会意外地app变成一个模块.

所以,我们需要一个解决方法.暂时,返回services目录并添加一个新模块,这里称为weather-service.shim.ts

// services/weather-service.shim.ts

import weatherService from './weather-service.ts';

declare global {
  interface Window {
    services: {
      weatherService: typeof weatherService;
    };
  }
}
window.services.weatherService = weatherService;
Run Code Online (Sandbox Code Playgroud)

然后,app.ts改为

/// <reference path="./services/weather-service.shim.ts" />
namespace app {
  export async function main() {
    const dataForWeatherWidget = await services.weatherService.getRandom();
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,除非您需要,否则不应该这样做.尝试组织转换为模块,以尽量减少这种情况.

备注:

为了正确执行这种逐步迁移,必须准确理解什么是什么,什么不是模块.

这由每个文件的源级别的语言解析器确定.

解析ECMAScript文件时,有两个可能的目标符号,脚本模块.

https://tc39.github.io/ecma262/#sec-syntactic-grammar

5.1.4句法语法ECMAScript的句法语法在第11,12,13,14和15条中给出.该语法具有由词汇语法定义的ECMAScript标记作为其终结符号(5.1.2).它定义了一组产品,从两个可选的目标符号Script和Module开始,描述了令牌序列如何形成ECMAScript程序的语法正确的独立组件.当要将代码点流解析为ECMAScript脚本或模块时,首先通过重复应用词法语法将其转换为输入元素流; 然后,这个输入元素流由语法语法的单个应用程序解析.如果输入元素流中的标记不能被解析为目标非终结符(脚本或模块)的单个实例,并且没有剩余标记,则输入流在语法上是错误的.

挥手致意,脚本是全球性的.使用TypeScript 内部模块编写的代码始终属于此类别.

当且仅当包含一个或多个顶级或语句*时,源文件才是模块.TypeScript用于引用外部模块等源,但它们现在简称为模块,以匹配ECMAScript规范的术语.importexport

以下是脚本和模块的一些源示例,以及它们如何区分是微妙但明确定义的.

square.ts - > 脚本

// This is a Script
// `square` is attached to the global object.

function square(n: number) {
  return n ** 2;
}
Run Code Online (Sandbox Code Playgroud)

now.ts - > 脚本

// This is also a Script
// `now` is attached to the global object.
// `moment` is not imported but rather assumed to be available, attached to the global.

var now = moment();
Run Code Online (Sandbox Code Playgroud)

square.ts - > 模块

// This is a Module. It has an `export` that exports a named function, square.
// The global is not polluted and `square` must be imported for use in other modules.

export function square(n: number) {
  return n ** 2;
}
Run Code Online (Sandbox Code Playgroud)

bootstrap.ts - > 模块

// This is also a Module it has a top level `import` of moment. It exports nothing.
import moment from 'moment';

console.info('App started running at: ' + moment()); 
Run Code Online (Sandbox Code Playgroud)

bootstrap.ts - > 脚本

// This is a Script (global) it has no top level `import` or `export`.
// moment refers to a global variable

console.info('App started running at: ' + moment());
Run Code Online (Sandbox Code Playgroud)