preface

Before learning NestJS, it is important to learn the dependency injection principle and the general principles of the Express framework source code (by default, NestJS adds functionality on top of Express).

Many students, that is, if it were not for the foundation to learn nestjs API is basically a rote learning, and work never used at ordinary times, most likely learned today, tomorrow will soon forget, so learning the basic principle, not only for you help, the interview also made some wheels to you in the future, use some wheels more handy.

So this article will write a simple version of the implementation around these two points, to help interested students understand.

Can you briefly explain what dependency injection means and why it is used?

Let’s start with a simple example:

Suppose there is a class called Lao Wang, which refers to the pretty boys who live next door to the pretty girls. They like to help people and give big hats. They’re good at running away.

class Hobby {
    constructor(gender){
        return [gender, 'Helping others']}}class Skill {
    constructor(){
        return ['Send the hat'.'run']}}class Person {
    hobby: Hobby
    skill: Skill
    constructor(){
        this.hobby = new Hobby('woman');
        this.skill = newSkill(); }}console.log(new Person()); // {hobby: [' hobby ', 'hobby '], skill: [' hobby ',' hobby ']}
Copy the code

Okay, the Person class, let’s see what’s wrong with it:

  • Each time we create an instance of the Person class, we pass in the Hobby class and the Skill class. Therefore, we rely on both classes. If we want to create a different Person class, for example, some Person likes fresh meat and some Person likes preserved meat, the Person class will be written to death and cannot be customized

Hobby and Skill as parameters. Yes, this method does solve the problem as follows:

class Hobby {
    constructor(gender){
        return [gender, 'Helping others']}}class Skill {
    constructor(){
        return ['Send the hat'.'run']}}class Person {
    hobby: Hobby
    skill: Skill
    constructor(hobby, skill){
        this.hobby = hobby;
        this.skill = skill; }}// {hobby: [' hobby ', 'hobby '], skill: [' hobby ',' hobby ']}
console.log(new Person(new Hobby('male'), new Skill()); 
Copy the code
  • But is there a better way, where we don’t have to go to new ourselves, and the system automatically imports it for us like this?
class Person {
    constructor(hobby: Hobby, skill: Skill){
        this.hobby = hobby;
        this.skill = skill; }}Copy the code

If you need a new Person, the hobby and skill parameters automatically import the new Person. If you need a new Person, you can automatically call new and import the parameters.

  • This extended the concept of the first inversion of control, before we all want to take the initiative to go to the new, so as to create an instance, now is to create an instance of the task was assigned to a container (later, you can see, the equivalent of a master, or the creator, dedicated to create an instance, and manage dependencies), so control is reversed, Instead of actively creating instances and controlling dependencies, you switch to the container creating instances and controlling dependencies.

  • The most common approach to inversion of control is called dependency injection, which means that the container dynamically injects dependencies into the component.

  • Inversion of control can be implemented through dependency injection, so it’s essentially the same thing.

At this point, we don’t think dependency injection is useful.

Say yes, hit 1 in the comments section lol.

Let’s talk a little bit more about how dependency injection decouples the dependencies of each class. As we have just seen, The instance of Lao Wang can be passed into the instance of Hobby, which is mapped to the actual work. That is, class A depends on class B. B must have an abstract interface, so some attributes and methods of class B contained in A are in accordance with the requirements of this interface.

So you create A class C with the same interface as before, so that the dependency of class A doesn’t have to be A big problem.

So all the places where we create new A, do we have to change to class C, so that we’re done with the substitution, but ideally, we want to rely on an abstract class B and C instead of A concrete class C, because class C if we want to replace it with class D and F and so on

Reflect and Reflect the Metadata

We rely on Reflect Metadata to implement dependency injection, so we’ll mention this API.

  • Reflect is a new API for ES6
  • Reflect Metadata can add some custom information to the decorator, which we can call Metadata.

Let’s go back to the brother Wang who likes to help people and give big hats.

import 'reflect-metadata';

const person = {};
Reflect.defineMetadata('name'.'laowang', person);
Reflect.defineMetadata('name'.'run so fast', person, 'skill');

console.log(Reflect.getOwnMetadata('name',target)); // laowang
console.log(Reflect.getOwnMetadata('name',target, 'skill')) // run so fast
Copy the code

We used defineMetadata to add metadata to the Person object, a name attribute with a value of old king

Then we added a skill property to the Person object. The name property of this property is run so fast

And this metadata we actually have to get using the getOwnMetadata API, which is essentially a hash table, and it’s defined something like this

person.name = 'laowang';
person.skill.name = 'run so fast';
Copy the code

Okay, so let’s see how we can do that with decorators.

import 'reflect-metadata';

function classMetadata(key, value) {
    return function(target){
        Reflect.defineMetadata(key, value, target)
    }
}

function methodMetadata(key, value) {
    return function(target, propertyName){
        Reflect.defineMetadata(key, value, target, propertyName)
    }
}

@classMetadata('name'.'laowang')
class Person {
    @methodMetadata('name'.'run so fast')
    skill():string {
        return  'Somebody run! Come get me! '}}console.log(Reflect.getMetadata('name', Person)) // laowang
console.log(Reflect.getMetadata('name', Person.prototype, 'skill')) // run so fast
Copy the code

Reflect Metadata practice with NestJS

import 'Reflect Metadata';

class A {
  skill() {
    console.log('Helping others is the happiest thing'); }}class B {
  skill() {
    console.log('It was great to run and climb the wall.'); }}function Module(metadata) {
  const propsKeys = Object.keys(metadata);
  return (target) = > {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        Reflect.defineMetadata(property, metadata[property], target); }}}; }@Module({
  controllers: [B],
  providers: [A],
})
class C {}

