请教关于 typescript 模块之间的循环依赖的问题是否有好的解决方法呢

那就让你的计算模块独立 不依赖其他模块

多的就不说了,AI问一下,基本就是答案。
但是这个答案它不好用,我也和你有过相同的问题。

我的解决方案是,不引入模块,直接将模块设置成全局变量。
小型项目完全没问题,方便高效还不会有引入问题。
大型项目就需要自行斟酌了。

假设,
你的NumberUtil 需要实现一些新的运算, 但这些新运算,可能由于某些原因, 已经在其他类里实现了,
如果你直接在NumberUtil里引用其他类, 其他类又引用一圈回到NumberUtil, 那基本上就GG了,

这种情况下, 你的NumberUtil应该算是一个业务无关,只提供运算的类, 他只能引用一些更基础的类, 例如 Int, Float ,
稍微复杂点, 如果想传入某些接口进行计算, 那接口要另外定义在一个Const文件里, 同样Const也不引用其他任何文件, 这样很大程度上可以避免循环引用

https://github.com/kaxifakl/XT/blob/main/XT/assets/xt/extern/log/log.ts
可以试下这种写法,注册到全局对象下,这样就不会有循环引用了

IA.ts
export interface IA {
test() : void;
}

A.ts
export class A extends implements IA {
test(){}
}

global类
class global {
a : IA;
}
在init.ts文件某个位置初始化,比如 global.a = new A,要用到A类功能的地方就global.a.test(),
保证init.ts绝对不会被循环依赖 就可以

面向接口+容器

目前确实是这么做的,如果发现循环依赖就把调用的接口的代码复制粘贴过来或者放入中间模块
因为也确实想不出更好的办法了

不过总觉得有很多重复代码略不好维护,如果发现哪里写的有点问题要挨个地方搜修改一遍
而塞中间模块,先在中间模块 各个Util 公共依赖的代码都在里面,也没有归类,找起来也略麻烦

大的团队可能会考虑便于不同人协作维护,模块设计上要解耦独立于项目环境也能运行,但个人的话总觉得方便点会更好些
所以才探讨下看看是否有更好的方法,如果没有也确实没办法

这个办法好像确实不错 :smile:

不要用注册到全局的方法,这个依靠脚本加载顺序,你在加载前使用绝对报错
你可以自己试试,比如 a.ts 和 b.ts,b 中注册全局在 a 使用

如果能保持一个全局方法文件(a*.ts)在顶层加载,后面的全局方法都挂载在这个文件申请的全局变量下,也是可以的

这是永久性的风险,反正这种方式已经被我淘汰了,我之前自己测试过所有模块导入方式

写个单例, 单例来中转所有引用, 其他地方调用这个单例的接口, 小项目最简单方便的方式了, AI给的方案多多少少都有过于复杂了

:grinning:这个办法好像可以借鉴

上面说的所有方式我都说过

我只能说最好的方式就是自己做好代码结构,确保父类不依赖子类,然后直接导入自己使用的类

至于工具类和框架则可以做成集合,不依赖外部模块

你最终是如何解决的呢?

注册全局确实是有弊端,依赖加载顺序,污染全局环境,所以才想探讨是否有更好的办法解决这类问题,毕竟import模块确实很好维护.

比如 StringUtil 里我写了可以便捷处理字符串格式的函数,这个函数比较通用,其他 Util 用了可以不必写大段的重复代码,但是用了又可能会循环依赖

所以也搜了很多建议,大多建议是在恰当时机对模块间依赖注入(init传入或者调函数时就传入) 或者把代码拆到中间通用模块里 或者每个模块都复斜体示例制粘贴一份这个函数

但是 依赖注入 需要自己维护每个模块初始化时机,规范但略不方便
拆到中间模块可能发现问题时要迁移代码,有的时候发现问题时 可能会连根拔起 要迁移走多个存在关联的Util 代码,而且中间模块代码也会各种模块用的函数都有,似乎也不大好
每个模块复制粘贴需要的代码确实可以让模块独立起来,但是如果发现有地方写的有纰漏就要搜索所有类似代码的地方修改,如果有的模块代码被修改的稍微有些变化可能会很不好搜

所以觉得这些办法虽然可能适用于多人协作的大型项目统一规范,但是对个人而言都有些不方便的地方,所以才会探讨是不是有更佳的办法解决这类问题呢?或者一般是如何解决这类问题的呢?

