With the advent of Node.js, JavaScript has become a common language for both the front and back ends. However, unlike node.js in the front-end domain where there are a number of excellent engineering frameworks such as Angular, React, Vue, etc., in the back-end domain, Express, Koa and other well-known tools failed to solve an important problem — architecture. Nest emerged in this context, inspired by Angular’s design philosophy, and many of Angular’s patterns are derived from the Spring framework in Java, so Nest is the Node.js version of the Spring framework.

Therefore, for many Java backend students, the design and writing methods of Nest are very easy to understand, but for traditional JS programmers from the front end, only mentioning the most important and core ideas of Nest such as inversion of control, dependency injection and other concepts is daunting. Not to mention the concept of TypeScript, decorators, metadata, reflection, etc., and the fact that its official documentation and core community are all in English has left many students out.

This series of articles will start with the design idea of Nest and explain its related concepts and principles in detail, and finally imitate the implementation of an extremely simple (or rudimentary) FakeNest framework. On the one hand, the students who have already used and want to further understand Nest principle can gain something, on the other hand, the students who are engaged in traditional JS front-end development can get started and learn some excellent ideas in back-end development.

This is the second part of the nest.js introduction, which will detail the syntax at the heart of the nest.js implementation — decorators and metadata. We start from the usage of TS decorator syntax, metadata concept, gradually explore the sparks of the collision between decorator and metadata, and finally go deep into the implementation principle behind the decorator, take you to understand decorator and metadata.

First taste of decoration

Decorators provide a way to add annotations to class declarations and members. Decorators in Javascript are currently in the second phase of the call for proposals, but are already supported as an experimental feature in TypeScript.

Note: Decorators are an experimental feature and may change in future releases.

A decorator is a special type of declaration that can be attached to a class declaration, method, accessor, property, or parameter. The decorator uses the form @expression. Expression must be evaluated as a function, which is called at run time (it is called first regardless of whether the class is instantiated or not), and the decorator’s information (slightly different depending on the decorator) is passed as an argument to the evaluated expression function.

As the saying goes, seeing is believing. We will not belabor the concept of decorator, but write a decorator to feel it.

// Class decorator use!! The decorator here is the class Example
@classDecor
class Example{
  // All properties are declared valid for this.text
  [x: string] :any
  
  print(){
    console.log(this.text)
  }
}

// Class decorator declaration!! You can see that the decorator information is passed in as an argument, where the class decorator argument is the constructor of the decorator class
function classDecor(constructor: Function){
    console.log('ClassDecor is called')
    constructor.prototype.text = 'Class is decorated'}console.log('New Example instance')
new Example().print() // What output?Bingo! //ClassDecor is called
// New Example instance
// Class is decorated
Copy the code

Here we define a function called classDecor, which is decorated with the @ symbol and placed before the Example class as a typical classDecor. At the end of the code, we generate an Example instance by calling new Example and calling the print method on it. You can see that because of the classDecor Class decorator, the instantiated print accesses the property text, which is not defined in the Example Class, and successfully prints its value Class is Decorated. In addition, because the ClassDecor is called the first time the program runs, the ClassDecor is called before the New Example instance is printed. That’s why we couldn’t mount the Text property on the Example instance (which didn’t exist when we ran classDecor). Instead, we mounted it on its prototype chain.

Of course, TS provides method decorators, accessor decorators, property decorators, and parameter decorators in addition to class decorators. In order not to make this article too lengthy, we will not repeat all the detailed uses of these decorators here. But to give you an idea of what they are, let’s put them all together and write a class that is decorated with decorators. (Note: These decorators have no practical meaning, just to illustrate how they should be defined and used)

// Class decorator
@classDecor
class Example{
  // Attribute decorator
  @attributeDecor
  attribute: string;

  Accessor decorator
  @accessorDecor
  get accessor() :string{
    return this.attribute
  }
	
  // Method decorator
  @functionDecor
  // Parameter decorator
  func(@paramsDecor params: number) :number{
    return params
  }
}

