[muzzik讨论]:如何更优雅的解决循环引用?

# 全局法

  • 每个模块都注册到全局,直接调用,最常用的
  • 每个模块注册到全局唯一键中,例如 cc 一样,更优雅也避免模块重名,缺点是必须注意模块间的调用和注册顺序有关,若模块A内调用了模块B,且是A模块先注册就有可能出现找不到模块B

# 合并法

将依赖的类全部放在一个模块内,缺点是代码臃肿,维护困难

# 间接法

假如a与b模块互相依赖,那么就将依赖的功能代码搬到c模块,c模块引用a和b,杜绝了循环依赖,缺点是增加使用成本,代码结构相对来说会越来越复杂。

# 动态加载法

把静态 import 修改为动态, 缺点是将跳过模板类型检查,以及不能 "立即使用"

  • 直接在使用处 import(“module_path”).default
  • 或者如下提前定义,免去每次手写路径
type ui_base<T1, T2> = (import("./ui_base").default<T1, T2>);
let ui_base: (typeof import("./ui_base").default);
// ------------------异步函数
// (async () => {
//     ui_base = (await import("./ui_base")).default;
// })();
// ------------------或者直接promise.then
import("./ui_base").then(module_a=> {
    ui_base = module_a.default;
});

目前本人已知解决循环引用的方法如上,随着项目增大循环引用是不可避免的,各位论坛大神有更好的办法吗?

5赞

最简单的办法,在使用时候才引入,而不是在文件一开头就引入,ezez,目测比你上述所有都强太多了

1赞

你需要使用cc.vv

你说的不就是动态加载?麻烦看完后再回复

1赞

通用渐进式游戏客户端开发框架:EasyGameFramework持续更新贴
可以了解一下

https://github.com/AILHC/EasyGameFrameworkOpen

egf-core
特性
极简、友好类型提示、零依赖

在使用AssetBundle时也能拥有友好的类型提示

  1. 基础使用
export class XXXLoader implements egf.IBootLoader {
    onBoot(app: egf.IApp, bootEnd: egf.BootEndCallback): void {
        app.loadModule(moduleIns,"moduleName");
        bootEnd(true);
    }

}

import { App } from "@ailhc/egf-core"
const app = new App();
//启动
app.bootstrap([new XXXLoader()]);
//初始化
app.init();
  1. 智能提示和接口提示
//智能提示的基础,可以在任意模块文件里进行声明比如
//ModuleA.ts
declare global {
    interface IModuleMap {
        moduleAName :IXXXModuleA
    }
}
//ModuleB.ts
declare global {
    interface IModuleMap {
        moduleBName :IXXXModuleB
    }
}

const app = new App<IModuleMap>();
//在运行时也可调用,这里的moduleIns可以是任意实例,moduleName可以有智能提示
app.loadModule(moduleIns,"moduleName");
  1. 全局模块调用
// 可以将模块实例的字典赋值给全局的对象,比如
//setModuleMap.ts
export let m: IModuleMap;
export function setModuleMap(moduleMap: IModuleMap) {
    m = moduleMap;
}
//AppMain.ts
import { setModuleMap, m } from "./ModuleMap";

...
setModuleMap(app.moduleMap); 
...
//在别的逻辑里可以通过
import { m } from "./ModuleMap";
//调用模块逻辑
m.moduleA.doSometing()

这里是对于循环依赖的问题讨论,请不要给自己的产品打广告

2赞

非常抱歉,我补充完整

你这种方案和我上面说的全局唯一键一样,并且还要手写declare,只适合用于框架模块,对于业务模块来说并不适合

不一样哦。你的注入cc或者注入任意一个window全局变量都是不安全的
而我的不一样:

这样全局都需要import { m } from “./ModuleMap”; 但依赖单一,可以避免循环依赖,而且在闭包内,可以避免控制台调用

可能你没看清楚,适用于任何模块

手写declare?只是在模块逻辑代码文件的顶部将模块的声明暴露到全局。
关于不同模块之间的依赖,可以通过模块加载周期函数优雅解决
比如
我有一个BagModule.ts

declare global {
    interface IModuleMap {
        bag: BagModule
    }
}
export class BagModule implements egf.IModule {
    key: string = "bag";
    onInit() {
        //所有模块实例注入之后
    }
    onAfterInit() {
        //所有模块初始化onInit调用之后
    }
    getItemById(id: number) {
        return { id: id, num: 2 };
    }
}
//HeroModule.ts
declare global {
    interface IModuleMap {
        hero: HeroModule
    }
}
export class HeroModule implements egf.IModule {
    key: string = "bag";
    