看了大家的回复很受教, fisharray 和 kaxifa 给出的方案两个结合一起再稍加修改 好像正好能解决 这个问题
可以既能 解决 开发新模块或扩展模块功能时发生 循环依赖 , 同时又能 避免写入全局 失去了模块之间的import 依赖关系 导致 代码运行流程 会受文件执行顺序的影响

我试试看这是否可行,果然问别人和问AI不同 ,还是有不一样的收获 :grin:

一旦两个或者多个功能需要被循环引用的时候,说明你要拆模块了。。。

拆一个独立的 不依赖其他文件的基础模块

大家给了非常多的建议和想法, 我稍微整理了一下做一下总结

模块设计 + import
好处:通过 import 加载关联模块,不依赖文件命名和加载顺序,不会因文件改名/混淆加载顺序改变无法正常运行
坏处:容易循环依赖,导致模块设计要尽可能独立,这可能导致想用其他模块现有功能只能复制粘贴,或者把代码写到一个可能混杂很多函数的中间模块里.想要模块清晰好用会考验设计,不然可能开发新模块或扩展已有模块功能时改变了依赖顺序关系 发生循环依赖,就不得不花精力修改

全局写入
好处:能避免循环依赖,一些人在typescript有模块这个机制的情况下还是选择要写全局大概就是不想费精力解决循环依赖,尤其是个人开发
缺点:强依赖文件的加载写入全局顺序,文件改名或者混淆名字后顺序变了会导致程序直接无法运行.cocos并没有能保障文件加载顺序的机制 (用A开头命名文件作为程序入口 import 其他模块控制这个做法很不靠谱,随时可能出问题),将游戏内所有文件都指定import顺序这个做法费时费力不通用,漏掉会成为隐患

最后我想了一种方法,既能保证模块不受文件加载顺序影响,又能避免循环依赖问题

我以 onLoad 为分界
onLoad 前注册的模块写入到一个 临时变量里 禁止写入 全局
onLoad 后 将 临时变量里 的全部模块注册写入全局

这样
onLoad 前没有任何写入全局变量的行为,走的完全是typescript模块的加载依赖机制,模块初始化用到其他模块的功能就必须import,这能保障游戏运行与文件加载顺序无关
onLoad 后所有模块写入全局,所有模块都可以全局访问到,调用函数依赖的模块 不需要 import,这样就能实现 ArrayUtil 的 sortRandomOrderBySeed 这种函数可以放心大胆的使用 RandomUtil 的封装好的功能, RandomUtil 的 choiceElemByWeight 这种函数可以放心大胆的使用 ArrayUtil 的封装好的功能,不需要互相大段复制对方的代码,也不需要改代码时逐个模块找代码去改

缺点就是因为所有模块都注册全局了,只能依赖cocos编辑器报错知晓哪个模块初始化依赖了哪个模块

至于 像 初始化 某个常量对象,比如初始化 EncryptKeysConst 时 import 了 EncryptUtil , EncryptUtil 用到了 ArrayUtil 的函数 这种情况, 由于 发生在 onLoad 前没有全局写入 , 所以 间接访问 ArrayUtil 会失败
这种情况 EncryptKeysConst 虽然没直接依赖 ArrayUtil,但是模块内间接用到了,这种我就封装一个临时写入 全局的函数 ,在 EncryptKeysConst 模块退出前再把临时全局的模块再删掉(不删下次临时写入或onLoad会有错误警告) .这样 模块加载完成后 全局环境还是没有被写入东西的 ,就不会影响到下一个模块的加载,也不会因为加载顺序变了 代码时好时坏

目前我试了下,这办法似乎可行,虽然有 依赖编辑器报错才知道模块初始化有依赖 这个明显缺点
但它免去模块用功能要复制粘贴其他模块代码,或者把各路代码都塞入一个中间模块 这种苦恼.自己封装好的函数却得考虑模块依赖关系不敢随便用, 或者设计不当 循环依赖 不得不花精力重新设计模块
也不用担心文件加载顺序变了程序直接无法运行,因为 onLoad前还是严格遵循 typescript 的模块加载机制,只是 onLoad 后做了解耦

不知道大家有没有其他好的想法

用全局变量耦合各个模块,还是能解决很多循环引用问题的。