为 Express 开外挂
作者:pingan8787
来源:SegmentFault 思否社区
https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Gist/LearnSource/OvernightDemo/
本文我将和大家一起回顾 Express,然后介绍一个超级外挂——OvernightJS,它强大的地方在于,它将为 Express 路由提供 TypeScript 装饰器支持,使得我们开发路由更加简单,代码复用性更好。这里也希望帮助大家对 TypeScript 的装饰器有更深了解。
一、背景介绍
// 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!');
});
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true, // 开启装饰器
"emitDecoratorMetadata": true, // 开启元编程
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
$ 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!')});
// ... 等等其他接口
// 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);
/api
/api/user
幸运的是,已经有大佬实现这个外挂了,它就是今天主角——OvernightJS。
下面一起看看这个很棒的 OvernightJS 吧。
二、基础知识介绍
1. 装饰器
1.1 什么是装饰器?
装饰器为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
装饰器是一个声明(表达式); 该表达式被执行后,返回一个函数; 函数的入参分别为 target、name 和 descriptor; 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象;
1.2 装饰器分类
类装饰器(Class decorators); 属性装饰器(Property decorators); 方法装饰器(Method decorators); 参数装饰器(Parameter decorators);
1.3 示例代码
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!';
从编译后 JS 代码中可以看出,装饰器是在模块导入时便执行的。如下:
LeoClass = __decorate([
MyDecorators,
__metadata("design:paramtypes", [])
], LeoClass);
1.5 小结
2. Reflect Metadata API
2.1 什么是 Reflect ?
2.2 为什么出现 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);
2.3 什么是 Reflect Metadata
npm i reflect-metadata --save。 在 tsconfig.json 里配置 emitDecoratorMetadata 选项。
使用 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'
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
}
}
使用 Reflect.defineMetadata() API 添加元数据; 使用 Reflect.getOwnMetadata() API 读取元数据。
2.4 小结
三、Overnight 详解
1. 概念介绍
是不是抽象了点?那看看下面这段代码吧:
@Controller('api/posts')
export class PostController {
@Get(':id')
private get(req: Request, res: Response) {
// do something
}
}
另外 OvernightJS 共提供了三个库:
OvernightJS/core:核心库; OvernightJS/logger:日志记录工具库; OvernightJS/jwt:JWT 库;
2. OvernightJS/core 快速上手
2.1 安装 OvernightJS/core
$ npm install --save @overnightjs/core express
$ npm install --save-dev @types/express
2.2 OvernightJS/core 示例代码
UserController 类,负责管理业务逻辑的控制器; ServerController 类,负责管理服务逻辑的控制器; 执行服务启动;
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 getList(req: Request, res: Response) {
return res.send([
{name: "leo", age: 17},
{name: "robin", age: 19}
])
}
}
然后在UserController 类中,通过 OvernightJS/core 提供 @Get 装饰器,分别使用 "/:id" 和 "/list" 路径作为参数,绑定路由。
/user/:id /users/list
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)});
}
}
另外在该类中,我们还声明 start 方法,用来启动服务器。
const server = new ServerController();
server.start(port);
声明了两个类:UserController 和 ServerController ,分别为业务逻辑的控制器和服务逻辑的控制器,最后在主入口中去实例化,并执行实例化结果的 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 装饰器分析
4. OvernightJS/core 架构分析
5. OvernightJS/core 与 Express 关联
app.use(path, route);
前一小节提到的addConterllers 方法是什么呢??
可以理解为 OvernightJS 与 Express 之间的桥梁,它将 OvernightJS/core 定义好的路由控制器作为参数,通过 Express 的 use 方法,将路由添加的 Express 中,实现 Express 路由注册。
// 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);
});
}
四、Overnight VS Express
下面我们分别通过 OvernightJS 和 Express 实现相同功能,功能包括:本地启动 4000 端口,支持 api/users/:id 接口。
1. OvernightJS 实现
// customApp.ts
import ServerController from "../controller/custom.server.controller";
const port = 4000;
const server = new ServerController();
server.start(port);
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
// 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);
// 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. 两者比较
五、总结
评论