目前项目里,因为前期的代码没有分层,底层和上层的代码相互import,出现了循环依赖,最终导致构建失败。

最好的解决方案当然时严格执行分层。TSLint里的import-patterns配置项,能控制一个文件只能从何处引入其他模块:

1
2
3
4
5
6
7
8
9
10
11
{
"import-patterns": [
true,
{
"target": "**/vs/base/common/**",
"restrictions": [
"vs/nls",
"**/vs/base/common/**"
]
},
}

比如👆摘录自VSCode的配置,它约束了基础模块目录下文件不能从目录外import。

但是考虑项目基本成形,而且缺乏各种单元测试集成测试,实在没有信心对现成代码大规模重构。所以我想的办法是,把被相互依赖的实例,挂到一个全局对象,类似早期前端开发的做法:

1
2
3
let ioc = {}
ioc.serviceA = {}
ioc.serviceB = {}

这样能解决文件级别的相互依赖,但是不能解决代码逻辑级别的依赖,比如两个类的方法会相互调用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let ioc = {}

class ServiceA {
foo() {
ioc.serviceB.bar()
}
}

class ServiceB {
bar() {
ioc.serviceA.foo()
}
}
ioc.serviceA = new ServiceA()
ioc.serviceB = new ServiceB()

可见,在挂载到依赖注入容器时,如果挂载的是实例,就会有👆的问题。InversifyJS是使用实例来注册依赖的

1
2
3
const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);

所以这个库也碰到循环依赖的问题。

如果在注册依赖时,注册的时构造函数,而不是实例,在获取依赖时再去初始化,应该就能解决循环依赖的问题。而且InversifyJS也好,VSCode也好,他们注册或注入依赖都受限于TypeScript写法,不太简洁。如果单纯考虑在JavaScript使用,依赖注册可以做成把构造函数赋值给依赖容器的某个属性名,依赖注入就是返回实例,如果没有初始化的,要初始化实例。刚开始时我用了getter/setter,其实使用Proxy机制,可以做到属性拦截,让依赖注入用起来更简洁,最后得到的依赖注入服务的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// module InistanceService
class InistanceService {
constructor() {
this.serviceCtors = {};
this.services = {};
}

get(name) {
let serv = this.services[name];
let Ctor = this.serviceCtors[name];
if (!serv && !Ctor) {
console.warn('[ioc]: Could not resolve ', name);
return null;
}

if (serv) return serv;
let instance = new Ctor();
this.services[name] = instance;
return instance;
}
}

const instance = new InistanceService();

const handler = {
get(target, name) {
if (target[name]) return target[name];
return target.get(name);
},
set(obj, prop, value) {
obj.set(prop, value);
return true;
}
};

export const ioc = new Proxy(instance, handler);