将TypeScript内部模块重组为外部模块 [英] Restructuring TypeScript internal modules to external modules

查看:96
本文介绍了将TypeScript内部模块重组为外部模块的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a website that uses a large typescript code base. All clases as in their own files, and wrapped with an internal module like so:

BaseClass.ts文件

file BaseClass.ts

module my.module {
  export class BaseClass {
  }
}

文件ChildClass.ts

file ChildClass.ts

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

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

All file are included globally with script tags, in the appropriate order (using ASP.NET Bundling).

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

I would like to move to a more modern setup and use webpack. I would like my module syntax to use whatever the new ECMASCRIPT module standard is. But there is much code using the existing "module namespaces" so I would like an update path that will support this type of code -

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

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

So I think I need to have something like this -

import * as my.module from ???;

还是使用名称空间?

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

However, if that is not best practices, I would like to stick with best practices. The internal modules are currently very helpful for organizing the different application layers and services...

由于模块"跨越许多文件,我将如何实现?真的,我要做的就是拥有一个命名空间,并摆脱全局脚本的影响.

How would I accomplish this since the "module" is across many files? Really, all I am trying to accomplish is to have a namespace, and get away from global scripts.

推荐答案

免责声明(这并不是作为全面的指南,而是作为概念上的起点.我希望证明迁移的可行性,但最终涉及一个公平的问题.努力的工作量

Disclaimer (this is not meant as a comprehensive guide but rather as a conceptual starting point. I hope to demonstrate the feasibility of migration, but ultimately it involves a fair amount of hard work)

我已经在一个大型企业项目中做到了这一点.不好玩,但是行得通.

I've done this in a large enterprise project. It was not fun, but it worked.

一些提示:

  1. 仅在需要它们的时间范围内保留全局命名空间对象.

  1. Only keep the global namespace object(s) around for as long as you need them.

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

Start at the leaves of your source, converting files which have no dependents into external modules.

尽管这些文件本身将依赖于您一直在使用的全局名称空间对象,但是如果您从外部进行仔细的工作,这将不是问题.

Although these files will themselves rely on the global namespace object(s) you have been using, this will not be a problem if you work carefully from the outside in.

假设您有一个像utils这样的全局名称空间,它按如下方式分布在3个文件中

Say you have a global namespace like utils and it is spread across 3 files as follows

// 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 名称空间的任何其他成员.例如,我将使用一项服务,该服务使用utils中的内容在全球随机位置查询天气信息.

Now imagine you have another globally scoped namespaced file only, this time, we can easily break it out into a proper module because it does not depend on any other members of its own namespace. For example I will use a service that queries for weather info at random locations around the globe using the stuff from 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全局命名空间常量转换为适当的外部模块,在这种情况下,这将非常容易

No we are going to turn our services.weatherSercice global, namespaced constant into a proper external module and it will be fairly easy in this case

// 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}`);
  } 
}

常见陷阱和解决方法:

如果我们需要从我们现有的全局命名空间之一中引用此新调制的代码的功能,则可能会遇到障碍

A snag can occur if we need to reference the functionality of this newly modulified code from one of our existing global namespaces

由于我们现在至少在部分代码中使用了模块,因此我们正在使用模块加载器或捆绑程序(如果您是为NodeJS编写的,即快速应用程序,则可以在平台集成了加载器的情况下忽略它,但是您还可以使用自定义加载程序).该模块加载器或捆绑器可以是SystemJS,RequireJS,Webpack,Browserify或其他更深奥的东西.

Since we are now using modules for at least some part of our code, we have a module loader or bundler in play (If you writing for NodeJS, i.e an express application you can disregard this as the platform integrates a loader, but you can also use a custom loader). That module loader or bundler could be SystemJS, RequireJS, Webpack, Browserify, or something more esoteric.

最大,最常见的错误是遇到这样的事情

The biggest, and most common mistake is to have something like this

// app.ts

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

而且,由于它不再起作用,因此我们改为编写此 代码

And, as that no longer works, we write this broken code instead

// app.ts

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

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

上面的代码被破坏了,因为只需添加import... from '...'语句(对import ... = require(...)同样适用),我们就可以在准备好之前将app意外地转换为模块 .

The above code is broken because, simply by adding an import... from '...' statement (the same applies to import ... = require(...)) we have turned app into a module accidentally, before we were ready.

因此,我们需要一种解决方法.暂时返回到services目录并添加一个新的 Module ,此处称为weather-service.shim.ts

So, we need a workaround. Temporarily, return to the services directory and add a new Module, here called 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();
  }
}

请注意,除非您需要这样做,否则不要这样做.尝试组织您到模块的转换,以最大程度地减少这种情况.

Note, this should not be done unless you need to. Try to organize you conversion to modules so as to minimize this.

备注:

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

In order to correctly perform this gradual migration it is important to understand precisely what defines what is and what is not a module.

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

This is determined by language parsers at the source level for each file.

解析ECMAScript文件时,有两个可能的目标符号 Script Module .

When an ECMAScript file is parsed, there are two possible goal symbols, Script and Module.

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

5.1.4语法语法 第11、12、13、14和15节给出了ECMAScript的语法语法.此语法具有由词法语法定义的ECMAScript标记作为其终端符号(5.1.2).它从两个替代的目标符号脚本"和模块"开始定义了一组产品,这些产品描述了令牌序列如何形成语法正确的ECMAScript程序独立组件. 当将代码点流解析为ECMAScript脚本或模块时,首先通过重复应用词法语法将其转换为输入元素流.然后,通过语法的单个应用程序来解析此输入元素流.如果无法将输入元素流中的标记解析为目标非终端(脚本或模块)的单个实例,而没有标记,则输入流在语法上是错误的.

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

Hand-wavingly, a Script is a global. Code written using TypeScript's internal modules, always falls into this category.

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

A source file is a Module when and only when it contains one or more top level import or export statements*. TypeScript used to refer to such sources as external modules but they are now known simply as modules in order to match the ECMAScript specification's terminology.

以下是脚本和模块的一些源示例.请注意,它们的区分方式很细微但定义明确.

Here are some source examples of scripts and modules. Note that how they are differentiated is subtle yet well-defined.

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

这篇关于将TypeScript内部模块重组为外部模块的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