十二、全局模块

当希望提供一组应该在所有地方开箱即用的提供者,可以使用@Global()装饰器将模块设置为全局

1
2
3
@Global()
@Module({})
export class XXXModule {}

目前的代码并没有实现隔离,所以即便不使用@Global(),模块也是全局的,现在咱们要处理这个问题,实现模块的隔离然后实现@Module装饰器

模块的隔离

第一步我们先对module.decorator进行修改,对所有的controllersproviders添加元数据,标明其属于哪一个模块

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
37
38
39
40
41
// @nestjs/common/module.decorator.ts

import 'reflect-metadata'

interface ModuleMetadata {
// 控制器
controllers?: Function[]
// providers
providers?: any[]
// 导入其他模块,可以使用导入模块中导出的providers
imports?: any[]
// 导出部分providers,供其他导入该模块的模块使用
exports?: any[]
}

// Module装饰器工厂函数
export function Module(metadata: ModuleMetadata): ClassDecorator {
return (target: Function) => {
// 给模块添加一个元数据进行标识,表明它是一个类
Reflect.defineMetadata('isModule', true, target)
// 给类添加元数据controllers,值为传入Module的controllers
Reflect.defineMetadata('controllers', metadata.controllers, target)
// 给控制器添加元数据nest-module, 表明属于哪个模块
defineModule(target, metadata.controllers ?? [])
// 给类添加元数据providers,值为要注入容器的所有provider
Reflect.defineMetadata('providers', metadata.providers, target)
// 给providers添加元数据nest-module, 表明属于哪个模块
defineModule(target, metadata.providers ?? [])
// 给类添加元数据imports,值为传入Module的imports
Reflect.defineMetadata('imports', metadata.imports, target)
// 给类添加元数据exports,值为要注入容器的所有exports
Reflect.defineMetadata('exports', metadata.exports, target)
}
}

export function defineModule(nestModule, targets) {
// 给每个元素添加一个元数据,标明每个controller/provider属于哪个模块
targets.forEach(target => {
Reflect.defineMetadata('nestModule', nestModule, target)
})
}

接下来还是在nest-application中进行逻辑处理

改动很多,首先移除掉之前的providers属性,添加了两个新的属性providerInstancesmoduleProviders,前者用来存放每个providertoken对应的实例,后者用来存放每个模块可以使用的provider

1
2
3
4
5
6
// @nestjs/core/nest-application.ts

// 此处保存所有的provider的示例,key是token,value是值
private readonly providerInstances = new Map()
// 记录每个模块里有哪些provider的token,不保存provider的值
private readonly moduleProviders = new Map()

后边都要用到模块来存储数据,所以要依次改动很多内容,首先是initProviders方法中,在调用registerProvidersFromModuleaddProvider两个方法的时候传入模块信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @nestjs/core/nest-application.ts

initProviders() {
// 拿到导入的模块
const imports = Reflect.getMetadata('imports', this.module) ?? []
// 遍历所有导入的模块
for (const importModule of imports) {
this.registerProvidersFromModule(importModule, this.module)
}
// 当前模块的providers也需要保存
const providers = Reflect.getMetadata('providers', this.module) ?? []
for (const provider of providers) {
this.addProvider(provider, this.module)
}
}

然后registerProvidersFromModule方法中,我们调用addProvider方法的时候,要给当前模块和引入当前模块的模块都执行一次,保证他们都可以使用这些providers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// @nestjs/core/nest-application.ts

private registerProvidersFromModule(importModule, ...parentModules) {
// 拿到导入模块的providers
const importedProviders = Reflect.getMetadata('providers', importModule) ?? []
// 拿到导入模块的exports
const exports = Reflect.getMetadata('exports', importModule) ?? []
// 根据providers和exports进行过滤,把需要的provider保存起来
for (const exportToken of exports) {
// exports中可能是provider,也可能是一个module
if (this.isModule(exportToken)) {
// 这里需要递归,因为重新导出的模块可能也导出了其他模块
this.registerProvidersFromModule(exportToken, ...parentModules)
} else {
const provider = importedProviders.find(pro => pro === exportToken || pro.provide === exportToken)
if (provider) {
// 当前模块以及引入当前模块的模块,都可以使用这个provider
[importModule, ...parentModules].forEach(m => {
this.addProvider(provider, m)
})
}
}
}
}

addProvider方法中,主要是加了一些新的逻辑,provider数据要存储到新的属性上,并且将模块的隔离数据也存储起来

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
37
38
39
40
41
42
43
44
45
46
47
48
// @nestjs/core/nest-application.ts

addProvider(provider, nestModule) {
// 需要处理模块间的隔离
// providers用来存储这个模块对应的provider的token
// 如果moduleProviders没存储当前模块,则创建一个放进去
const providers = this.moduleProviders.get(nestModule) || new Set()
if (!this.moduleProviders.has(nestModule)) {
this.moduleProviders.set(nestModule, providers)
}
// 如果已经有了就直接保存后返回,避免循环依赖
const injectToken = provider?.provide ?? provider
if (this.providerInstances.has(injectToken)) {
providers.add(injectToken)
return
}
if (provider.provide && provider.useClass) {
// 提供的是一个类
// 依赖项可能有其他依赖,需要递归instance的依赖
const dependencies = this.resolveDependencies(provider.useClass)
// 创建实例
const instance = new provider.useClass(...dependencies)
// 保存到providers
this.providerInstances.set(provider.provide, instance)
providers.add(provider.provide)
} else if (provider.provide && provider.useValue) {
// 提供的是一个值
this.providerInstances.set(provider.provide, provider.useValue)
providers.add(provider.provide)
} else if (provider.provide && provider.useFactory) {
// 提供的是一个函数,我们保存函数执行结果
// 拿到要注入的参数,有可能是一个其他的token
const inject = provider.inject ?? [].map(token => {
// 找到对应token则返回对应值,否则是一个常量,直接返回
return this.providerInstances.get(token) ?? token
})

// 注入参数并保存
this.providerInstances.set(provider.provide, provider.useFactory(...inject))
providers.add(provider.provide)
} else {
// 直接传的类名
// 递归依赖
const dependencies = this.resolveDependencies(provider)
this.providerInstances.set(provider, new provider(...dependencies))
providers.add(provider)
}
}