function classDecor(constructor: Function){
  // Constructor class constructor
  console.log('classDecor is called')}function functionDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
  // Target is the class constructor for static members and the prototype object for instance members
  // Name of the propertyKey member
  Attribute descriptor of the descriptor member
  console.log('functionDecor is called')}function accessorDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
  // Target is the class constructor for static members and the prototype object for instance members
  // Name of the propertyKey member
  Attribute descriptor of the descriptor member
  console.log('accessorDecor is called')}function attributeDecor(target: any, propertyKey: string){
  // Target is the class constructor for static members and the prototype object for instance members
  // Name of the propertyKey member
  console.log('attributeDecor is called')}function paramsDecor(target: Object, propertyKey: string | symbol, parameterIndex: number){
  // Target is the class constructor for static members and the prototype object for instance members
  // Name of the propertyKey member
  // parameterIndex Specifies the index of the parameter in the function argument list
  console.log('paramsDecor is called')}console.log('new Example instance')
new Example()

// attributeDecor is called
// accessorDecor is called
// paramsDecor is called
// functionDecor is called
// classDecor is called
// new Example instance
Copy the code

Hopefully, you have a general idea of how these decorators are used and their parameters before you read further. If you are a decorator, so consult a decorator TS website section (www.tslang.cn/docs/handbo…

Metadata and reflection

To understand how decorators are implemented, we also need to introduce the concept of metadata.

Metadata is Data that describes other Data. — Ruan Yifeng, MetaData

In TS, we typically use reflect-metadata to support metadata-related apis. The library is not currently part of the ECMAScript (JavaScript) standard. In the future, however, these extensions will be recommended for adoption by ECMAScript as decorators are adopted by the official ECMAScript standard.

TS itself supports information such as defining the type of data, but this information exists only for the TS compiler to perform static type checking at compile time. The compiled code becomes untyped traditional JS code. Reflect-metadata provides a series of related methods to enable JS to retrieve data types, code state, and custom content at runtime. The following code shows the basic usage of this method.

// It is not yet standard, so to use the reflect-metadata method, you need to manually import the library, which will automatically hang on the Reflect-metadata global object
import 'reflect-metadata'

class Example {
  text: string
}
// define an exp instance that receives Example. : Example/: string is provided to the TS compiler for static type checking, but the type information disappears after compilation
const exp: Example = new Example()

// Note: Adding metadata manually only shows how reflect-metadata can be used; in most cases, the compiler should automatically add the relevant code at compile time
// To get the type of exp at run time, we manually called defineMetadata method to add a metadata value of exp with key type and value Example
Reflect.defineMetadata('type'.'Example', exp)
// To get the type of the text property at run time, we manually called the defineMetadata method text for exp to add a metadata with key type and value Example
Reflect.defineMetadata('type'.'String', exp, 'text')

// The runtime calls the getMetadata method, passing in the desired metadata key and the target to get the relevant information (exp and text are available here).
// Output 'Example' 'String'
console.log(Reflect.getMetadata('type', exp))
console.log(Reflect.getMetadata('type', exp, 'text'))
Copy the code

Besides defineMetadata and getMetadata, two basic methods, Reflection-metadata also provides hasMetadata, hasOwnMetadata, getOwnMetadata, and getMetadataKeys Metadata manipulation methods include getOwnMetadataKeys (enumerating metadata that exists on non-prototype chains), deleteMetadata (deleting metadata), and @Reflect. Metadata decorator (defining metadata).

Some of you might be wondering here, why do we need to get metadata information like data types at runtime? Admittedly, this information doesn’t mean much to the business we’re trying to implement in general, but it’s the basis for implementing Javascript reflection!

In computer science, reflection refers to a class of applications that are self-describing and self-controlling. In other words, this kind of application adopts some mechanism to realize self-representation and examination of its own behavior, and can adjust or modify the state and related semantics of the behavior described by the application according to the state and result of its own behavior.

Reflection is useful for dependency injection, runtime type assertion, and testing. Although JavaScript can Object. GetOwnPropertyDescriptor () or Object. The keys () function for some instance information, however we still need to reflection to realize more powerful function, Thus TS implements reflection by introducing reflect-metadata and through its operational metadata.

Reflection mechanism is the basis of Nest. Js to realize inversion of control and dependency injection. (For those of you who don’t understand these concepts, refer back to the first nex.js introduction in this series, Inversion of Control and Dependency Injection (I).) However, it’s not time to delve into how Nex.js uses this mechanism to complete its core code design.

Decorators and metadata

TS removes the information related to the original data type during compilation and converts TS files into traditional JS files for the JS engine to execute. However, once we introduce reflect-metadata and decorate a class or its methods, attributes, accessors, or method parameters using decorator syntax, TS automatically adds type-specific metadata to the object we decorate after compilation. Currently, there are only three keys:

  • Type metadata using the metadata key “Design: Type”
  • Parameter type metadata uses the metadata key “Design: Paramtypes”
  • Return value type metadata using the metadata key “Design: ReturnType”

These keys are automatically assigned different values depending on the decorator type, so let’s put them in the Example class and see what they return.

@classDecor
class Example{
  @attributeDecor
  attribute: string;

  @accessorDecor
  get accessor() :string{
    return this.attribute
  }
  
  constructor(attribute: string){
    this.attribute = attribute
  }

  @functionDecor
  func(@paramsDecor params: number) :number{
    return params
  }
}

function classDecor(constructor: Function){
  // classDecor - undefined [[Function: String]] undefined
  console.log('classDecor')
  console.log(Reflect.getMetadata('design:type'.constructor))
  console.log(Reflect.getMetadata('design:paramtypes', constructor))
  console.log(Reflect.getMetadata('design:returntype', constructor))}function functionDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor) {[Function: Number]] [Function: Number]
  console.log('functionDecor')
  console.log(Reflect.getMetadata('design:type', target, propertyKey ))
  console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
  console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
function accessorDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
  AccessorDecor [Function: String] [] undefined
  console.log('accessorDecor')
  console.log(Reflect.getMetadata('design:type', target, propertyKey ))
  console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
  console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
function attributeDecor(target: any, propertyKey: string){
  // Outputs attributeDecor [Function: String] undefined
  console.log('attributeDecor')
  console.log(Reflect.getMetadata('design:type', target, propertyKey ))
  console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
  console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
function paramsDecor(target: Object, propertyKey: string | symbol, parameterIndex: number){
  ParamsDecor [Function: Function] [[Function: Number]] [Function: Number]
  console.log('paramsDecor')
  console.log(Reflect.getMetadata('design:type', target, propertyKey ))
  console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
  console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
Copy the code

As you can see, for different types of decorators, different types of type-dependent metadata are automatically assigned related values that can be retrieved and used differently during subsequent program runs. In order to facilitate the reader to understand its function, here we take a simple parameter verification example to show its powerful function.

Validate in the code below is a method decorator that validates input arguments in any method it decorates at run time and compares them to the TS type we define to see if the two types are consistent. You may wonder why you should validate at runtime, since TS already provides static validation of types. But as you’ll soon see, some users don’t necessarily call our methods as expected, and there are actually quite a few TS programmers messing around with any! This will make the TS static check null and void. Therefore, runtime validation is required in some cases!

To minimize code length and simplify code structure, validate only supports validation of base type parameters and does not consider parameter defaults.

class Example {
  // method decorator to validate the input parameter of the print method
  @validate
  print(val: string){
    console.log(val)
  }
}

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // Get all the argument types on the decorated method
  const paramstypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
  const originFunc = descriptor.value
  // Modify the decorated method with the descriptor, dynamically checking the input type by calling _innervalidate before running the original method
  descriptor.value = function(. args:any[]){
    _innervalidate(args, paramstypes)
    originFunc.apply(this, args)
  }

  function _innervalidate(args: any[], types: any[]) {
    // The required parameter length is not the same, error! (Parameter defaults are not considered)
    if(args.length ! = types.length){throw new Error('Error: Wrong params length')}// Compare the types of input parameters and determine whether they are equal.
    args.forEach((arg, index) = >{
      const type = types[index].name.toLowerCase()
      if((typeofarg) ! =type) {throw new Error(`Error: Need a The ${type}, but get the The ${typeof arg} ${arg}instead! `)}})}}const example = new Example()
// Val1 is as expected, but val2 is fooling the TS compiler here
const val1:any = 'test'
const val2:any = 23
// Try to run
try{
  // Print 'test' to verify
  example.print(val1)
  / / an error! 'Error: Need a string, but get the number 23 instead! '
  // Didn't fool our validate decorator because we got its type dynamically at runtime!
  example.print(val2)
}catch(e) {
  console.log(e.message)
}
Copy the code

We can see that by using decorators and metadata, we can do things that were unimaginable before!

Four, decorator implementation

Now, let’s dig a little deeper and look at how the TS decorator syntax is implemented. We use the TSC directive to ask TS to help us compile the code in section 1. Open the compiled JS file and you can see the following code. All of the code is pasted here for clarity, but don’t rush to read it line by line. I’ll break it down and describe what each part does in detail.

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect= = ="object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect= = ="object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }};Object.defineProperty(exports."__esModule", { value: true });
require("reflect-metadata");
let Example = class Example {
    get accessor() {
        return this.attribute;
    }
    func(params) {
        returnparams; }}; __decorate([ attributeDecor, __metadata("design:type".String)
], Example.prototype, "attribute".void 0);
__decorate([
    accessorDecor,
    __metadata("design:type".String),
    __metadata("design:paramtypes", [])
], Example.prototype, "accessor".null);
__decorate([
    functionDecor,
    __param(0, paramsDecor),
    __metadata("design:type".Function),
    __metadata("design:paramtypes"[Number]),
    __metadata("design:returntype".void 0)
], Example.prototype, "func".null);
Example = __decorate([
    classDecor
], Example);
function classDecor(constructor) {
    console.log('classDecor is called');
}
function functionDecor(target, propertyKey, descriptor) {
    console.log('functionDecor is called');
}
function accessorDecor(target, propertyKey, descriptor) {
    console.log('accessorDecor is called');
}
function attributeDecor(target, propertyKey) {
    console.log('attributeDecor is called');
}
function paramsDecor(target, propertyKey, parameterIndex) {
    console.log('paramsDecor is called');
}
console.log('new Example instance');
const example = new Example();
Copy the code