const providers = Reflect.getMetadata('providers', C);
const controllers = Reflect.getMetadata('controllers', C);

console.log(providers, controllers); // [ [class A] ] [ [class B] ]
Copy the code

It’s time to implement dependency injection

We just said that a container is responsible for creating instances and managing dependencies, so let’s just say that you’re responsible for those things, and there must be classes in the data structure, or you can’t manage them, so there’s a way to add dependencies.

Implementing service registration

import 'reflect-metadata';

interface Type<T> {
  new(... args: any[]): T; } interface ClassProvider<T> {provide: Type<T>;
  useClass: Type<T>;
}

class Container {
   providers = new Map<Type<any>, ClassProvider<any>>();
  /**
   * 注册
   */
  addProvider<T>(provider: ClassProvider<T>) {
    this.providers.set(provider.provide, provider); }}class Laowang {
  age: 93;
}
const continer = new Container();

continer.addProvider<Laowang>({ provide: Laowang, useClass: Laowang });

console.log(continer.providers) 
Copy the code

Returns the following:

So Continer does one thing: register dependencies.

For instance

Now we can register dependent classes, but how do we get them? (You may be thinking, what does this have to do with dependency injection? Don’t worry, we’ll put it all together later.)

We add an Inject method to get it.

class Container {
   providers = new Map<Type<any>, ClassProvider<any>>();
  /**
   * 注册
   */
  addProvider<T>(provider: ClassProvider<T>) {
    this.providers.set(provider.provide, provider);
  }
  /** * get */
  inject(token: Type<any>){
     return this.providers.get(token)?.useClass;
  }
}

class Laowang {
  age: 93;
}
const continer = new Container();

continer.addProvider<Laowang>({ provide: Laowang, useClass: Laowang });

console.log(continer.inject(Laowang)); // class Laowang {}
Copy the code

We need to understand the syntax of decorators before we get to the following:

/** * class decorator *@param The constructor class */
function classDecorator(constructor: Function){}

/** * Attribute decorator *@param The target static property is the class constructor, and the instance property is the class's prototype object *@param Property Indicates the property name */
function propertyDecorator(target:any, property: string) {}

/** * method decorator *@param The target static method is the constructor of the class, and the instance method is the prototype object of the class *@param Propery method name *@param Descriptor method descriptor */
function methodDecorator(target: any, propery: string, descriptor: PropertyDescriptor/** * Parameter decorator * @param targetStatic methods are class constructors, and instance methods are class prototype objects * @param methodNameMethod name * @param paramIndexParameter index */function paramDecorator(target: any, methodName: string, paramIndex)
Copy the code

Implement _decorate

Next we’ll use Reflect Metadta, and let’s implement a few functions to understand what they do

class Hobby {}
const Test = (): ClassDecorator= > (target) = > {};

@Test()
class Person {
  constructor(hobby: Hobby){}}Copy the code

The above code will be converted to the following, and you’ll see why the Person constructor has an argument that you don’t need to pass in new. In fact, these parameters are retrieved from the key of ‘design: paramtypes’ as follows:

// Note that the following Hobby is a type, not an instance
const Person = _decorate([_metadata('design: paramtypes', [Hobby])], Person);
const _metadata = function (key, value) {
  return function (target) {
    Reflect.defineMetadata(key, value, target);
    return target;
  };
};
const _decorate = function (decorator, target, key, desc) {
  const argsLength = arguments.length;
  If the parameter is less than 3, it is a class decorator. If the parameter is greater than 3, it is probably a method decorator, so return desc */
  let decoratorTarget =
    argsLength < 3
      ? target
      : desc === null
      ? (desc = Object.getOwnPropertyDescriptor(target, key))
      : desc;
  decoratorTarget =
    argsLength < 3
      ? decorator(decoratorTarget)
      : argsLength > 3
      ? decorator(target, key, decoratorTarget)
      : decorator(target, key) || decoratorTarget;
};
Copy the code

Ok, so now we know that we just register our provider implementation on the continer, and then according to our _decorate above, we can bring instances registered on the provider to the Person by type, so we get them on new. The key thing to remember here is, The constructor argument is placed on the ‘Design: Paramtypes’ identifier.

Let’s take an example of what the ‘design: Paramtypes’ identifier can do.

import 'reflect-metadata';

type Constructor<T = any> = new(... args:any[]) => T;

const Test = (): ClassDecorator= > (target) = > {};

class OtherService {
  a = 1;
}

@Test(a)class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a); }}const Factory = <T>(target: Constructor<T>): T= > {
  // Get all the injected services
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) = > new provider());
  return newtarget(... args); }; Factory(TestService).testMethod();/ / 1