    onInit() {
        //所有模块实例注入之后

    }
    onAfterInit() {
        //所有模块初始化onInit调用之后
        const item = m.bag.getItemById(2);
    }
    getHeroById(id: number) {
        return { id: id, power: 1000000000000 };
    }
}

//注册
app.loadModule(new BagModule());
app.loadModule(new HeroModule());
//调用
app.moduleMap.bag.getItemById(1);
1赞

你这个注册的helloworld是什么?另外我指的全局唯一键只适合用于框架模块就是因为需要自己去手写declare,而业务模块是最容易增删改的,难道每修改一次就得再去修改一次declare,这就是你说的适合所有模块吗?这会极大的增加使用成本和开发时间

还有你说的加载周期是类似于动态加载再使用还是什么,不然不可能避免我上面说的两个模块互相引用又立即使用造成的找不到模块的情况

如果有更好的想法也可以探讨

我每次打包的是,都会在控制台有黄色的警告,提示我循环引用了。但是每次游戏都能运行的很正常。所以我从来没有管过这个事情。

我更新了
declare 如果贪图方便 每个模块文件只需写一次

关于两个模块相互引用又立即使用造成找不到模块的情况
可能你是指 ts的import 互相引用的情况。这种情况是编译器的问题(其实nodejs的commonjs的模块加载机制是不会出现这种循环引用的问题的)
我的方式是
在使用时,通过一些机制进行规避

每个模块都可以实现初始化周期函数,类似Cc的onLoad、onStart
比如HeroModule对BagModule有依赖,BagModule在Constructor函数或者onInit时进行初始化,而HeroModule 在onAfterInit进行对BagModule的依赖。

我之前理解错了,以为要写所有接口,我试试你这种方案

那就有可能是非立即使用造成的,非立即使用能正常运行,立即使用才会报错

其实对于框架模块,我是使用暴露接口

或者对于一些我不想暴露太多接口的模块,我选择写一个接口,然后暴露接口

这个可以看个人选择,按需处理。

我的方案并不是死的,可以灵活变通

比如模块注册,一定要集中注册吗?可以更加方便吗?

可以,使用修饰器

在Cocos中使用修饰器是非常方便的。因为Cocos在启动时就已经对所有脚本都执行了一次(可以触发修饰器)

在Laya就没那么方便了。需要在一个地方import那个脚本
比如

import {BagModule} from "xxxx"

BagModule

em,这个模块化方案经历了两个中大型Laya项目,到现在依然是挺舒服的

装饰器是很好用,我也写过注册某些特殊功能的类装饰器,对于你这个框架的动态加载使用还是避免不了立即使用的问题,有没有办法解决?还是说只能尽量杜绝?我想到的解决办法就是通过异步使用,但这实际上已经不是真正的立即使用了

我重新看了一下你描述

我所理解你想要的立刻使用

可能是这样

//A.ts
import {B} from "./B.ts"
export class A {
    doSometing(){
       const b = new B();
    }
}
//B.ts
import {A} from "./A.ts"
export class A {
    doSometing(){
       const a = new A();
    }
}

在我的设想中,使用egf-core的模块化机制

游戏业务逻辑肯定是在模块化初始化完成之后运行的

那么模块化是否可以注入模板类构造器呢?
好像是可以的

//A.ts
declare global {
    interface IModuleMap {
        A: typeof A
    }
}
export class A  {
    
    doSomething(){
        const b = new m.B();
    }
}
//B.ts
declare global {
    interface IModuleMap {
        B: typeof B
    }
}
export class B  {
    
    doSomething(){
        const a = new m.A();
    }
}
//注册
app.loadModule(A, "A");
app.loadModule(B, "B");

更新了declare

注册也可以用修饰器

虽然不能解决所有问题,但可极大的避免

类似于在导入后使用

// A.ts
import B form "…"

class A {
    public instance() {...}
    public log() {
        cc.log(B.test);
    }
}
module A {
    export let test = 0;
}
export default A;

// B.ts
import A from "…"

class B {...}
module B {
    export const test = A.test + 1;
    A.instance().log();
}
export default B;