Let’s start with the __metadata function, which can simply be interpreted as equivalent to reflect.metadata.

Looking closely at this function, it can be broken down into two parts:

  1. If __metadata is defined in the current environment (this), use the defined __metadata. If the function is undefined, define a function that accepts two parameters.

  2. Function to determine if reflect. metadata exists and is a function. (Remember we said reflect-metadata is not part of the ECMAScript standard and needs to be introduced manually?) If the method exists, call it directly and return a function that takes the k/v arguments as the metadata key and value; If not, the __metadata method is an empty function that does nothing!

var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect= = ="object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Copy the code

Next, let’s look at what the __param function does, which is a typical higher-order function (Currization)!

  1. If __param has been defined in the current environment (this), then the function is used instead of being defined. If the function is undefined, define a function that accepts two parameters.

  2. Returns a function that takes two values. This function executes using the decorator custom parameter decorator obtained in the first step as a method, calling as arguments the ordinal number of the parameter decorated by paramIndex, the constructor or prototype object of the class decorated by target, and the name of the class method decorated by key.

As you can see, all it does is call a user-defined parameter decorator in the future and pass in the parameters it needs. This is actually the implementation of the parameter decorator. The simplest decorator does not add any metadata to the decorator class.

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }};Copy the code

Now, before we dive into the details of the __decorate function, let’s look at how the function is called in the main flow.

  1. Attribute decoratorWe pass inFour parameters. Where the first argument is an array of functions [custom decorator functions, addedkey: 'design:type' | value: StringMetadata function]; The second argument is the class’s prototype object (the class’s constructor when decorating a static static property); The third argument is the name of the decorated class member;The fourth parameter isundefined(void 0isundefined, the reason why it is usedvoid 0becauseundefinedCan be changed by user assignment, unsafe).
  2. Accessor decoratorWe pass inFour parameters. Where the first argument is an array of functions [custom decorator functions, addedkey: 'design:type' | value: StringMetadata function, addkey: 'design:paramtypes' | value: []Metadata function]; The second argument is the class’s prototype object (the class’s constructor when decorating a static static property); The third argument is the name of the decorated class member;The fourth parameter isnull.
  3. Method decoratorWe pass inFour parameters. Where the first argument is the array of functions [custom decorator function, parameter decorator _param function, addkey: 'design:type' | value: StringMetadata function, addkey: 'design:paramtypes' | value: []Metadata function, addkey: 'design:returntype' | value: undefiedMetadata function]; The second argument is the class’s prototype object (the class’s constructor when decorating a static static property); The third argument is the name of the decorated class member;The fourth parameter isnull.
  4. Class decoratorWe pass inTwo parameters. The first argument is an array of functions [custom decorator functions]. The second argument is the constructor of the class. Notice in the class decorator__decorateThe return value of the function overrides the decorated class, giving us the ability to modify a class using the class decorator.

