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

vfh0ocws  于 2023-04-21  发布在  Webpack
关注(0)|答案(1)|浏览(138)

我有一个网站,它使用了一个大型的typescript代码库。所有的clases都在自己的文件中,并用一个内部模块 Package ,如下所示:
文件BaseClass.ts

module my.module {
  export class BaseClass {
  }
}

文件ChildClass.ts

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

所有文件都以适当的顺序(使用ASP.NET捆绑包)全局包含脚本标记。
我想移动到一个更现代的设置和使用webpack.我想我的模块语法使用任何新的ECMASCRIPT模块标准是.但有很多代码使用现有的“模块命名空间”,所以我想一个更新路径,将支持这种类型的代码-

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

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

import * as my.module from ???;

还是使用名称空间?
然而,如果这不是最佳实践,我想坚持最佳实践。内部模块目前对组织不同的应用程序层和服务非常有帮助...
既然“模块”是跨多个文件的,我该如何实现这一点呢?实际上,我所要做的就是拥有一个名称空间,并摆脱全局脚本。

bvjveswy

bvjveswy1#

免责声明(这不是一个全面的指南,而是一个概念性的起点。我希望证明迁移的可行性,但最终它涉及到相当多的艰苦工作)
我曾经在一个大型企业项目中这样做过,虽然不好玩,但很有效。
一些提示:
1.只要您需要,就只保留全局名称空间对象。
1.从源代码的叶子开始,将没有依赖项的文件转换为外部模块。
1.尽管这些文件本身依赖于您一直使用的全局名称空间对象,但如果您从外到内仔细工作,这将不会成为问题。
假设您有一个像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);
      }
    };
  }
}

现在假设你有另一个全局范围的命名空间文件,这一次,我们可以很容易地将它分解成一个适当的模块,因为它不依赖于它自己的命名空间的任何其他成员。例如,我将使用一个服务,使用来自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编写,即express应用程序,您可以忽略这一点,因为平台集成了加载器,但您也可以使用自定义加载器)。该模块加载器或打包器可以是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意外地转换为模块。
因此,我们需要一个变通方法。

// 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.es/ecma262/multipage/notational-conventions.html#sec-syntactic-grammar

5.1.4句法语法

ECMAScript的 * 句法语法 * 在第13至16节中给出。该语法将由词法语法定义的ECMAScript标记作为其终端符号(5.1.2)。它定义了一组产生式,从两个可选的目标符号Script和Module开始,描述标记序列如何形成ECMAScript程序的语法正确的独立组件。
当代码点流被解析为ECMAScript脚本或模块时,首先通过重复应用词法语法将其转换为输入元素流;如果输入元素流中的标记不能被解析为目标非终结符(脚本或模块)的单个示例而没有标记剩余,则输入流在语法上是错误的。
手动地,Script 是全局的。使用TypeScript的 * 内部模块 * 编写的代码总是福尔斯这一类。
当且仅当源文件包含一个或多个顶级importexport语句 * 时,它才是一个 Module。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());

相关问题