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

时间:2017-05-17 16:01:00

标签: typescript module webpack amd ecmascript-harmony

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

文件BaseClass.ts

module my.module {
  export class BaseClass {
  }
}

文件ChildClass.ts

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

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

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

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

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

import * as my.module from ???;

或使用命名空间?

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

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

1 个答案:

答案 0 :(得分:6)

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

我在一个大型企业项目中完成了这项工作。这不好玩,但确实有效。

一些提示:

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

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

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

  4. 假设您有一个像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);
        }
      }
    }
    

    现在假设您只有另一个全局范围的命名空间文件,这次,我们可以轻松地将其分解为适当的模块,因为它不依赖于其自己的命名空间的任何其他成员。例如,我将使用一种服务,使用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意外转入模块 ,在我们准备好之前。

    所以,我们需要一个解决方法。暂时返回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;
    

    然后,将app.ts更改为

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

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

    <强>说明:

    为了正确执行这种渐进式迁移,必须准确理解什么是什么,什么不是模块。

    这取决于每个文件的源级别的语言解析器

    解析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的内部模块编写的代码始终属于此类别。

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

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

    square.ts - &gt; 脚本

    // This is a Script
    // `square` is attached to the global object.
    
    function square(n: number) {
      return n ** 2;
    }
    

    now.ts - &gt; 脚本

    // 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 - &gt; 模块

    // 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 - &gt; 模块

    // 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 - &gt; 脚本

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