The project source code address: github.com/pingan8787/…

As Nodejs is more and more involved in the field of front-end, and more and more mature, I believe that many friends have tried or used Nodejs to develop server-side projects. In this post I will review Express and introduce a super plug, OvernightJS. The power is to provide TypeScript decorator support for Express routes, making it easier to develop routes and code reuse. I also want to give you a better understanding of TypeScript decorators.

Let’s take a look at this plug-in with Leo

I. Background introduction

Leo recently decided to use Express to start refactoring his blog’s server side project. After careful research and design, Leo started to do it:

// app.ts

import express, { Application, Request, Response } from 'express';

const app: Application = express();

app.get('/'.(req: Request, res: Response) = > {
  res.send('Hello World! ');
});

app.listen(3000.() = > {
  console.log('Example app listening on port 3000! ');
});
Copy the code

The tsconfig.json configuration is as follows:

{
  "compilerOptions": {
    "target": "es6"."module": "commonjs"."strict": true."esModuleInterop": true."experimentalDecorators": true.// Open the decorator
    "emitDecoratorMetadata": true.// Enable metaprogramming
    "skipLibCheck": true."forceConsistentCasingInFileNames": true}}Copy the code

Once the basic code is written, test if it can run. Leo executes on the command line using the TS-Node command line. Ts-node is used to run ts files directly.

$ ts-node app.ts
Copy the code

See the command line output:

Example app listening on port 3000!
Copy the code

The service is running and in a good mood. Leo then writes other interfaces using Express routing methods:

// app.ts

app.get('/article'.(req: Request, res: Response) = > {res.send('Hello get! ')});
app.post('/article'.(req: Request, res: Response) = > {res.send('Hello post! ')});
app.put('/article'.(req: Request, res: Response) = > {res.send('Hello put! ')});
app.delete('/article'.(req: Request, res: Response) = > {res.send('Hello delete! ')});
app.get('/article/list'.(req: Request, res: Response) = > {res.send('Hello article/list! ')});
/ /... And so forth other interfaces
Copy the code

The Express routing method is derived from one of the HTTP methods and is attached to an instance of the Express class. The following routing methods corresponding to HTTP methods are supported: GET, POST, PUT, HEAD, DELETE, options, and so on.

My colleague Robin looked at the code and said,

To address the complexity and redundancy that came with writing more and more interfaces, Leo introduced Router() for Express to create an installable modular route handler. A Router instance is a complete middleware and routing system. For this reason, they are often referred to as “microapplications.”

Leo create a new file app.router. Ts, re-implement the above interface:

// app.router.ts

import express, { Router, Request, Response } from 'express';
const router: Router = express.Router();

router.get('/'.(req: Request, res: Response) = > {res.send('Hello get! ')});
router.post('/'.(req: Request, res: Response) = > {res.send('Hello post! ')});
router.put('/'.(req: Request, res: Response) = > {res.send('Hello put! ')});
router.delete('/'.(req: Request, res: Response) = > {res.send('Hello delete! ')});
router.get('/user'.(req: Request, res: Response) = > {res.send('Hello api/user! ')});

export default router;
Copy the code

It is then used in app.ts. Since express.router () is middleware, it can be used with app.use() :

// app.ts

// Delete the original route declaration
import router from ".. /controller/app.router";
app.use('/api', router);
Copy the code

Here app.use the first parameter/API represents the root path of this group of routing objects, and the second parameter router represents a group of routing objects.

The following API interface is implemented:

  • /api
  • /api/user

After making sure all the interfaces were working correctly, Leo wondered, since each Express route consists of a route name and route handling method, why not add a plug-in to Express? Add decorators for each route to decorate. Fortunately, there is a big man to implement this plug, it is today’s protagonist — OvernightJS. Take a look at this great OvernightJS.

2. Introduction of basic knowledge

Before we introduce Overnight, let’s review decorators and Reflect:

1. A decorator

1.1 What are Decorators?

In TypeScript, Decorators are special types of declarations that can be attached to class declarations, methods, accessors, properties, or parameters, and are essentially functions. Decorators provide a way to add annotations to class declarations and members through metaprogramming syntax.

A few points to keep in mind:

  • A decorator is a declaration (expression);
  • When this expression is executed, it returns a function;
  • The input arguments to the function are respectivelytarget,namedescriptor;
  • After executing this function, it may returndescriptorObject for configurationtargetObject;

For more details on decorators, see the document TypeScript Decorators.

1.2 Classification of decorators

Decorators generally include:

  • Class decorators;
  • Property decorators;
  • Method decorators;
  • Parameter decorators;

1.3 Sample Code

Here’s an example of how to use Class decorators:

function MyDecorators(target: Function) :void {
  target.prototype.say = function () :void {
    console.log("Hello front End Self study class!");
  };
}

@MyDecorators
class LeoClass {
  constructor() {}
  say(){console.log("Hello Leo")}}let leo = new LeoClass();
leo.say(); 
// 'Hello Leo! ';
Copy the code

1.4 Compilation Results

The decorator is actually very simple, and once it’s compiled, it’s just a function, so let’s move on. Here is an example of 1.3 sample code to see its compilation result:

"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);
};
function MyDecorators(target) {
    target.prototype.say = function () {
        console.log("Hello front End Self study class!");
    };
}
let LeoClass = class LeoClass {
    constructor(){}say() { console.log("Hello Leo"); }}; LeoClass = __decorate([ MyDecorators, __metadata("design:paramtypes", [])
], LeoClass);
let leo = new LeoClass();
leo.say();
// 'Hello Leo! ';
Copy the code

As you can see from the compiled JS code, the decorator is executed when the module is imported. As follows:

LeoClass = __decorate([
    MyDecorators,
    __metadata("design:paramtypes", [])
], LeoClass);
Copy the code

1.5 summary

Let’s review decorators using the following figure.

2. Reflect Metadata API

2.1 What is Reflect?

Reflect is a built-in object added to ES6 that provides an API for intercepting and manipulating JavaScript objects. And all of Reflect’s attributes and methods are static, just like Math objects (math.random (), etc.).

For more details on Reflect, see MDN Reflect.

2.2 Why Reflect?

The core purpose is to keep JS simple so that we don’t have to write a lot of code. For example, 🌰, we can see the difference between using Reflect and not using: how do we iterate through the keys of an object when it has a Symbol in it?

const s = Symbol('foo');
const k = 'bar';
const o = { [s]: 1, [k]: 1 };

// Reflect is not used
const keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));

/ / use Reflect
Reflect.ownKeys(o);
Copy the code

Doesn’t that seem so much easier?

For more details on Reflect, see MDN Reflect.

2.3 What is Reflect Metadata

Reflect Metadata is a proposal in ES7 for adding and reading Metadata at declarative time. TypeScript already supports TypeScript in version 1.5+. You just need to:

  • npm i reflect-metadata --save.
  • intsconfig.jsonIn the configurationemitDecoratorMetadataOptions.

Reflect Metadata can be used as a decorator and has two apis:

  • useReflect.metadata() API Adding metadata;
  • useReflect.getMetadata() API Reading metadata.
@Reflect.metadata('inClass'.'A')
class LearnReflect {@Reflect.metadata('inMethod'.'B')
  public hello(): string {
    return 'hello world'; }}console.log(Reflect.getMetadata('inClass', LearnReflect)); // 'A'
console.log(Reflect.getMetadata('inMethod'.new LearnReflect(), 'hello')); // 'B'
Copy the code

Of course Reflect offers a number of other apis:

import 'reflect-metadata';

// Define metadata for objects or attributes
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// Check whether there are metadata keys on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// Check whether an object or attribute has its own metadata key
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// Gets the metadata value of the metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// Get the metadata value of the object or property's own metadata key
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);

// Get all metadata keys on the prototype chain of an object or property
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// Get all of the object or property's own metadata keys
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

// Remove metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// Apply metadata to constructors via decorators
@Reflect.metadata(metadataKey, metadataValue)
class C {
  // Apply metadata to methods (attributes) via decorators
  @Reflect.metadata(metadataKey, metadataValue)
  method(){}}Copy the code

Remember to configure tsconfig.json:

{
  "compilerOptions": {
    "target": "es5"."lib": ["es6"."dom"]."types": ["reflect-metadata"]."module": "commonjs"."moduleResolution": "node"."experimentalDecorators": true."emitDecoratorMetadata": true}}Copy the code

Two apis are used primarily in Overnight:

  • useReflect.defineMetadata() API Adding metadata;
  • useReflect.getOwnMetadata() API Reading metadata.

The following uses the Overnight Common Class decorator to introduce the use of both apis:

2.4 summary

Relect Metadata:With that in mind, we can start looking Overnight.

C. Overnight

1. Concept introduction

OvernightJS** mainly provides TypeScript decorator support for Express routes, manage routes via decorators **. Isn’t that a little abstract? Take a look at this code:

@Controller('api/posts')
export class PostController {
    @Get(':id')
    private get(req: Request, res: Response) {
        // do something}}Copy the code

This is how to use OvernightJS, simple and clear as shown above. OvernightJS provides three storage facilities:

  • OvernightJS/core;
  • OvernightJS/ Logger: Log tool store;
  • OvernightJS/ JWT: JWT;

OvernightJS/core, two other interested can see by themselves, in fact, the core is the same.

2. Fast learning by OvernightJS/core

2.1 installation OvernightJS/core

$ npm install --save @overnightjs/core express 
$ npm install --save-dev @types/express
Copy the code

2.2 OvernightJS/core Example code

Let’s start with what our sample code needs to do:

  1. UserControllerClass, responsible for managementThe business logicController;
  2. ServerControllerClass, responsible for managementService logicController;
  3. Perform service startup;

First, import the required dependencies:

import { Controller, Get, Server } from '@overnightjs/core';
import { Request, Response } from 'express';
import * as bodyParser from 'body-parser';
const port = 3000;
Copy the code

Step 2: Implement the UserController class:

@Controller('/users')
class UserController {
    @Get('/:id')
    private get(req: Request, res: Response) {
        return res.send(`hello, your id is:${req.params.id}`)}@Get('/list')
    private getList(req: Request, res: Response) {
        return res.send([
          {name: "leo".age: 17},
          {name: "robin".age: 19}}}])Copy the code

“/users” path is used to specify a routing address for the current routing Controller, which is the root of the group of routes. All interface paths implemented in this class are based on this root path. Then in UserController class, with @get decorator by OvernightJS/core, bind route with “/:id” and “/list” paths respectively.

The final routing addresses implemented by the UserController class include:

  • /user/:id
  • /users/list

Step 3: Implement the ServerController class:

class ServerController extends Server {
    constructor() {
        super(a);this.app.use(bodyParser.json());
        super.addControllers(new UserController());
    }
    publicstart(port? :number) :void {
        this.app.listen(port, () = > {console.log('Started successfully, port number:',port)}); }}Copy the code

OvernightJS Server class inherited by OvernightJS/core Add the class to the OvernightJS/core controller array by calling super.addControllers(new UserController()). Also in this class, we declare the start method to start the server.

Step 4, implement the server startup logic:

const server = new ServerController();
server.start(port);
Copy the code

Starting the server is fairly simple

The entire implementation of the sample code flow is as follows: declare two classes: UserController and ServerController, respectively, for the controller of the business logic and the controller of the service logic. Finally, in the main entrance to de-instantiate, and execute the start method of the instantiation result to start the service. The final complete code is as follows:

import { Controller, Get, Server } from '@overnightjs/core';
import { Request, Response } from 'express';
import * as bodyParser from 'body-parser';
const port = 3000;

@Controller('users')
class UserController {
    @Get(':id')
    private get(req: Request, res: Response) {
        return res.send(`hello, your id is:${req.params.id}`)}@Get('list')
    private get(req: Request, res: Response) {
        return res.send([
          {name: "leo".age: 17},
          {name: "robin".age: 19}}}])class ServerController extends Server {
    constructor() {
        super(a);this.app.use(bodyParser.json());
        super.addControllers(new UserController());
    }
    publicstart(port? :number) :void {
        this.app.listen(port, () = > {console.log('Started successfully, port number:',port)}); }}const server = new ServerController();
server.start(port);
Copy the code

 

3. OvernightJS/core Upholsterer analysis

In reading the source CODE, I will OvernightJS/core all the decorators according toSource directory structure dimensionThe results are as follows:OvernightJS provides four main types of upholstery by OvernightJS/core

4. OvernightJS/ Core structure analysis

OvernightJS/core structure design is simple, the structure is as follows:OvernightJS/core, two main categories are provided:ServerClasses andDecoratorsRelated methods. Among themServerIn the classaddConterllersMethod is the key, which is covered in more detail in the next section. Ha ha

5. Association with Express

To review Express, we often define an interface via app.use(path, route) :

app.use(path, route);
Copy the code

What about OvernightJS? What is the addConterllers method mentioned in the previous section?

OvernightJS is essentially associated with Express by calling addConterllers(). Can be understood as a bridge between OvernightJS and Express, it takes the routing controller defined by OvernightJS/core as parameter, add the route to Express by use method of Express, Implement Express route registration.

Let’s look at what the addControllers method does in the source code:

// core/lib/Server.ts

publicaddControllers( controllers: Controller | Controller[], routerLib? : RouterLib, globalMiddleware? : RequestHandler, ):void {
    controllers = (controllers instanceof Array)? controllers : [controllers];const routerLibrary: RouterLib = routerLib || Router;
    controllers.forEach((controller: Controller) = > {
        if (controller) {
            const routerAndPath: IRouterAndPath | null = this.getRouter(routerLibrary, controller);
            if (routerAndPath) {
                if (globalMiddleware) {
                    this.app.use(routerAndPath.basePath, globalMiddleware, routerAndPath.router);
                } else {
                    this.app.use(routerAndPath.basePath, routerAndPath.router); }}}}); }Copy the code

Let’s simplify the above code and keep the source of the core functions:

publicaddControllers( controllers: Controller | Controller[], routerLib? : RouterLib, globalMiddleware? : RequestHandler, ):void {
  / /... Omit other code
    controllers = (controllers instanceof Array)? controllers : [controllers]; controllers.forEach((controller: Controller) = > {
        this.app.use(routerAndPath.basePath, routerAndPath.router);
    });
}
Copy the code

As you can see from the code above, the addControllers method supports passing in a single controller or an array of controllers, traversing each controller through forEach, And pass path and router as parameters into app.use method to realize Express route registration.

Overnight VS Express

OvernightJS provides TypeScript decorator support for Express routes to manage routes.

So what’s the difference between OvernightJS and no OvernightJS? OvernightJS supports API/Users /: ID with local startup 4000 port.

1. OvernightJS implementation

Start by implementing the entry file, which starts the service by instantiating the ServerController class and executing the start method of the instantiation structure:

// customApp.ts

import ServerController from ".. /controller/custom.server.controller";
const port = 4000;

const server = new ServerController();
server.start(port);
Copy the code

The tsconfig.json configuration is as follows:

{
  "compilerOptions": {
    "target": "es6"."module": "commonjs"."strict": true."esModuleInterop": true."experimentalDecorators": true."skipLibCheck": true."forceConsistentCasingInFileNames": true}}Copy the code

The general process is shown in the code above, and the next step is to implement the specific ServerController class:

// controller/custom.server.controller.ts

import { Server } from "@overnightjs/core";
import RouterController from "./custom.router.controller";

class ServerController extends Server {
    constructor() {
        super(a);super.addControllers(new RouterController());
    }
    publicstart(port? :number) :void {
        this.app.listen(port, () = > {
            console.log('Started successfully, port number:',port)}); }}export default ServerController;
Copy the code

RouterController: RouterController: RouterController: RouterController: RouterController

// controller/custom.router.controller.ts
import { Request, Response } from 'express';
import { Controller, Get, Put } from '@overnightjs/core';

@Controller("api/users")
class RouterController {
    @Get(":id")
    private get(req:Request, res:Response): any{
        res.send("hello leo!")}}export default RouterController;
Copy the code

 

2. The Express

As before, here also implements the entry file:

// app.ts

import ServerController from ".. /controller/server.controller";
const port = 4000;

const server = new ServerController();
server.start(port);
Copy the code

Then implement the concrete ServerController class:

// controller/server.controller/.ts

import express, { Application } from 'express';
import RouterController from "./router.controller";

class ServerController {
    app: Application = express();
    constructor(){this.addControllers()};
    public addControllers(){
        const Router = new RouterController().getController();
        this.app.use('/api/users', Router);
    }
    publicstart(port? :number) :void {
        this.app.listen(port, () = > {console.log('Started successfully, port number:',port)}); }}export default ServerController;
Copy the code

RouterController class

// controller/router.controller.ts

import express, { Router, Application, Request, Response, NextFunction } from "express";

class RouterController {
    router: Router = express.Router();
    constructor() { this.addControllers()};
    public getController = (): Router= > this.router;
    public addControllers(): void {
        this.router.get("/:id".this.get);
    }
    public get (req: Request, res: Response, next: NextFunction){
        res.send("hello leo!") next(); }}export default RouterController;
Copy the code

 

3. Compare them

I believe that the friends who see here have a general understanding of the first two implementation methods. Next, through a picture, to see the difference between the two implementation.

Five, the summary

This paper mainly introduces the basic use of OvernightJS and Express routing function, and then use them to achieve the same routing function, the advantages of OvernightJS are compared. If you recommend Express + TypeScript, try OvernightJS