除了这些,我们还有一个方法,就是resolveDependecies解析依赖的服务,这里也需要做改动,如果当前模块存在这个服务才可以注入进去,也就是隔离的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// @nestjs/core/nest-application.ts

// 解析依赖的服务
private resolveDependencies(constructor) {
// 获取构造函数中通过@Inject注入的内容 - 可能只有部分参数是@Inject装饰的
const injectTokens = Reflect.getMetadata('injectTokens', constructor) ?? []

// 通过design:paramtypes获取到构造函数的参数列表类型
const paramtypes = Reflect.getMetadata('design:paramtypes', constructor) ?? []

// 做一个映射,把对应的实例返回,用于注入
return paramtypes.map((param, index) => {
// 拿到所属模块
const nestModule = Reflect.getMetadata('nestModule', constructor)
// paramtypes是构造函数的所有参数,我们在遍历的过程中,根据每一个下标取injectTokens中去找,找到了就用@Inject的值,否则就使用当前位置参数对应的类
const token = injectTokens[index] ?? param
// 从当前模块中找是否有当前的provider,有则返回对应实例
if (this.moduleProviders.get(nestModule)?.has(token)) {
return this.providerInstances.get(token)
}
// 当前模块没有存储对应provider则不能使用,抛出错误
// throw new Error('')
})
}

@Global

接下来我们实现@Global()装饰器,新建global.decorator文件,记得导出

1
2
3
4
5
6
7
8
9
10
// @nestjs/common/global.decorator.ts

import 'reflect-metadata'

// @Global装饰器
export function Global() {
return (target: Function) => {
Reflect.defineMetadata('global', true, target)
}
}

接下来还是在nest-application中修改逻辑

首先添加一个新的属性:

1
2
3
4
// @nestjs/core/nest-application.ts

// 记录全局可用的providers
private readonly globalProviders = new Set()

registerProvidersFromModule方法中,拿到是否是全局模块并传入addProvider方法中

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
// @nestjs/core/nest-application.ts

private registerProvidersFromModule(importModule, ...parentModules) {
// 获取是否是全局模块
const isGlobal = Reflect.getMetadata('global', importModule)
// 拿到导入模块的providers
const importedProviders = Reflect.getMetadata('providers', importModule) ?? []
// 拿到导入模块的exports
const exports = Reflect.getMetadata('exports', importModule) ?? []
// 根据providers和exports进行过滤,把需要的provider保存起来
for (const exportToken of exports) {
// exports中可能是provider,也可能是一个module
if (this.isModule(exportToken)) {
// 这里需要递归,因为重新导出的模块可能也导出了其他模块
this.registerProvidersFromModule(exportToken, ...parentModules)
} else {
const provider = importedProviders.find(pro => pro === exportToken || pro.provide === exportToken)
if (provider) {
// 当前模块以及引入当前模块的模块,都可以使用这个provider
[importModule, ...parentModules].forEach(m => {
this.addProvider(provider, m, isGlobal)
})
}
}
}
}

addProvider中根据是否是全局模块,使用不同的属性来存储数据

1
2
3
4
5
6
7
8
9
10
// @nestjs/core/nest-application.ts

addProvider(provider, nestModule, isGlobal = false) {
// 需要处理模块间的隔离
// providers用来存储这个模块对应的provider的token
// 如果是全局模块,就使用对应的Set
// 否则,如果moduleProviders没存储当前模块,则创建一个放进去
const providers = isGlobal ? this.globalProviders : (this.moduleProviders.get(nestModule) || new Set())
// ...其他不变
}

还有就是resolveDependencies方法中也要进行修改,不仅当前模块的provider可以使用,全局模块的provider也可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// @nestjs/core/nest-application.ts

private resolveDependencies(constructor) {
// 获取构造函数中通过@Inject注入的内容 - 可能只有部分参数是@Inject装饰的
const injectTokens = Reflect.getMetadata('injectTokens', constructor) ?? []

// 通过design:paramtypes获取到构造函数的参数列表类型
const paramtypes = Reflect.getMetadata('design:paramtypes', constructor) ?? []

// 做一个映射,把对应的实例返回,用于注入
return paramtypes.map((param, index) => {
// 拿到所属模块
const nestModule = Reflect.getMetadata('nestModule', constructor)
// paramtypes是构造函数的所有参数,我们在遍历的过程中,根据每一个下标取injectTokens中去找,找到了就用@Inject的值,否则就使用当前位置参数对应的类
const token = injectTokens[index] ?? param
// 从当前模块中找是否有当前的provider,有则返回对应实例
// 或者全局模块中有这个provider也可以返回
~ if (this.moduleProviders.get(nestModule)?.has(token) || this.globalProviders.has(token)) {
return this.providerInstances.get(token)
}
return null
})
}

至此,我们完成了全局模块的功能。

作者

胡兆磊

发布于

2025-06-08

更新于

2025-05-06

许可协议