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

标签 typescript module webpack amd ecmascript-harmony

我有一个使用大型 typescript 代码库的网站。所有类都在它们自己的文件中,并用内部模块包装,如下所示:

文件 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 ???;

还是使用命名空间?

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

由于“模块”跨越许多文件,我将如何实现这一点?真的,我想要完成的只是拥有一个命名空间,并远离全局脚本。

最佳答案

免责声明(这不是一个全面的指南,而是一个概念的起点。我希望证明迁移的可行性,但最终它涉及大量的艰苦工作)

我已经在一个大型企业项目中做到了这一点。这并不有趣,但它奏效了。

一些技巧:

  • 仅在需要时保留全局命名空间对象。
  • 从源代码的叶子开始,将没有依赖项的文件转换为外部模块。
  • 尽管这些文件本身将依赖于您一直在使用的全局命名空间对象,但如果您从外到内小心地工作,这将不是问题。

  • 假设您有一个全局命名空间,例如 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 编写,即快速应用程序,您可以忽略它,因为平台集成了一个加载器,但您也可以使用自定义加载器)。那个模块加载器或打包器可以是 SystemJS、RequireJS、Webpack、Browserify 或更深奥的东西。

    最大也是最常见的错误是有这样的错误
    // app.ts
    
    /// <reference path="./services/weather-service.ts" />
    namespace app {
      export async function main() {
        const dataForWeatherWidget = await services.weatherService.getRandom();
      }
    }
    

    而且,由于这不再有效,我们写了这个 splinter 代码代替
    // 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 文件时,有两个可能的目标符号,Script 和 Module。

    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 的内部模块编写的代码总是属于这一类。

    当且仅当源文件包含一个或多个顶级 import 时,源文件才是模块。或 export声明*。 TypeScript 曾经将此类源称为外部模块,但现在为了匹配 ECMAScript 规范的术语,它们被简称为模块。

    以下是一些脚本和模块的源示例。请注意,如何区分它们是微妙但明确的。

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

    现在.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());
    

    关于typescript - 将 TypeScript 内部模块重构为外部模块,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44029788/

    相关文章:

    Angular:在与 ngModel 一起使用时,在 select 的选项上选择不能正常工作

    javascript - 用户完成输入时的 Angular2 控件验证

    python - 运行 setuptools 测试时访问包的 `__init__.py` 中定义的名称

    python - 创建 Python 子模块

    linux - 升级特定的 Linux 内核子系统?

    javascript - 在 webpack 上,webpackJsonp() 函数接受的参数是什么以及它的作用是什么?

    webpack - 具有多个入口点的动态库选项

    javascript - 使用 Typescript 从元组中选择

    javascript - Angular2 rc1,shims_for_IE 在哪里

    javascript - Heroku 无法使用 "npm start"启动应用程序