이 글은 여러분에게 Node 백엔드 프레임워크 Nest.js를 이해하고 Nestjs 모듈 메커니즘의 개념과 구현 원리를 소개하는 데 도움이 될 것입니다.
Nest는 모듈 메커니즘을 제공합니다. 모듈 데코레이터에서 공급자, 가져오기, 내보내기 및 공급자 생성자를 정의하여 종속성 주입이 완료되고 전체 애플리케이션의 개발이 모듈 트리를 통해 구성됩니다. 프레임워크 자체의 규칙에 따라 애플리케이션을 직접 실행하는 데에는 전혀 문제가 없습니다. 그러나 저는 프레임워크에서 선언한 종속성 주입, 제어 반전, 모듈, 공급자, 메타데이터, 관련 데코레이터 등에 대한 명확하고 체계적인 이해가 부족하다고 느낍니다.
- 제어 반전이 필요한 이유는 무엇인가요?
- 의존성 주입이란 무엇인가요?
- 데코레이터는 어떤 일을 하나요?
- 모듈(@Module)에서 공급자, 가져오기, 내보내기의 구현 원칙은 무엇인가요?
이해하고 감사할 수 있을 것 같은데, 처음부터 명확하게 설명하자면, 명확하게 설명할 수는 없습니다. 그래서 좀 조사를 해보니 이런 글이 나오더군요. 이제부터 처음부터 시작하여 본문을 입력해 보겠습니다.
1.1 Express, Koa
언어의 개발 과정과 기술 커뮤니티는 나무의 뿌리가 천천히 가지로 자라는 것처럼 바닥부터 위쪽으로 기능을 점차 풍부하게 하고 발전해야 합니다. 잎이 다시 자라는 과정. 이전에는 Nodejs가 Express 및 Koa와 같은 기본 웹 서비스 프레임워크와 함께 등장했습니다. 매우 기본적인 서비스 기능을 제공할 수 있습니다. 이러한 프레임워크를 기반으로 커뮤니티에서는 수많은 미들웨어와 플러그인이 탄생하기 시작했고, 프레임워크에 대한 더욱 풍부한 서비스를 제공하게 되었습니다. 우리는 애플리케이션 종속성을 구성하고 애플리케이션 스캐폴딩을 직접 구축해야 하는데, 이는 유연하고 번거로우며 특정 작업 부하도 필요합니다.
나중에 개발되면서 보다 효율적인 생산과 보다 통일된 규칙을 갖춘 일부 프레임워크가 탄생하여 새로운 단계를 시작했습니다.
1.2 EggJs, Nestjs
빠른 생산 애플리케이션에 더 잘 적응하고, 표준을 통일하고 즉시 사용할 수 있도록 하기 위해 EggJs, NestJs, Midway와 같은 프레임워크가 개발되었습니다. 이러한 유형의 프레임워크는 기본 라이프사이클을 구현하여 애플리케이션 구현을 보편적이고 확장 가능한 프로세스로 추상화합니다. 애플리케이션을 보다 간단하게 구현하려면 프레임워크에서 제공하는 구성 방법만 따르면 됩니다. 프레임워크는 프로그램의 프로세스 제어를 구현하며, 부품을 적절한 위치에 조립하기만 하면 됩니다. 이는 조립 라인 작업과 비슷해 보입니다. 각 프로세스가 명확하게 구분되어 구현 비용이 많이 절감됩니다.
1.3 요약
위의 두 단계는 단지 예시일 뿐입니다. 프레임워크 업그레이드를 통해 생산 효율성이 향상된다는 점을 대략적으로 이해할 수 있습니다. 제어 반전, 종속성 주입 및 메타프로그래밍의 개념이 Nest에 등장했습니다. 아래에서 이에 대해 이야기해 보겠습니다.
2.1 종속성 주입
애플리케이션은 실제로 서로 호출하여 애플리케이션의 모든 기능을 구현하는 많은 추상 클래스입니다. 애플리케이션 코드와 기능의 복잡성이 증가함에 따라 클래스가 점점 더 많아지고 이들 간의 관계가 점점 더 복잡해지기 때문에 프로젝트를 유지 관리하기가 점점 더 어려워질 것입니다.
예를 들어 Koa를 사용하여 애플리케이션을 개발하는 경우 Koa 자체는 주로 기본 웹 서비스 기능 세트를 구현합니다. 애플리케이션을 구현하는 과정에서 많은 클래스, 이러한 클래스의 인스턴스화 방법 및 상호 의존성을 정의합니다. 관계는 우리의 코드 로직에 의해 자유롭게 구성되고 제어됩니다. 각 클래스의 인스턴스화는 우리가 수동으로 새로 수행하는 작업이며 클래스가 한 번만 인스턴스화되고 공유되는지 아니면 매번 인스턴스화되는지 제어할 수 있습니다. 다음 클래스 B는 A에 의존합니다. B가 인스턴스화될 때마다 A는 한 번 인스턴스화되므로 각 인스턴스 B에 대해 A는 공유되지 않는 인스턴스입니다.
class A{} // B class B{ contructor(){ this.a = new A(); } }
아래 C는 얻은 외부 인스턴스이므로 여러 C 인스턴스가 app.a 인스턴스를 공유합니다.
class A{} // C const app = {}; app.a = new A(); class C{ contructor(){ this.a = app.a; } }
다음 D는 생성자 매개변수를 통해 전달됩니다. 매번 비공유 인스턴스를 전달하거나 공유 app.a 인스턴스(D 및 F 공유 app.a)를 전달할 수 있습니다. 이제 매개변수 메서드이므로 X 클래스 인스턴스를 전달할 수도 있습니다.
class A{} class X{} // D const app = {}; app.a = new A(); class D{ contructor(a){ this.a = a; } } class F{ contructor(a){ this.a = a; } } new D(app.a) new F(app.a) new D(new X())
이 방법은 B가 의존하는 A를 값을 전달하여 B에 주입하는 종속성 주입입니다. 생성자를 통한 주입(값 전달)은 하나의 구현 방법일 뿐입니다. 또한 외부 종속성을 내부 종속성으로 전달할 수 있는 한 set 메서드 호출이나 다른 메서드를 구현하여 전달할 수도 있습니다. 정말 간단합니다.
class A{} // D class D{ setDep(a){ this.a = a; } } const d = new D() d.setDep(new A())
2.2 올인원 주입?
随着迭代进行,出现了 B 根据不同的前置条件依赖会发生变化。比如,前置条件一 this.a
需要传入 A 的实例,前置条件二this.a
需要传入 X 的实例。这个时候,我们就会开始做实际的抽象了。我们就会改造成上面 D 这样依赖注入的方式。
初期,我们在实现应用的时候,在满足当时需求的情况下,就会实现出 B 和 C 类的写法,这本身也没有什么问题,项目迭代了几年之后,都不一定会动这部分代码。我们要是去考虑后期扩展什么的,是会影响开发效率的,而且不一定派的上用场。所以大部分时候,我们都是遇到需要抽象的场景,再对部分代码做抽象改造。
// 改造前 class B{ contructor(){ this.a = new A(); } } new B() // 改造后 class D{ contructor(a){ this.a = a; } } new D(new A()) new D(new X())
按照目前的开发模式,CBD三种类都会存在,B 和 C有一定的几率发展成为 D,每次升级 D 的抽象过程,我们会需要重构代码,这是一种实现成本。
这里举这个例子是想说明,在一个没有任何约束或者规定的开发模式下。我们是可以自由的写代码来达到各种类与类之间依赖控制。在一个完全开放的环境里,是非常自由的,这是一个刀耕火种的原始时代。由于没有一个固定的代码开发模式,没有一个最高行动纲领,随着不同开发人员的介入或者说同一个开发者不同时间段写代码的差别,代码在增长的过程中,依赖关系会变得非常不清晰,该共享的实例可能被多次实例化,浪费内存。从代码中,很难看清楚一个完整的依赖关系结构,代码可能会变得非常难以维护。
那我们每定义一个类,都按照依赖注入的方式来写,都写成 D 这样的,那 C 和 B 的抽象过程就被提前了,这样后期扩展也比较方便,减少了改造成本。所以把这叫All in 依赖注入
,也就是我们所有依赖都通过依赖注入的方式实现。
可这样前期的实现成本又变高了,很难在团队协作中达到统一并且坚持下去,最终可能会落地失败,这也可以被定义为是一种过度设计,因为额外的实现成本,不一定能带来收益。
2.3 控制反转
既然已经约定好了统一使用依赖注入的方式,那是否可以通过框架的底层封装,实现一个底层控制器,约定一个依赖配置规则,控制器根据我们定义的依赖配置来控制实例化过程和依赖共享,帮助我们实现类管理。这样的设计模式就叫控制反转。
控制反转可能第一次听说的时候会很难理解,控制指的什么?反转了啥?
猜测是由于开发者一开始就用此类框架,并没有体验过上个“Express、Koa时代”,缺乏旧社会毒打。加上这反转的用词,在程序中显得非常的抽象,难以望文生义。
前文我们说的实现 Koa 应用,所有的类完全由我们自由控制的,所以可以看作是一个常规的程序控制方式,那就叫它:控制正转。而我们使用 Nest,它底层实现一套控制器,我们只需要在实际开发过程中,按照约定写配置代码,框架程序就会帮我们管理类的依赖注入,所以就把它叫作:控制反转。
本质就是把程序的实现过程交给框架程序去统一管理,控制权从开发者,交给了框架程序。
控制正转:开发者纯手动控制程序
控制反转:框架程序控制
举个现实的例子,一个人本来是自己开车去上班的,他的目的就是到达公司。它自己开车,自己控制路线。而如果交出开车的控制权,就是去赶公交,他只需要选择一个对应的班车就可以到达公司了。单从控制来说,人就是被解放出来了,只需要记住坐那趟公交就行了,犯错的几率也小了,人也轻松了不少。公交系统就是控制器,公交线路就是约定配置。
通过如上的实际对比,我想应该有点能理解控制反转了。
2.4 小结
从 Koa 到 Nest,从前端的 JQuery 到 Vue React。其实都是一步步通过框架封装,去解决上个时代低效率的问题。
上面的 Koa 应用开发,通过非常原始的方式去控制依赖和实例化,就类似于前端中的 JQuery 操作 dom ,这种很原始的方式就把它叫控制正转,而 Vue React 就好似 Nest 提供了一层程序控制器,他们可以都叫控制反转。这也是个人理解,如果有问题期望大神指出。
下面再来说说 Nest 中的模块 @Module,依赖注入、控制反转需要它作为媒介。
Nestjs实现了控制反转,约定配置模块(@module)的 imports、exports、providers 管理提供者也就是类的依赖注入。
providers 可以理解是在当前模块注册和实例化类,下面的 A 和 B 就在当前模块被实例化,如果B在构造函数中引用 A,就是引用的当前 ModuleD 的 A 实例。
import { Module } from '@nestjs/common'; import { ModuleX } from './moduleX'; import { A } from './A'; import { B } from './B'; @Module({ imports: [ModuleX], providers: [A,B], exports: [A] }) export class ModuleD {} // B class B{ constructor(a:A){ this.a = a; } }
exports
就是把当前模块中的 providers
中实例化的类,作为可被外部模块共享的类。比如现在 ModuleF 的 C 类实例化的时候,想直接注入 ModuleD 的 A 类实例。就在 ModuleD 中设置导出(exports)A,在 ModuleF 中通过 imports
导入 ModuleD。
按照下面的写法,控制反转程序会自动扫描依赖,首先看自己模块的 providers 中,有没有提供者 A,如果没有就去寻找导入的 ModuleD 中是否有 A 实例,发现存在,就取得 ModuleD 的 A 实例注入到 C 实例之中。
import { Module } from '@nestjs/common'; import { ModuleD} from './moduleD'; import { C } from './C'; @Module({ imports: [ModuleD], providers: [C], }) export class ModuleF {} // C class C { constructor(a:A){ this.a = a; } }
因此想要让外部模块使用当前模块的类实例,必须先在当前模块的providers
里定义实例化类,再定义导出这个类,否则就会报错。
//正确 @Module({ providers: [A], exports: [A] }) //错误 @Module({ providers: [], exports: [A] })
后期补充
模块查找实例的过程回看了一下,确实有点不清晰。核心点就是providers里的类会被实例化,实例化后就是提供者,模块里只有providers里的类会被实例化,而导出和导入只是一个组织关系配置。模块会优先使用自己的提供者,如果没有,再去找导入的模块是否有对应提供者
这里还是提一嘴ts的知识点
export class C { constructor(private a: A) { } }
由于 TypeScript 支持 constructor 参数(private、protected、public、readonly)隐式自动定义为 class 属性 (Parameter Property),因此无需使用 this.a = a
。Nest 中都是这样的写法。
元编程的概念在 Nest 框架中得到了体现,它其中的控制反转、装饰器,就是元编程的实现。大概可以理解为,元编程本质还是编程,只是中间多了一些抽象的程序,这个抽象程序能够识别元数据(如@Module中的对象数据),其实就是一种扩展能力,能够将其他程序作为数据来处理。我们在编写这样的抽象程序,就是在元编程了。
4.1 元数据
Nest 文档中也常提到了元数据,元数据这个概念第一次看到的话,也会比较费解,需要随着接触时间增长习惯成理解,可以不用太过纠结。
元数据的定义是:描述数据的数据,主要是描述数据属性的信息,也可以理解为描述程序的数据。
Nest 中 @Module 配置的exports、providers、imports、controllers
都是元数据,因为它是用来描述程序关系的数据,这个数据信息不是展示给终端用户的实际数据,而是给框架程序读取识别的。
4.2 Nest 装饰器
如果看看 Nest 中的装饰器源码,会发现,几乎每一个装饰器本身只是通过 reflect-metadata 定义了一个元数据。
@Injectable装饰器
export function Injectable(options?: InjectableOptions): ClassDecorator { return (target: object) => { Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target); }; }
这里存在反射的概念,反射也比较好理解,拿 @Module 装饰器举例,定义元数据 providers
,只是往providers
数组里传入了类,在程序实际运行时providers
里的类,会被框架程序自动实例化变为提供者,不需要开发者显示的去执行实例化和依赖注入。类只有在模块中实例化了之后才变成了提供者。providers
中的类被反射了成了提供者,控制反转就是利用的反射技术。
换个例子的话,就是数据库中的 ORM(对象关系映射),使用 ORM 只需要定义表字段,ORM 库会自动把对象数据转换为 SQL 语句。
const data = TableModel.build(); data.time = 1; data.browser = 'chrome'; data.save(); // SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":"chrome"}]
ORM 库就是利用了反射技术,让使用者只需要关注字段数据本身,对象被 ORM 库反射成为了 SQL 执行语句,开发者只需要关注数据字段,而不需要去写 SQL 了。
4.3 reflect-metadata
reflect-metadata 是一个反射库,Nest 用它来管理元数据。reflect-metadata 使用 WeakMap,创建一个全局单实例,通过 set 和 get 方法设置和获取被装饰对象(类、方法等)的元数据。
// 随便看看即可 var _WeakMap = !usePolyfill && typeof WeakMap === "function" ? WeakMap : CreateWeakMapPolyfill(); var Metadata = new _WeakMap(); function defineMetadata(){ OrdinaryDefineOwnMetadata(){ GetOrCreateMetadataMap(){ var targetMetadata = Metadata.get(O); if (IsUndefined(targetMetadata)) { if (!Create) return undefined; targetMetadata = new _Map(); Metadata.set(O, targetMetadata); } var metadataMap = targetMetadata.get(P); if (IsUndefined(metadataMap)) { if (!Create) return undefined; metadataMap = new _Map(); targetMetadata.set(P, metadataMap); } return metadataMap; } } }
reflect-metadata 把被装饰者的元数据存在了全局单例对象中,进行统一管理。reflect-metadata 并不是实现具体的反射,而是提供了一个辅助反射实现的工具库。
现在再来看看前面的几个疑问。
为什么需要控制反转?
什么是依赖注入?
装饰器做了啥?
模块 (@Module) 中的提供者(providers),导入(imports)、导出(exports)是什么实现原理?
1 和 2 我想前面已经说清楚了,如果还有点模糊,建议再回去看一遍并查阅一些其它文章资料,通过不同作者的思维来帮助理解知识。
5.1 问题 [3 4] 总述:
Nest 利用反射技术、实现了控制反转,提供了元编程能力,开发者使用 @Module 装饰器修饰类并定义元数据(providers\imports\exports),元数据被存储在全局对象中(使用 reflect-metadata 库)。程序运行后,Nest 框架内部的控制程序读取和注册模块树,扫描元数据并实例化类,使其成为提供者,并根据模块元数据中的 providers\imports\exports 定义,在所有模块的提供者中寻找当前类的其它依赖类的实例(提供者),找到后通过构造函数注入。
本文概念较多,也并没有做太详细的解析,概念需要时间慢慢理解,如果一时理解不透彻,也不必太过着急。好吧,就到这里,这篇文章还是花费不少精力,喜欢的朋友期望你能一键三连~
更多node相关知识,请访问:nodejs 教程!
위 내용은 Node.js Nestjs 프레임워크의 모듈 메커니즘을 이해하고 구현 원칙에 대해 이야기합니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!