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

Restructuring TypeScript internal modules to external modules

我有一个使用大型打字稿代码库的网站。所有类都在它们自己的文件中,并用这样的内部模块包装:

文件BaseClass.ts

module my.module {
  export class BaseClass {
  }
}

文件ChildClass.ts

module my.module {
  export ChildClass extends my.module.BaseClass  {
  }
}

所有文件都以适当的顺序(使用 ASP.NET 捆绑)使用脚本标签全局包含。

我想转向更现代的设置并使用 webpack。我希望我的模块语法使用任何新的 ECMASCRIPT 模块标准。但是有很多代码使用现有的 "module namespaces" 所以我想要一个支持这种类型代码的更新路径 -

let x = new my.module.ChildClass();

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

import * as my.module from ???;

或者使用命名空间?

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

由于 "module" 跨多个文件,我将如何完成此操作?真的,我想要完成的就是拥有一个命名空间,并摆脱全局脚本。

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

我在一个大型企业项目中做过这个。这并不好玩,但它奏效了。

一些小技巧:

  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()}` : '';
    return {
      get<T>(url, options): Promise<T> {
        return implementation.get(url + appendToUrl, {...options}).then(({data}) => <T>data);
      }
    };
  }
}

现在假设您只有另一个全局范围的命名空间文件,这一次,我们可以轻松地将它分解成一个合适的模块,因为它不依赖于其 own[=131= 的任何其他成员] 命名空间。例如,我将使用一项服务,该服务使用 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}`);
    }
  }
}

不,我们要将我们的 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}`);
  } 
}

常见陷阱和解决方法

如果我们需要从我们现有的全局命名空间之一引用这个新修改的代码的功能,就会出现问题

因为我们现在至少在部分代码中使用模块,所以我们有一个模块加载器或捆绑器在起作用(如果你为 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();
  }
}

并且,由于它不再有效,我们将此 损坏的 代码改为

// app.ts

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

namespace app {
  export async function main() {
    const dataForWeatherWidget = await weatherService.getRandom();
  }
}

上面的代码是错误的,因为,仅仅通过添加一个 import... from '...' 语句(同样适用于 import ... = require(...))我们已经将 app 变成了一个模块 ,在我们准备好之前。

所以,我们需要一个解决方法。暂时把return放到services目录下,新增一个Module,这里叫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;

然后,将app.ts改为

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

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

备注:

为了正确执行这种逐步迁移,重要的是准确理解什么定义了什么是模块,什么不是模块。

这是由每个 文件 的源代码级别的语言解析器确定的。

解析ECMAScript文件时,有两种可能的目标符号ScriptModule.

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

5.1.4The Syntactic Grammar The syntactic grammar for ECMAScript is given in clauses 11, 12, 13, 14, and 15. This grammar has ECMAScript tokens defined by the lexical grammar as its terminal symbols (5.1.2). It defines a set of productions, starting from two alternative goal symbols Script and Module, that describe how sequences of tokens form syntactically correct independent components of ECMAScript programs. When a stream of code points is to be parsed as an ECMAScript Script or Module, it is first converted to a stream of input elements by repeated application of the lexical grammar; this stream of input elements is then parsed by a single application of the syntactic grammar. The input stream is syntactically in error if the tokens in the stream of input elements cannot be parsed as a single instance of the goal nonterminal (Script or Module), with no tokens left over.

挥手,一个脚本是全局的。使用 TypeScript 的 内部模块 编写的代码始终属于此类。

当且仅当源文件包含一个或多个顶级 importexport 语句*时,它才是 模块。 TypeScript 曾经将此类来源称为 external modules,但现在它们被简称为 modules 以匹配 ECMAScript 规范的术语。

这里有一些脚本和模块的源代码示例。请注意,它们的区别是微妙而明确的。

square.ts --> 脚本

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

function square(n: number) {
  return n ** 2;
}

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();

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;
}

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()); 

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());