如何解决打字稿中的循环依赖

How to resolve circular dependencies in typescript

设置:

设想以下设置:

有一个 api,假设包含一个文件夹 foobar。这些文件夹将所有 public 内容导出到本地 index.ts,这将通过 export * from [...] 重新导出 public 内容以使其更方便。

在我的示例中,存在循环依赖,因为 foo.ts 需要 bar 的一部分,反之亦然 - 我完全理解为什么会这样.

见下方截图:

问题:

如何在具有数百个 类、函数、常量、类型、枚举等的环境中有效地使用 TypeScript 解决此问题? 我想我需要某种帮助文件来解决共性问题。

即使我创建了某种需要 foobarfoobar 文件夹,然后将所有内容导出到一个大的导出文件中,它也可能很快就会变得混乱。 如果我只需要 barfoo 怎么办?命名导出是否足够好?

我也想避免以后出现问题,所以我正在寻找一个健壮的解决方案。调用优先级不是我在这里尝试解决的主要问题。更多的是关于如何以一种聪明的方式设置依赖关系。

目标:

我想分别使用 foo 和 bar,它们应该能够彼此共享 functions/types/enums/interfaces 等。

可以在此处找到一个非常简单的代码片段:

codesandbox.io

你的例子似乎很笼统,所以我试着描述一些一般的经验法则,但它们可能有点自以为是。

  1. 如果 foobar 之间存在循环依赖,消除它的最简单方法是将所有循环依赖单元提取到单独的 module/directory 中,例如foobar。你提到了这个解决方案,但是依赖的方向应该是不同的。 foobar 都需要 foobar 来提供提取的共享代码。这样您仍然可以分别导入 foobar(暂时导入 foobar)。
  2. 这也适用于您提到的每个 functions/types/enums/interfaces - 如果某些东西同时被 foobar 使用,那么它应该放在单独的 module/directory 中。
  3. 关于像 functions/types/enums/interfaces 这样的名字——我会考虑使用这样的名字作为最后的手段(例如,作为目录树中叶子目录的名称),因为它们传达的信息通常是无关紧要的。更好的方法是围绕业务领域概念而不是与实现细节相关的名称来组织(共同定位)代码。这增加了代码的可发现性并使代码组织不那么脆弱。例如而不是
<sourcesRoot>/api/functions/getUser.ts
<sourcesRoot>/api/functions/getPosts.ts
<sourcesRoot>/api/enums/UserRole.ts
<sourcesRoot>/api/types/User.ts
<sourcesRoot>/api/types/Post.ts
<sourcesRoot>/api/index.ts

...考虑使用:

<sourcesRoot>/users/api/getUser.ts
<sourcesRoot>/users/model/User.ts
<sourcesRoot>/users/model/UserRole.ts
<sourcesRoot>/users/index.ts

<sourcesRoot>/posts/api/getPosts.ts
<sourcesRoot>/posts/model/Post.ts
<sourcesRoot>/posts/index.ts
  1. 保持 foobar 小。尝试从 foobar 中提取一些 sub-domains/areas/contexts 到单独的目录中。这应该会生成更小的 index.ts 文件。
  2. 这个可能是固执己见且不受欢迎的...请考虑避免使用桶形文件。在我看来,如果你发布一个库,桶文件会很好地工作。在这种情况下,您可以精确指定 public 成员(假设是 CommonJS 模块)。但是,在我看来,目录树中较低级别的桶文件并没有提供太多价值。您仍然可以直接导入模块(绕过桶文件)。它们使重构变慢,因为在许多情况下您需要手动更新桶文件。如果您忘记更新它们,它们有时会创建一个非常讨厌的循环依赖,在运行时以未定义值的形式出现。您可能拥有看起来更干净的导入路径,但实际上很少有人手动处理导入路径。这是 IDE.
  3. 的工作

对于命名的误解,我们深表歉意。不幸的是,我有机会在实际应用程序中看到类似的名称,并且以某种方式错误地假设您也想使用此约定。当谈到“最终要么是所有东西都堆积如山的大文件,要么是 super-tiny 个文件”。这是一个找到良好平衡的问题。我不介意很多专注于单一功能的小文件(js 模块)——这是一个标志,表明一个人已经从一些更大的用例中正确地提取了更小的职责。这会生成更易于理解、测试和维护的代码。大文件(js 模块)或大 classes/functions 通常是 SRP 损坏的标志。关于 sandbox.io 示例 - 我无法理解它,也不理解 helloworld 函数背后的意图。它们只是简单的函数,递归地相互调用(导致堆栈溢出)。最简单的重构是只使用共享函数,例如buildGreeting(msg1, msg2) 放置在 foobar 目录中。从 foo 目录导出 const world = 'world',从 bar 目录导出 const hello = 'hello',然后在其他同级目录中创建一个模块,调用如下:


import {hello} from '../foo'
import {word} from '../bar'

buildGreeting(hello, word);

但是,要说明对这个示例代码的任何有意义的改进是具有挑战性的,因为它没有说明任何实际用例。