Home>Article>Web Front-end> Understand the module mechanism of Node.js Nestjs framework and talk about the implementation principles
This article will take you to understand theNodeback-end framework Nest.js, and introduce the concept and implementation principle of the Nestjs module mechanism. I hope it will be helpful to everyone!
Nest provides a module mechanism. Dependency injection is completed by defining providers, imports, exports and provider constructors in the module decorator, and the entire module tree is organized Application development. There is absolutely no problem in directly launching an application according to the conventions of the framework itself. However, for me, I feel that I lack a clearer and systematic understanding of the dependency injection, inversion of control, modules, providers, metadata, related decorators, etc. declared by the framework.
- Why is inversion of control needed?
- What is dependency injection?
- What does the decorator do?
- What are the implementation principles of providers, imports, and exports in the module (@Module)?
It seems that I can understand and understand it, but let me explain it clearly from the beginning, I can’t explain it clearly. So I did some research and came up with this article. From now on, we start from scratch and enter the main text.
1.1 Express, Koa
The development process of a language and its technical community must be The gradual development of functions from the bottom to the top is like the process of tree roots slowly growing into branches and then full of leaves. Earlier, basic web service frameworks such as Express and Koa appeared inNodejs. Able to provide a very basic service capability. Based on such a framework, a large number of middleware and plug-ins began to be born in the community, providing richer services for the framework. We need to organize application dependencies and build application scaffolding ourselves, which is flexible and cumbersome, and also requires a certain workload.
As the development progressed, some frameworks with more efficient production and more unified rules were born, ushering in a newer stage.
1.2 EggJs and Nestjs
In order to be more adaptable to rapid production applications, unify specifications and make them available out of the box, EggJs and NestJs were developed , Midway and other frameworks. This type of framework abstracts the implementation of an application into a universal and extensible process by implementing the underlying life cycle. We only need to follow the configuration method provided by the framework to implement the application more simply. The framework implements process control of the program, and we only need to assemble our parts at the appropriate location. This looks more like assembly line work. Each process is clearly divided, and a lot of implementation costs are saved.
1.3 Summary
The above two stages are just a foreshadowing. We can roughly understand that the upgrade of the framework improves production efficiency. To upgrade the framework, some design ideas and patterns will be introduced. The concepts of control inversion, dependency injection, and metaprogramming appear in Nest. Let’s talk about it below.
2.1 Dependency injection
An application actually There are a lot of abstract classes that realize all the functions of the application by calling each other. As the complexity of application code and functions increases, the project will definitely become more and more difficult to maintain because there are more and more classes and the relationships between them become more and more complex.
For example, if we use Koa to develop our application, Koa itself mainly implements a set of basic Web service capabilities. In the process of implementing the application, we will define many classes, and the instantiation of these classes The methods and interdependencies will be freely organized and controlled by us in the code logic. The instantiation of each class is manually new by us, and we can control whether a class is only instantiated once and then shared, or instantiated every time. The following class B depends on A. Every time B is instantiated, A will be instantiated once, so for each instance B, A is an instance that is not shared.
class A{} // B class B{ contructor(){ this.a = new A(); } }
The C below is the external instance obtained, so multiple C instances share the app.a instance.
class A{} // C const app = {}; app.a = new A(); class C{ contructor(){ this.a = app.a; } }
The following D is passed in through the constructor parameter. You can pass in a non-shared instance each time, or you can pass in the shared app.a instance (D and F share app.a), and Since it is now passed in as a parameter, I can also pass in an X class instance.
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())
This method isDependency injection, which injects A, which B depends on, into B by passing value. Injection through the constructor (passing by value) is only one implementation method. It can also be passed in by implementing the set method call, or any other method, as long as an external dependency can be passed into the internal one. It's really that simple.
class A{} // D class D{ setDep(a){ this.a = a; } } const d = new D() d.setDep(new A())
2.2 All in dependency injection?
随着迭代进行,出现了 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 教程!
The above is the detailed content of Understand the module mechanism of Node.js Nestjs framework and talk about the implementation principles. For more information, please follow other related articles on the PHP Chinese website!