我爲 Express 開了外掛

本項目源碼地址:
https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/LearnSource/OvernightDemo/

隨着 Nodejs 在前端涉及領域愈來愈廣,也愈來愈成熟,相信不少朋友已經嘗試或使用過 Nodejs 開發服務端項目了。
本文我將和你們一塊兒回顧 Express,而後介紹一個超級外掛——OvernightJS,它強大的地方在於,它將爲 Express 路由提供 TypeScript 裝飾器支持,使得咱們開發路由更加簡單,代碼複用性更好。
這裏也但願幫助你們對 TypeScript 的裝飾器有更深瞭解。javascript

接下來跟本文主角 Leo 一塊兒來看看這個外掛吧~html

1、背景介紹

最近 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 配置以下:java

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
        "esModuleInterop": true,
    "experimentalDecorators": true, // 開啓裝飾器
    "emitDecoratorMetadata": true,  // 開啓元編程
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

基本代碼寫完,測試能不能跑起來。
Leo 在命令行使用 ts-node 命令行執行。(ts-node 用來直接運行 ts 文件,詳細介紹請查看文檔,這裏不細講咯):node

$ ts-node app.ts

看到命令行輸出:git

Example app listening on port 3000!

服務跑起來了,心情愉快。
接下來 Leo 使用 Express 的路由方法寫了其餘接口:es6

// 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 看了看代碼,問到:
Overnight-Learn-1.pnggithub

隨着接口越寫越多,代碼難免出現複雜和冗餘的狀況,爲了解決這個問題,Leo 引入 Express 的 Router() ,來建立可安裝的模塊化路由處理程序。Router 實例是完整的中間件路由系統。所以,經常將其稱爲「微型應用程序」。typescript

Leo 新建文件 app.router.ts ,從新實現上面接口:express

// 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 吧。

2、基礎知識介紹

Overnight-Learn-2.png
在開始介紹 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 小結

接下來經過下圖來回顧裝飾器的知識。
Decorator-Introduce.png

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 使用過程:
Reflect-Metadata-Use.png

2.4 小結

這裏回顧下 Relect Metadata 的知識:
Reflect-Metadata-Introduce.png
理解清楚前面兩個知識點後,咱們接下來開始看看 Overnight。

3、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 中全部的裝飾器按照源碼目錄結構維度作了分類,結果以下:
Overnight-Decorators-Classify.png
經過上圖能夠清晰看出,OvernightJS/core 爲咱們提供了四個大類的裝飾器,具體的使用方式,還請看看官網文檔啦~

4. OvernightJS/core 架構分析

OvernightJS/core 結構設計上仍是比較簡單,大體以下架構:
Overnight-Design.png
在 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 的路由註冊。

4、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. 二者比較

相信看到這裏的朋友,對前面兩種實現方法大體瞭解了,接下來經過一張圖,來看看總結二者實現的區別吧。
Overnight-VS-Express.png

5、總結

本文主要介紹 OvernightJS 與 Express 路由功能的基本使用,而後分別用二者實現相同的路由功能,對比得出 OvernightJS 的優勢,推薦使用 Express + TypeScript 的朋友能夠嘗試使用 OvernightJS 咯~ 

相關文章
相關標籤/搜索