将一个大型Typescript项目转换为ES6模块

3
我有一个相当大的应用程序(~650个文件),目前实现了ES6模块和旧版全局命名空间内部模块和类的混合。
我想转向100%的ES6模块。
迭代方法:
为了达到这个目标,我需要先通过添加“export”关键字将所有全局对象转换为ES6模块。
一旦我添加了“export”,该全局对象就不再是全局的,使用该对象的每个文件都会出现编译器错误“找不到对象”(即Cannot read property 'xxx' of undefined)。
要解决这个问题,必须在文件中添加“import”,从而将其转换为ES6模块,而这反过来会“取消全局化”该文件中的所有对象,在其他文件中引起新的“对象未找到”错误。
简而言之,这种迭代方法似乎不会很好地工作。
可能的一步到位的解决方案:
1. 迭代所有ts文件
- 以编程方式向所有顶级类、模块和变量添加导出。 - 将它们收集到一个列表中。
2. 生成一个桶(globalBarrel)文件,重新导出在上一步中收集的所有导出。
3. 再次迭代所有ts文件
- 向每个ts文件添加`import {list, of, symbols, exported, by, the, barrel,file} from "./globalBarrel"`。
4. 编译干净?
- 很可能会有一些循环依赖性问题需要解决。
5. 在将来编辑每个文件时,使用vsCode的“Organize Imports”命令删除多余的导入。
正如@jfriend000所说:
将非模块化代码尝试以编程方式转换为模块化代码只会创建一堆麻烦。
但是,一次将所有内容都放入模块将使真正的模块化过程更加容易。
问题:
这是最好的方法吗?有什么建议吗?

1
趣闻:我以前做过类似规模的项目。最终,这个项目变成了一个1100行的脚本(用TS编写),它解析了800个文件(可以说是有主观意见的层次化正则表达式),构建了依赖树(幸运的是,当时使用的是Angular 1.0,所以有一个伪模块系统,但所有代码都在全局作用域中),生成了导出和导入语句,将代码转换为类型化对象(最初只是一堆匿名JS表达式),添加了数千个类型注解。经过几天的反复运行和回退,最终效果还不错。而且,这个过程也很有趣。 :) - Aaron Beall
2个回答

3
要达到这个目的,我需要首先通过添加"export"关键字将所有全局对象转换为ES6模块。
当我添加"export"时,该全局对象不再是全局的,使用该对象的每个文件现在都会出现编译器错误"找不到对象"(即无法读取未定义的属性'xxx')。
为了解决这个问题,必须在文件中添加一个"import",从而将其转换为ES6模块,这反过来又会使该文件中的所有对象"脱离全局",导致其他文件中出现新的"找不到对象"错误。
没错。避免这种恶性循环的技巧是,每次将文件转换为模块时,还要将该模块分配给一个全局变量,以便您可以从其他非模块文件中引用它,而无需将它们转换为模块。这需要一些样板代码;这个开放建议可以稍微减少样板代码。
我猜测你目前一定在使用某种捆绑工具来确保所有非模块文件都被加载并贡献给单个全局作用域,以便它们可以访问彼此的定义。首先创建一个包含以下代码的文件,并配置你的捆绑工具确保它首先运行:
namespace Modules {
    // Force `Modules` to be created at runtime.
    (() => {})();
}

现在如果你开始的是:
// a.ts
namespace A {
    export const a = 42;
}

// b.ts
namespace B {
    console.log(A.a);
}

你可以过渡到:
// a.ts
export const a = 42;

import * as A_ from "./a";
declare global {
    namespace Modules {
        export import A = A_;
    }
}
Modules.A = A_;

// b.ts
namespace B {
    console.log(Modules.A.a);
}

并且,b.ts 将能够访问 Modules.A 的完整类型信息,而无需自身成为一个模块(除了与您的捆绑器有关的加载顺序问题)。

2
这是最好的方法吗?有什么建议吗?
我怀疑这不是一个好方法,也肯定不是最好的方法。
将非模块化代码尝试通过编程方式转换成模块化代码只会制造混乱。我相信可以做到,但它并没有真正带来任何好处。它只是使事情变得比它们本来就已经混乱的更加混乱。你将把一个非模块化的架构硬塞到一堆非模块化、高度交叉依赖的模块中。你的架构不会得到改进,而代码现在会变得更加复杂,而不是更简单了。
更好的方法是逐步重新设计功能区域,以实际成为“模块化”。为某个功能区创建一个接口,将其打包成一个模块,然后将所有使用该功能的用户转换为导入接口并使用它。这可以逐个功能区域地完成。你不必一次性打破整个程序,然后再尝试把它们全部放在一起。
消除共享全局变量需要重新思考设计。将一堆共享全局变量打包成一个模块,然后让每个人都导入它,实际上并没有真正改善设计——事实上,它可能甚至会使事情变得更糟。代码与以前一样纠缠不清,相互依赖。
你可以逐步进行模块化重新设计,逐个功能区域地完成,而不是创建一个大混乱,实际上并没有改进你的代码架构。
如果我要解决这个问题,我可能会创建一个顶层图表,列出程序中所有主要功能点,并确保所有650个文件都在图表中有所体现。然后,我会开始逐步选择最容易模块化(具有最清晰接口和最少意大利面条式依赖关系)的部分。当你越来越多地将事物移到模块中时,剩下的部分就开始变得不那么复杂了。如果真的有访问全局变量散布在整个代码中,那么你真的必须重新思考那部分的设计,无论是创建包含数据的对象并传递它们,还是将原来的全局数据移动到相关模块中,并为该数据提供导出访问器。

正如我在问题中所提到的,我已经尝试逐步转换现有文件,但是当我将一个文件作为模块时,该文件的任何使用者本身都会成为一个模块,并且会引发“对象未找到”的级联错误。您能否说明如何避免这种情况发生? - bnieland
@bnieland - 你的执行环境是什么?浏览器?Node.js?你是否进行了打包? - jfriend00
这是一个跨浏览器产品(Chrome,FF,IE11,Edge)。至于捆绑,我使用Closure编译器将我的“全局命名空间”文件和现有模块文件生成一个文件。 - bnieland

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接