How to quickly get started with VUE3.0: Enter and learn
Nest provides a module mechanism. Dependency injection is completed by defining providers, imports, exports, and provider constructors in the module decorator, and the development of the entire application is organized through the module tree. 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 modules (@Module)?
I seem to be able to understand and appreciate 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 gradually enrich and develop from the bottom functions upwards, just 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 in Nodejs. 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.
Later in development, 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 standards and make them available out of the box, frameworks such as EggJs, NestJs, and Midway have been developed. 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 achieve the upgrade of the framework, some design ideas and patterns will be introduced. Inversion of control appears in Nest. , dependency injection, and the concepts of metaprogramming, let’s talk about it below.
2.1 Dependency Injection
An application is actually a lot of abstract classes, which 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, the instantiation methods and interdependencies of these classes, It will all 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 whether it is 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{ constructor(){ 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{ constructor(){ 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 because of the way it is now a parameter Pass in, I can also pass in an X class instance.
class A{} class X{} //D const app = {}; app.a = new A(); class D{ constructor(a){ this.a = a; } } class F{ constructor(a){ this.a = a; } } new D(app.a) new F(app.a)
new
D(new
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?
As the iteration proceeds, it appears that B's dependencies will change according to different preconditions. For example, precondition one this.a
needs to pass in the instance of A, and precondition two this.a
needs to pass in the instance of X. At this time, we will start to do the actual abstraction. We will transform it into a dependency injection method like D above.
In the early days, when we implemented the application, we would implement the writing method of classes B and C as long as it met the needs at that time. This itself was not a problem. After the project iterated for several years, this part of the code would not necessarily be touched. . If we consider later expansion, it will affect development efficiency and may not be useful. So most of the time, we encounter scenarios that require abstraction, and then abstractly transform part of the code.
// class B{ before transformation constructor(){ this.a = new A(); } } new B() //Class D{ after transformation constructor(a){ this.a = a; } } new D(new A())new D
(
new
implementation costs.
This example is given here to illustrate that in a development model without any constraints or regulations. We can freely write code to control dependencies between various classes. In a completely open environment, it is very free. This is a primitive era of slash-and-burn farming. Since there is no fixed code development model and no highest action plan, as different developers intervene or the same developer writes code at different times, the dependency relationship will become very different as the code grows. Clearly, the shared instance may be instantiated multiple times, wasting memory. From the code, it is difficult to see a complete dependency structure, and the code may become very difficult to maintain.
Then every time we define a class, we write it according to the dependency injection method, and write it like D. Then the abstraction process of C and B is advanced, which makes later expansion more convenient and reduces the cost of transformation. So this is called All in 依赖注入
, that is, all our dependencies are implemented through dependency injection.
However, the early implementation cost becomes high again, and it is difficult to achieve unity and persistence in team collaboration. In the end, the implementation may fail. This can also be defined as an over-design, because the additional implementation cost may not necessarily be achieved. bring benefits.
2.3 Inversion of Control
Now that we have agreed on the unified use of dependency injection, can we implement a bottom-level controller through the underlying encapsulation of the framework and agree on a dependency configuration rule? The controller will control the instantiation process based on the dependency configuration we defined. and dependency sharing to help us achieve class management. This design pattern is called inversion of control .
Inversion of control may be difficult to understand when you hear it for the first time. What does control mean? What was reversed?
It is speculated that this is because developers have used such frameworks from the beginning and have not experienced the last "Express and Koa era" and lack the beatings of the old society. Coupled with the reversed wording, the program appears very abstract and difficult to understand.
As we mentioned earlier, when implementing Koa applications, all classes are completely controlled by us, so it can be regarded as a conventional program control method, so we call it: control forward rotation. We use Nest, which implements a set of controllers at the bottom. We only need to write configuration code according to the agreement during the actual development process, and the framework program will help us manage the dependency injection of classes, so we call it: inversion of control. .
The essence is to hand over the implementation process of the program to the framework program for unified management, and transfer the control power from the developer to the framework program.
Control forward rotation: developer’s purely manual control program
Inversion of control: framework program control
To give a real example, a person drives to work by himself, and his purpose is to reach the company. It drives itself and controls its own route. And if he hands over the control of driving and catches the bus, he only needs to choose a corresponding shuttle bus to reach the company. In terms of control alone, people are liberated. They only need to remember which bus to take. The chance of making mistakes is also reduced, and people are much more relaxed. The bus system is the controller, and the bus lines are the agreed configurations.
Through the above actual comparison, I think I should be able to understand the inversion of control.
2.4 Summary
From Koa to Nest, from front-end JQuery to Vue React. In fact, they are all implemented step by step through framework encapsulation to solve the inefficiency problems of the previous era.
The above Koa application development uses a very primitive way to control dependencies and instantiation, which is similar to JQuery operating dom in the front end. This very primitive way is called control forwarding, and Vue React is like what Nest provides. A layer of program controller, they can all be called inversion of control. This is also my personal understanding. If there are any problems, I hope God will point them out.
Let’s talk about the module @Module in Nest. Dependency injection and control inversion require it as a media.
Nestjs implements inversion of control and agrees to configure the imports, exports, and providers of the module (@module) to manage the provider, which is the dependency injection of the class.
Providers can be understood as registering and instantiating classes in the current module. The following A and B are instantiated in the current module. If B references A in the constructor, it refers to the A instance of the current ModuleD.
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
refers to classes instantiated in providers
in the current module as classes that can be shared by external modules. For example, when the C class of ModuleF is instantiated, I want to directly inject the A class instance of ModuleD. Just set exports A in ModuleD, and import ModuleD through imports
in ModuleF.
According to the following writing method, the inversion of control program will automatically scan dependencies. First, check whether there is provider A in the providers of your own module. If not, look for an instance of A in the imported ModuleD. If it is found, get the A of ModuleD. Instance is injected into the C instance.
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; } }
Therefore, if you want an external module to use the class instance of the current module, you must first define the instantiation class in providers
of the current module, and then define and export this class, otherwise an error will be reported.
//Correct @Module({ providers: [A], exports: [A] }) //Error @Module({ providers: [], exports: [A] })
Looking back at the process of finding instances of thelater supplementary
module, it is indeed a bit unclear. The core point is that the classes in providers will be instantiated, and after instantiation they become providers. Only the classes in providers in the module will be instantiated, and export and import are just organizational relationship configurations. The module will give priority to using its own provider. If not, then check whether the imported module has a corresponding provider.
Let me mention some ts knowledge points.
export class C { constructor(private a: A) { } }
Since TypeScript supports constructor parameters (private, protected, public, readonly) to be implicitly and automatically defined as class attributes (Parameter Property), there is no need to use this.a = a
. This is the way it is written in Nest.
The concept of metaprogramming is reflected in the Nest framework. Inversion of control and decorators are the implementation of metaprogramming. It can be roughly understood that the essence of metaprogramming is still programming, but there are some abstract programs in the middle. This abstract program can identify metadata (such as object data in @Module), which is actually an expansion capability that can use other programs as data. to handle. When we write such abstract programs, we are metaprogramming.
4.1 Metadata
Metadata is often mentioned in Nest documents. The concept of metadata can be confusing when you see it for the first time. You need to get used to it and understand it as time goes by, so you don’t need to get too entangled.
The definition of metadata is: data that describes data, mainly information that describes data attributes, and can also be understood as data that describes programs.
exports、providers、imports、controllers
configured by @Module in Nest are all metadata , because it is data used to describe program relationships. This data information is not the actual data displayed to the end user, but is read and recognized by the framework program. .
4.2 Nest Decorator
If you look at the decorator source code in Nest, you will find that almost every decorator itself only defines a metadata through reflect-metadata.
@Injectable decorator
export function Injectable(options?: InjectableOptions): ClassDecorator { return (target: object) => { Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target); Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target); }; }
There is the concept of reflection here, and reflection is relatively easy to understand. Take the @Module decorator as an example to define metadata providers
. It just passes classes into the providers
array. When the program actually runs, the classes in providers
will be automatically used by the framework program. Instantiation becomes a provider, and developers do not need to explicitly perform instantiation and dependency injection. A class only becomes a provider after it is instantiated in a module. The classes in providers
are reflected and become providers. Inversion of control is the reflection technology used.
Another example is the ORM (Object Relational Mapping) in the database. To use ORM, you only need to define table fields, and the ORM library will automatically convert the object data into SQL statements.
const data = TableModel.build(); data.time = 1; data.browser = 'chrome'; data.save(); // SQL: INSERT INTO tableName (time,browser) [{"time":1,"browser":"chrome"}]
The ORM library uses reflection technology, so that users only need to pay attention to the field data itself, and the object is ORM Library reflection becomes a SQL execution statement. Developers only need to focus on data fields and do not need to write SQL.
4.3 reflect-metadata
reflect-metadata is a reflection library that Nest uses to manage metadata. reflect-metadata uses WeakMap to create a global single instance, and sets and obtains the metadata of the decorated object (class, method, etc.) through the set and get methods.
// Just take a look 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 stores the metadata of the decorated person in the global singleton object for unified management. reflect-metadata does not implement specific reflection, but provides a tool library to assist reflection implementation.
, let’s look at the previous questions.
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 modules (@Module)?
1 and 2 I think I have made it clear before. If it is still a bit vague, I suggest you go back and read it again and consult some other articles to help you understand the knowledge through the thinking of different authors.
5.1 Problem [3 4] Overview:
Nest uses reflection technology to implement inversion of control and provides metaprogramming capabilities. Developers use the @Module decorator to decorate classes and define metadata (providersimportsexports), and the metadata is Stored in a global object (using reflect-metadata library). After the program runs, the control program inside the Nest framework reads and registers the module tree, scans the metadata and instantiates the class to become a provider, and provides it in all modules according to the providersimportsexports definition in the module metadata. Search for instances (providers) of other dependent classes of the current class, and inject them through the constructor after finding them.
This article has many concepts and does not provide too detailed analysis. The concepts take time to be understood slowly. If you do not understand thoroughly at the moment, don’t be too anxious. Okay, that’s it. This article still took a lot of effort. Friends who like it hope you can connect three times with one click~