Here I’ve put the type of decorator and the number of arguments it passes, as well as the fourth argument in bold, which will be used later in our __decorate implementation!

// Attribute decorator
__decorate([
    attributeDecor,
    __metadata("design:type".String)
], Example.prototype, "attribute".void 0);
Accessor decorator
__decorate([
    accessorDecor,
    __metadata("design:type".String),
    __metadata("design:paramtypes", [])
], Example.prototype, "accessor".null);
// Method decorator
__decorate([
    functionDecor,
  	// Parameter decorator
    __param(0, paramsDecor),
    __metadata("design:type".Function),
    __metadata("design:paramtypes"[Number]),
    __metadata("design:returntype".void 0)
], Example.prototype, "func".null);
// Class decorator
Example = __decorate([
    classDecor
], Example)
Copy the code

Finally, let’s look at the __decorate function.

  1. Check if there is already a __class defined in the current environment (this). If this function is already defined, use the one that is already defined. If the function is undefined, define a function that takes four arguments: an array of decorators functions, the constructor or prototype object of the class decorated by target, the name of the class member decorated by key, and the descriptor of the class member decorated by desc.

  2. Define a variable c that stores the number of arguments actually passed to the __pipeline function at runtime.

  3. Define a variable r that stores different things depending on the number of arguments actually passed to __decorate:

    A. When two are passed, r is the constructor of the class or the prototype object of the class;

    B. If desc is null, the accessor or method decorator is undefined.

  4. Define an uninitialized variable d;

  5. Check if Reflect. Decorate exists, and if so, call that method directly. We won’t go too far into Reflect. Decorate; it works the same way as the else line below. This is because TS wants to Reflect. Decorate as standard for ES in the future, so that old code doesn’t have to change to be compatible with the new standard.

  6. This step is the heart of the function. Traverse the array of decorators decorator functions from back to front, assigning the traversed function to variable D on each pass. If d is not empty, different branches are entered based on the number of arguments actually passed to the __pipeline function at runtime:

    A. When passed two times (<3, class decorator), pass the constructor of the class stored in R or the prototype object of the class as the only argument into D and call it.

    B. Pass four (>3, property decorator, accessor decorator, method decorator), pass the constructor or prototype object of the target class, the name of the class member decorated by key, and the descriptor or undefined of the class member of r into D and call it.

    C. Pass the constructor of the target class or the prototype object of the class, and the name of the class member decorated by the key into d and call it.

    Finally, reassign r to the return value of d (if any) or r itself (d has no return value);

  7. If we actually pass in four arguments to __decorate and r exists, we replace the value of the decorator target with r;

  8. Returns the r.

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect= = ="object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Copy the code

At this point, we have covered all the implementation of decorators. There is no decorator implementation for static methods, parameters, accessors, they are basically the same as non-static! Try to think of how they call the __decorate function, and verify that for yourself!)

Through the implementation of the decorator, it can be clearly seen that the decorator mainly does the following two things:

  1. Depending on the decorator type, different metadata (design:type,design:paramtypes,design:returntype) added to the decorated target;
  2. Calls the decorator method and passes in the parameters it needs based on the decorator type.

Here are a few more details worth noting:

  1. decoratorsThe array of decorator functions is traversed and called in reverse order, so decorators are executed from the decorator near the decorator and up;
  2. If there is a return value in the decorator function after execution, the return value passesObject.defineProperty(target, key, r)Replace the value of the decorator;
  3. Instead of trying to reassign the desc descriptor object from the accessor decorator or method decorator, this will not work. Instead, try changing some properties of the DESC descriptor object.
  4. Decorators are executed immediately after the class declaration, not after instantiation;
  5. The different types of decorators are executed in this order — property decorator, accessor decorator, parameter decorator, method decorator, and class decorator. This is the same order we printed after the code ran in section 1!

Five, the summary

Decorators and metadata are unfamiliar to most of the front-end students, but their skillful use allows us to take our code to the next level, making it easier to accomplish previously unsolvable tasks. It is helpful to learn how they are used and how they work.

Returning to the main task of this series, learning to understand the use and implementation of decorators and metadata is fundamental to understanding the source code of Nex.js. By combining inversion of control and dependency injection with the use of decorators and metadata, the core design of Nex.js can be easily accomplished. In the next article, we’ll look at the main design ideas behind Nest.js and emulate a simplified version of the FakeNest framework.