Copy the code

Okay, so now we’re going to implement a class method that reflects metaDta does.

class C {
    @log
    foo(n: number) {
        return n * 2; }}Copy the code

You need to define this method somewhere in your application before using @log. So let’s take a look at the implementation of the log method Decorator.

function log(target: Function, key: string, value: any) {
    return {
        value: function (. args:any[]) {
            // Do something
            // Target, key, value}}; }Copy the code

Let’s look at the translated code

var C = (function () {
    function C() {
    }
    C.prototype.foo = function (n) {
        return n * 2;
    };
    Object.defineProperty(C.prototype, "foo",
        __decorate([
            log
        ], C.prototype, "foo".Object.getOwnPropertyDescriptor(C.prototype, "foo")));
    returnC; }) ();Copy the code

The most important part is this part

Object.defineProperty(
  __decorate(
    [log],                                              // decorators
    C.prototype,                                        // target
    "foo".// key
    Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc); ;Copy the code

Ok, so with the previous __decorate code, we can see that

function log(target: Function, key: string, value: any)
Copy the code
  • target === C.prototype
  • key === “foo”
  • value === Object.getOwnPropertyDescriptor(C.prototype, “foo”)

So you can add a method decorator to do it.

The above is a simple implementation of nestJS dependency injection ideas. Let’s look at a simple implementation of Express.

Express Lite (very simple code)

In the first step, we achieve the following effect:

  • Access the /name path and return the name string
  • Access the /age path and return the age string
/ / into the express
var express = require('./express');
// Execute the express function
var app = express();
// Listen on the port
app.get('/name'.function (req,res) {
   res.end('name');
});
app.get('/age'.function (req,res) {
    res.end('age');
});
app.get(The '*'.function (req,res) {
    res.setHeader('content-type'.'text/plain; charset=utf8');
    res.end('cannot match path');
});
app.listen(3000);
Copy the code

Get method implementation:

// Declare the express function
var express = function () {
    var app = function (req,res) {
        var urlObj  = require('url').parse(req.url, true);
        var pathname = urlObj.pathname;
        var method = req.method.toLowerCase();
        // Find a matching route
        var route = app.routes.find(function (item) {
            return item.path === pathname && item.method === method;
        });
        if(route){
            route.fn(req,res);
        }
        res.end(`CANNOT  ${method} ${pathname}`)};// Add listener methods
    app.listen = function (port) {
        require('http').createServer(app).listen(port);
    };
    app.routes = [];
    // Add the get method
    app.get = function (path,fn) {
        app.routes.push({method:'get'.path:path,fn:fn});
    };
    return app;
};
module.exports = express;
Copy the code

Match all paths with * :

var route = app.routes.find(function (item) {-return item.path === pathname && item.method === method;
+      return (item.path === pathname || item.path === The '*') && item.method === method;
});
Copy the code

Express post method

POST requests from clients are processed based on the request path

  • The first argument, path, is the requested path
  • The second argument is the callback function that handles the request
app.post(path,function(req,res));
Copy the code

Use the post method:

/ / into the express
var express = require('./express');
// Execute the express function
var app = express();
// Listen on the port
app.post('/hello'.function (req,res) {
   res.end('hello');
});
app.post(The '*'.function (req,res) {
    res.end('Post not found');
});
app.listen(3000);
Copy the code

Post requests are sent using Linux commands

$ curl -X POST http://localhost:3000/hello
Copy the code

Post implementation:

Add methods to all requests

var methods = ['get'.'post'.'delete'.'put'.'options'];
methods.forEach(function (method) {
    app[method] = function (path,fn) {
        app.routes.push( { method:method, path:path, fn:fn } );
    };
});
Copy the code

The middleware

Middleware is a function that processes HTTP requests and is used to complete various specific tasks, such as checking whether the user is logged in and checking whether the user has permission to access. Its characteristics are as follows:

  • Requests and responses can be processed by one middleware and then passed to the next middleware
  • The next argument to the callback function accepts calls from other middleware, and next() in the function body continues to pass the request data
  • Middleware that return execution can be differentiated based on the path

Use of middleware:

Adding middleware

var express = require('express');
var app = express();
app.use(function (req,res,next) {
    console.log('Filter stone');
    next();
});
app.use('/water'.function (req,res,next) {
    console.log('Filter sand');
    next();
});
app.get('/water'.function (req,res) {
    res.end('water');
});
app.listen(3000);
Copy the code

Use method implementation: add middleware in the routing array

app.use = function (path, fn) {
    if(typeoffn ! = ='function'){
        fn = path;
        path = '/';
    }
    app.routes.push({method:'middle'.path:path, fn:fn});
}
Copy the code

Add Middleware judgment to app method:

- var route = app.routes.find(function (item) {-returnitem.path==pathname&&item.method==method; -}); -if(route){ - route.fn(req,res); -}var index = 0;
function next(){
    if(index>=app.routes.length){
         return res.end(`CANNOT  ${method} ${pathname}`);
    }
    var route = app.routes[index++];
    if(route.method == 'middle') {if(route.path == '/'||pathname.startsWith(route.path+'/')|| pathname==route.path){
            route.fn(req,res,next)
        }else{ next(); }}else{
        if((route.path==pathname||route.path==The '*')&&(route.method==method||route.method=='all')){
            route.fn(req,res);
        }else{
            next();
        }
    }
}
next();
Copy the code

Error middleware: Errors can be passed in next, and error middleware is executed by default

var express = require('express');
var app = express();
app.use(function (req,res,next) {
    console.log('Filter stone');
    next('stone is too big');
});
app.use('/water'.function (req,res,next) {
    console.log('Filter sand');
    next();
});
app.get('/water'.function (req,res) {
    res.end('water');
});
app.use(function (err,req,res,next) {
    console.log(err);
    res.end(err);
});
app.listen(3000);
Copy the code

Error middleware implementation: error middleware for processing

function  next(err){
    if(index>=app.routes.length){
        return res.end(`CANNOT  ${method} ${pathname}`);
    }
    var route = app.routes[index++];
+    if(err){
+        if(route.method == 'middle'&&route.fn.length==4){ + route.fn(err,req,res,next); +}else{ + next(err); + +}}else{
        if(route.method == 'middle') {if(route.path == '/'||pathname.startsWith(route.path+'/')|| pathname==route.path){
                route.fn(req,res,next)
            }else{ next(); }}else{
            if((route.path==pathname||route.path==The '*')&&(route.method==method||route.method=='all')){
                route.fn(req,res);
            }else{ next(); }} +}}Copy the code

Gets parameters and query strings

  • req.hostnameReturns the host name in the request header
  • req.pathReturns the pathname of the requested URL
  • req.queryQuery string
//http://localhost:3000/? a=1
app.get('/'.function(req,res){
    res.write(JSON.stringify(req.query))
    res.end(req.hostname+""+req.path);
});
Copy the code

Concrete implementation: add method to request

+     req.path = pathname;
+     req.hostname = req.headers['host'].split(':') [0];
+     req.query = urlObj.query;
Copy the code

Gets the params parameter

Req.params is an object consisting of all path parameters matched

app.get('/water/:id/:name/home/:age'.function (req,res) {
    console.log(req.params);
    res.end('water');
});
Copy the code

Params implementation: Add params properties

methods.forEach(function (method) {
    app[method] = function (path,fn) {
        var config = {method:method,path:path,fn:fn};
        if(path.includes(":")) {// Is the path parameter converted to re
            // And add params
            var arr = [];
            config.path   = path.replace(/:([^/]+)/g.function () {
                arr.push(arguments[1]);
                return '([^ /] +)';
            });
            config.params = arr;
        }
        app.routes.push(config);
    };
});
+ if(route.params){
+    var matchers = pathname.match(new RegExp(route.path));
+    if(matchers){
+       var params = {};
+        for(var i = 0; i<route.params.length; i++){ + params[route.params[i]] = matchers[i+1]; + } + req.params = params; + route.fn(req,res); +}else{ + next(); + +}}else{}
Copy the code

The Send method in Express

The parameter is the content to respond to, and can intelligently handle different types of data, with automatic Settings such as HEAD information, HTTP cache support, and so on when the response is output

res.send([body]);
Copy the code

When the argument is a string, this method sets the content-type to text/ HTML

app.get('/', function (req,res) {
    res.send('<p>hello world</p>');
});
Copy the code

When the argument is an Array or Object, this method returns the JSON format

app.get('/json', function (req,res) { res.send({obj:1}); }); App. Get ('/arr ', function (the req, res) {res. Send ([1, 2, 3]); });Copy the code

When the argument is of type number, this method returns the corresponding status code phrase

app.get('/status', function (req,res) { res.send(404); //not found //res.status(404).send(' not found '); Set phrase});Copy the code

Send method implementation: User-defined SEND method

res.send = function (msg) {
    var type = typeof msg;
    if (type === 'string' || Buffer.isBuffer(msg)) {
        res.contentType('text/html').status(200).sendHeader().end(msg);
    } else if (type === 'object') {
        res.contentType('application/json').sendHeader().end(JSON.stringify(msg));
    } else if (type === 'number') {
        res.contentType('text/plain').status(msg).sendHeader().end(_http_server.STATUS_CODES[msg]); }};Copy the code

Application of templates

3.1 installation ejs

NPM install ejs

$ npm install ejs
Copy the code

Set the template

Use ejS templates

var express = require('express');
var path = require('path');
var app = express();
app.set('view engine'.'ejs');
app.set('views',path.join(__dirname,'views'));
app.listen(3000);
Copy the code
3.3 render HTML

Set it to HTML format

app.set('view engine'.'html')
app.set('views',path.join(__dirname,'views')); 
app.engine('html'.require('ejs').__express);
Copy the code
3.4 Render View
  • The first parameter is the template to render
  • The second parameter is the data required for rendering
app.get('/'.function (req,res) {
    res.render('hello', {title:'hello'},function(err,data){});
});
Copy the code

Implementation of templates

Read template render

res.render = function (name, data) {
    var viewEngine = engine.viewEngineList[engine.viewType];
    if (viewEngine) {
        viewEngine(path.join(engine.viewsPath, name + '. ' + engine.viewType), data, function (err, data) {
            if (err) {
                res.status(500).sendHeader().send('view engine failure' + err);
            } else {
                res.status(200).contentType('text/html').sendHeader().send(data); }}); }else {
        res.status(500).sendHeader().send('view engine failure'); }}Copy the code

Static file server

If you want to load static files (CSS, JS, img) in a web page, you need to specify a separate directory for storing static files. When the browser sends a request for non-HTML files, the server will go to this directory to find the relevant files

var express = require('express');
var app = express();
var path = require('path');
app.use(express.static(path.join(__dirname,'public')));
app.listen(3000);
Copy the code

Static file server implementation

Configuring a Static Server

express.static = function (p) {
    return function (req, res, next) {
        var staticPath = path.join(p, req.path);
        var exists = fs.existsSync(staticPath);
        if (exists) {
            res.sendFile(staticPath);
        } else{ next(); }}};Copy the code

redirect

The redirect method allows you to redirect a url to a specified URL and specify status. The default value is 302.

  • Parameter 1 status code (optional)
  • Parameter 2 Indicates the jump path
res.redirect([status], url);
Copy the code

Use redirect

Using redirection

app.get('/'.function (req,res) {
    res.redirect('http://www.baidu.com')});Copy the code

The realization of the redirect

302 redirect

res.redirect = function (url) {
    res.status(302);
    res.headers('Location', url || '/');
    res.sendHeader();
    res.end();
};
Copy the code

Receive the BODY of the POST response

Install the body – the parser

$ npm install body-parser
Copy the code

Use the body – parser

Receive data in the request body

app.get('/login'.function (req,res) {
    res.sendFile('./login.html', {root:__dirname})
});
app.post('/user'.function (req,res) {
    console.log(req.body);
    res.send(req.body);
});
app.listen(3000);
Copy the code

The req. The realization of the body

Implement bodyParser

function bodyParser () {
    return function (req,res,next) {
        var result = ' ';
        req.on('data'.function (data) {
            result+=data;
        });
        req.on('end'.function () {
            try{
                req.body = JSON.parse(result);
            }catch(e){
                req.body = require('querystring').parse(result); } next(); })}};if((route.path==pathname||route.path==The '*')&&(route.method==method||route.method=='all')){
        route.fn(req,res);
    }else{ next(); }}Copy the code

Reference: