乐趣区

关于前端:我为-Express-开了外挂

本我的项目源码地址:
https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/LearnSource/OvernightDemo/

随着 Nodejs 在前端波及畛域越来越广,也越来越成熟,置信很多敌人曾经尝试或应用过 Nodejs 开发服务端我的项目了。
本文我将和大家一起回顾 Express,而后介绍一个超级外挂——OvernightJS,它弱小的中央在于,它将为 Express 路由提供 TypeScript 装璜器反对,使得咱们开发路由更加简略,代码复用性更好。
这里也心愿帮忙大家对 TypeScript 的装璜器有更深理解。

接下来跟本文配角 Leo 一起来看看这个外挂吧~

一、背景介绍

最近 Leo 打算应用 Express 来开始重构本人博客的服务端我的项目,通过认真钻研和设计,并确定完计划,Leo 开始下手啦:

// 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!');
});

其中 tsconfig.json 配置如下:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
        "esModuleInterop": true,
    "experimentalDecorators": true, // 开启装璜器
    "emitDecoratorMetadata": true,  // 开启元编程
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

根本代码写完,测试能不能跑起来。
Leo 在命令行应用 ts-node 命令行执行。(ts-node 用来间接运行 ts 文件,具体介绍请查看文档,这里不细讲咯):

$ ts-node app.ts

看到命令行输入:

Example app listening on port 3000!

服务跑起来了,心情愉快。
接下来 Leo 应用 Express 的路由办法写了其余接口:

// 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!')});
// ... 等等其余接口

Express 路由办法派生自 HTTP 办法之一,附加到 express 类的实例。反对对应于 HTTP 办法的以下路由办法:get、post、put、head、delete、options 等等。

共事 Robin 看了看代码,问到:

随着接口越写越多,代码未免呈现简单和冗余的状况,为了解决这个问题,Leo 引入 Express 的 Router(),来创立 可装置的模块化路由处理程序 。Router 实例是 残缺的中间件 路由零碎 。因而,经常将其称为“ 微型应用程序”。

Leo 新建文件 app.router.ts,从新实现下面接口:

// 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;

接着在 app.ts 中应用,因为express.Router() 是个中间件,因而能够应用 app.use() 来应用:

// app.ts

// 删除原来路由申明
import router from "../controller/app.router";
app.use('/api', router);

这里 app.use 第一个参数 /api 示意这一组路由对象的根门路,第二个参数 router 示意一组路由对象。

于是就实现了上面 API 接口:

  • /api
  • /api/user

确定所有接口失常运行后,Leo 推敲着,既然 Express 每个路由都是由 路由名称 路由解决办法 组成,那为什么不能给 Express 加个外挂?为每个路由增加装璜器来装璜。
侥幸的是,曾经有大佬实现这个外挂了,它就是明天配角——OvernightJS。
上面一起看看这个很棒的 OvernightJS 吧。

二、基础知识介绍


在开始介绍 Overnight 之前,咱们先回顾下“装璜器”和“Reflect”:

1. 装璜器

1.1 什么是装璜器?

TypeScript 中,装璜器(Decorators)是一种非凡类型的申明,它可能被附加到类申明、办法、拜访符、属性或参数上,实质上还是个函数
装璜器为咱们在类的申明及成员上通过 元编程语法 增加标注提供了一种形式。

须要记住这几点:

  • 装璜器是一个申明(表达式);
  • 该表达式被执行后,返回一个函数
  • 函数的入参别离为 targetnamedescriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象;

更多装璜器具体介绍,请浏览文档《TypeScript 装璜器》。

1.2 装璜器分类

装璜器个别包含:

  • 类装璜器(Class decorators);
  • 属性装璜器(Property decorators);
  • 办法装璜器(Method decorators);
  • 参数装璜器(Parameter decorators);

1.3 示例代码

这里以类装璜器(Class decorators)为例,介绍如何应用装璜器:

function MyDecorators(target: Function): void {target.prototype.say = function (): void {console.log("Hello 前端自习课!");
  };
}

@MyDecorators
class LeoClass {constructor() {}
  say(){console.log("Hello Leo")}
}

let leo = new LeoClass();
leo.say(); 
// 'Hello Leo!';

1.4 编译后果

装璜器实际上非常简单,编译进去当前,只是个函数,咱们接着看。
这里以《1.3 示例代码》为例,看看它的编译后果:

"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 前端自习课!");
    };
}
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!';

其实就是 __decorate 函数啦,具体大家能够自行细看咯~
从编译后 JS 代码中能够看出,装璜器是在模块导入时便执行的。如下:

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

1.5 小结

接下来通过下图来回顾装璜器的常识。

2. Reflect Metadata API

2.1 什么是 Reflect?

Reflect(即反射)是 ES6 新增的一个 内置对象 ,它提供用来 拦挡和操作 JavaScript 对象的 API。并且 Reflect 的所有属性和办法都是动态的,就像 Math 对象(Math.random() 等)。

更多 Reflect 具体介绍,请浏览文档《MDN Reflect》。

2.2 为什么呈现 Reflect?

其外围目标,是为了放弃 JS 的简略,让咱们能够不必写很多代码,这里举个栗子????,看看有应用 Reflect 和没应用有什么区别:
当对象里有 Symbol 时,如何遍历对象的 keys

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

// 没有应用 Reflect
const keys = Object.getOwnPropertyNames(o).concat(Object.getOwnPropertySymbols(o));

// 应用 Reflect
Reflect.ownKeys(o);

这看起来是不是简略多了?

更多 Reflect 具体介绍,请浏览文档《MDN Reflect》。

2.3 什么是 Reflect Metadata

Reflect Metadata 是 ES7 的一个提案,它次要用来 在申明的时增加和读取元数据。TypeScript 在 1.5+ 的版本曾经反对它,你只须要:

  • npm i reflect-metadata --save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项。

Reflect Metadata 能够当做装璜器应用,有两个 API:

  • 应用 Reflect.metadata() API 增加元数据
  • 应用 Reflect.getMetadata() API 读取元数据
@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'

当然 Reflect 提供很多其余 API:

import 'reflect-metadata';

// 定义对象或属性的元数据
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// 查看对象或属性的原型链上是否存在元数据键
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// 查看对象或属性是否存在本人的元数据键
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// 获取对象或属性原型链上元数据键的元数据值
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// 获取对象或属性的本人的元数据键的元数据值
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);

// 获取对象或属性原型链上的所有元数据键
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// 获取对象或属性的所有本人的元数据键
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

// 从对象或属性中删除元数据
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// 通过装璜器将元数据利用于构造函数
@Reflect.metadata(metadataKey, metadataValue)
class C {// 通过装璜器将元数据利用于办法(属性)
  @Reflect.metadata(metadataKey, metadataValue)
  method() {}
}

须要记得配置 tsconfig.json:

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

在 Overnight 中次要应用有两个 API:

  • 应用 Reflect.defineMetadata() API 增加元数据
  • 应用 Reflect.getOwnMetadata() API 读取元数据

上面以 Overnight 中类装璜器(Common Class)来介绍这两个 API 应用过程:

2.4 小结

这里回顾下 Relect Metadata 的常识:

了解分明后面两个知识点后,咱们接下来开始看看 Overnight。

三、Overnight 详解

1. 概念介绍

OvernightJS 次要是为 Express 路由提供 TypeScript 装璜器反对,通过装璜器来治理路由
是不是形象了点?那看看上面这段代码吧:

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

如下面代码所示,OvernightJS 就是这样应用,简略,明了。
另外 OvernightJS 共提供了三个库:

  • OvernightJS/core:外围库;
  • OvernightJS/logger:日志记录工具库;
  • OvernightJS/jwt:JWT 库;

接下来次要介绍 OvernightJS/core 外围库,其余两个有趣味能够本人看哈,触类旁通,其实外围一样的。

2. OvernightJS/core 疾速上手

2.1 装置 OvernightJS/core

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

2.2 OvernightJS/core 示例代码

首先介绍下咱们示例代码须要实现的性能:

  1. UserController 类,负责管理 业务逻辑 的控制器;
  2. ServerController 类,负责管理 服务逻辑 的控制器;
  3. 执行服务启动;

第一步,导入须要的依赖:

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

第二步,实现 UserController 类:

@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}
        ])
    }
}

在申明 UserController 类时,应用 OvernightJS/core 的 @Controller 装璜器,应用 "/users" 门路作为参数,作用是为以后路由控制器指定一个路由地址,能够了解为这组路由的“根门路”,该类中实现的所有接口门路,都会以该“根门路”为根底。
而后在UserController 类中,通过 OvernightJS/core 提供 @Get 装璜器,别离应用 "/:id" 和 "/list" 门路作为参数,绑定路由。

最终 UserController 类实现的路由地址包含:

  • /user/:id
  • /users/list

第三步,实现 ServerController 类:

class ServerController extends Server {constructor() {super();
        this.app.use(bodyParser.json());
        super.addControllers(new UserController());
    }
    public start(port?: number): void {this.app.listen(port, () => {console.log('启动胜利,端口号:',port)});
    }
}

ServerController 类继承 OvernightJS/core 提供的 Server 类,通过在构造函数中调用 super.addControllers(new UserController()) 来实现将后面申明好的路由控制器类,增加到 OvernightJS/core 对立治理的控制器数组中。
另外在该类中,咱们还申明 start 办法,用来启动服务器。

第四步,实现启动服务器逻辑:

const server = new ServerController();
server.start(port);

这里启动服务器就相当简略咯~~

 整个实现示例代码的流程如下:
申明了两个类:UserControllerServerController,别离为 业务逻辑的控制器 服务逻辑的控制器 ,最初在主入口中去实例化,并执行实例化后果的 start 办法启动服务。
 
最初残缺代码如下:

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();
        this.app.use(bodyParser.json());
        super.addControllers(new UserController());
    }
    public start(port?: number): void {this.app.listen(port, () => {console.log('启动胜利,端口号:',port)});
    }
}

const server = new ServerController();
server.start(port);

 

3. OvernightJS/core 装璜器剖析

在浏览源码过程中,我将 OvernightJS/core 中所有的装璜器依照 源码目录构造维度 做了分类,后果如下:

通过上图能够清晰看出,OvernightJS/core 为咱们提供了四个大类的装璜器,具体的应用形式,还请看看官网文档啦~

4. OvernightJS/core 架构剖析

OvernightJS/core 结构设计上还是比较简单,大抵如下架构:

在 OvernightJS/core 中,次要提供两个大类:Server 类和 Decorators 相干办法。
其中 Server 类中的 addConterllers 办法是要害,下一节将具体介绍。哈哈

5. OvernightJS/core 与 Express 关联

回顾下 Express,咱们常常通过 app.use(path, route) 来定义一个接口:

app.use(path, route);

那么在 OvernightJS 中呢??
前一大节提到的addConterllers 办法是什么呢??

 其实 OvernightJS 实质上是通过调用 addConterllers() 办法来和 Express 做关联。
能够了解为 OvernightJS 与 Express 之间的桥梁,它将 OvernightJS/core 定义好的路由控制器作为参数,通过 Express 的 use 办法,将路由增加的 Express 中,实现 Express 路由注册。

 咱们看下源码中addControllers 办法做了什么事件:

// core/lib/Server.ts

public addControllers(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);
                }
            }
        }
    });
}

咱们简化下下面代码,保留外围性能的源码:

public addControllers(controllers: Controller | Controller[],
    routerLib?: RouterLib,
    globalMiddleware?: RequestHandler,
): void {
  // ... 省略其余代码
    controllers = (controllers instanceof Array) ? controllers : [controllers];
    controllers.forEach((controller: Controller) => {this.app.use(routerAndPath.basePath, routerAndPath.router);
    });
}

从下面代码能够看出,addControllers 办法反对传入单个 controller 或一个数组的 controller,办法内通过 forEach 遍历每个控制器,并将 path 和 router 作为参数传入 app.use 办法中,实现 Express 的路由注册。

四、Overnight VS Express

从后面概念介绍中,咱们晓得:OvernightJS 次要是为 Express 路由提供 TypeScript 装璜器反对,通过装璜器来治理路由。

那么 应用 OvernightJS 跟没有应用有什么区别呢?
上面咱们别离通过 OvernightJS 和 Express 实现雷同性能,性能包含:本地启动 4000 端口,反对 api/users/:id 接口。

1. OvernightJS 实现

首先实现入口文件,其中通过实例化 ServerController 类,并执行实例化构造的 start 办法来启动服务:

// customApp.ts

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

const server = new ServerController();
server.start(port);

其中 tsconfig.json 配置如下:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

大抵过程如下面代码,接下来要开始实现具体的 ServerController  类:

// controller/custom.server.controller.ts

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

class ServerController extends Server {constructor() {super();
        super.addControllers(new RouterController());
    }
    public start(port?: number): void {this.app.listen(port, () => {console.log('启动胜利,端口号:',port)});
    }
}

export default ServerController;

 
最初实现 RouterController  类,该 API 下的路由办法,都定义在这个类中:

// 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;

 

2. Express 实现

跟后面一下,这里也是先实现入口文件:

// app.ts

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

const server = new ServerController();
server.start(port);

而后实现具体的 ServerController  类:

// 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);
    }
    public start(port?: number): void {this.app.listen(port, () => {console.log('启动胜利,端口号:',port)});
    }
}

export default ServerController;

 
最初实现 RouterController  类:

// 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;

 

3. 两者比拟

置信看到这里的敌人,对后面两种实现办法大抵理解了,接下来通过一张图,来看看总结两者实现的区别吧。

五、总结

本文次要介绍 OvernightJS 与 Express 路由性能的根本应用,而后别离用两者实现雷同的路由性能,比照得出 OvernightJS 的长处,举荐应用 Express + TypeScript 的敌人能够尝试应用 OvernightJS 咯~ 

退出移动版