如何让你的Express飞起来

接下来本文的重心将围绕 装饰器 的应用展开,不过在分析装饰器在 OvernightJS 的应用之前,阿宝哥先来简单介绍一下 OvernightJS。

目前成都创新互联已为上千多家的企业提供了网站建设、域名、虚拟主机、网站托管、服务器托管、企业网站设计、江州网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。

一、OvernightJS 简介 

 
 
 
 
  1. TypeScript decorators for the ExpressJS Server.

OvernightJS 是一个简单的库,用于为要调用 Express 路由的方法添加 TypeScript 装饰器。此外,该项目还包含了用于管理 json-web-token 和打印日志的包。

1.1 OvernightJS 特性
OvernightJS 并不是为了替代 Express,如果你之前已经掌握了 Express,那你就可以快速地学会它。OvernightJS 为开发者提供了以下特性:

  • 使用 @Controller 装饰器定义基础路由;
  • 提供了把类方法转化为 Express 路由的装饰器(比如 @Get,@Put,@Post,@Delete);
  • 提供了用于处理中间件的 @Middleware 和 @ClassMiddleware 装饰器;
  • 提供了用于处理异常的 @ErrorMiddleware 装饰器;
  • 提供了 @Wrapper 和 @ClassWrapper 装饰器用于包装函数;
  • 通过 @ChildControllers 装饰器支持子控制器。

出于篇幅考虑,阿宝哥只介绍了 OvernightJS 与装饰器相关的部分特性。了解完这些特性,我们来快速体验一下 OvernightJS。

1.2 OvernightJS 入门
1.2.1 初始化项目
首先新建一个 overnight-quickstart 项目,然后使用 npm init -y 命令初始化项目,然后在命令行中输入以下命令来安装项目依赖包:

 
 
 
 
  1. $ npm i @overnightjs/core express -S

在 Express 项目中要集成 TypeScript 很简单,只需安装 typescript 这个包就可以了。但为了在开发阶段能够在命令行直接运行使用 TypeScript 开发的服务器,我们还需要安装 ts-node 这个包。要安装这两个包,我们只需在命令行中输入以下命令:

 
 
 
 
  1. $ npm i typescript ts-node -D

1.2.2 为 Node.js 和 Express 安装声明文件
声明文件是预定义的模块,用于告诉 TypeScript 编译器的 JavaScript 值的形状。类型声明通常包含在扩展名为 .d.ts 的文件中。这些声明文件可用于所有最初用 JavaScript 而非 TypeScript 编写的库。

幸运的是,我们不需要重头开始为 Node.js 和 Express 定义声明文件,因为在 Github 上有一个名为 DefinitelyTyped 项目已经为我们提供了现成的声明文件。

要安装 Node.js 和 Express 对应的声明文件,我们只需要在命令行执行以下命令就可以了:

 
 
 
 
  1. $ npm i @types/node @types/express -D

该命令成功执行之后,package.json 中的 devDependencies 属性就会新增 Node.js 和 Express 对应的依赖包版本信息:

 
 
 
 
  1. {
  2.   "devDependencies": {
  3.      "@types/express": "^4.17.8",
  4.      "@types/node": "^14.11.2",
  5.      "ts-node": "^9.0.0",
  6.      "typescript": "^4.0.3"
  7.   }
  8. }

1.2.3 初始化 TypeScript 配置文件
为了能够灵活地配置 TypeScript 项目,我们还需要为本项目生成 TypeScript 配置文件,在命令行输入 tsc --init 之后,项目中就会自动创建一个 tsconfig.json 的文件。对于本项目来说,我们将使用以下配置项:

 
 
 
 
  1. {
  2.   "compilerOptions": {
  3.     "target": "es6",
  4.     "module": "commonjs",
  5.     "rootDir": "./src",
  6.     "outDir": "./build",
  7.     "esModuleInterop": true,
  8.     "experimentalDecorators": true,
  9.     "strict": true
  10.   }
  11. }

1.2.4 创建简单的 Web 服务器
在创建简单的 Web 服务器之前,我们先来初始化项目的目录结构。首先在项目的根目录下创建一个 src 目录及 controllers 子目录:

 
 
 
 
  1. ├── src
  2. │   ├── controllers
  3. │   │   └── UserController.ts
  4. │   └── index.ts

接着新建 UserController.ts 和 index.ts 这两个文件并分别输入以下内容:

UserController.ts

 
 
 
 
  1. import { Controller, Get } from "@overnightjs/core";
  2. import { Request, Response } from "express";
  3. @Controller("api/users")
  4. export class UserController {
  5.   @Get("")
  6.   private getAll(req: Request, res: Response) {
  7.     return res.status(200).json({
  8.       message: "成功获取所有用户",
  9.     });
  10.   }
  11. }

index.ts

 
 
 
 
  1. import { Server } from "@overnightjs/core";
  2. import { UserController } from "./controllers/UserController";
  3. const PORT = 3000;
  4. export class SampleServer extends Server {
  5.   constructor() {
  6.     super(process.env.NODE_ENV === "development");
  7.     this.setupControllers();
  8.   }
  9.   private setupControllers(): void {
  10.     const userController = new UserController();
  11.     super.addControllers([userController]);
  12.   }
  13.   public start(port: number): void {
  14.     this.app.listen(port, () => {
  15.       console.log(`️[server]: Server is running at http://localhost:${PORT}`);
  16.     });
  17.   }
  18. }
  19. const sampleServer = new SampleServer();
  20. sampleServer.start(PORT);

完成上述步骤之后,我们在项目的 package.json 中添加一个 start 命令来启动项目:

 
 
 
 
  1. {
  2.   "scripts": {
  3.     "start": "ts-node ./src/index.ts"
  4.   },
  5. }

添加完 start 命令,我们就可以在命令行中通过 npm start 来启动 Web 服务器了。当服务器成功启动之后,命令行会输出以下消息:

 
 
 
 
  1. > ts-node ./src/index.ts
  2. ️[server]: Server is running at http://localhost:3000

接着我们打开浏览器访问 http://localhost:3000/api/users 这个地址,你就会看到 {"message":"成功获取所有用户"} 这个信息。

1.2.5 安装 nodemon
为了方便后续的开发,我们还需要安装一个第三方包 nodemon。对于写过 Node.js 应用的小伙伴来说,对 nodemon 这个包应该不会陌生。nodemon 这个包会自动检测目录中文件的更改,当发现文件异动时,会自动重启 Node.js 应用程序。

同样,我们在命令行执行以下命令来安装它:

 
 
 
 
  1. $ npm i nodemon -D

安装完成后,我们需要更新一下前面已经创建的 start 命令:

 
 
 
 
  1. {
  2.   "scripts": {
  3.     "start": "nodemon ./src/index.ts"
  4.   }
  5. }

好的,现在我们已经知道如何使用 OvernightJS 来开发一个简单的 Web 服务器。接下来,阿宝哥将带大家一起来分析 OvernightJS 是如何使用 TypeScript 装饰器实现上述的功能。

二、OvernightJS 原理分析
在分析前面示例中 @Controller 和 @Get 装饰器原理前,我们先来看一下直接使用 Express 如何实现同样的功能:

 
 
 
 
  1. import express, { Router, Request, Response } from "express";
  2. const app = express();
  3. const PORT = 3000;
  4. class UserController {
  5.   public getAll(req: Request, res: Response) {
  6.     return res.status(200).json({
  7.       message: "成功获取所有用户",
  8.     });
  9.   }
  10. }
  11. const userRouter = Router();
  12. const userCtrl = new UserController();
  13. userRouter.get("/", userCtrl.getAll);
  14. app.use("/api/users", userRouter);
  15. app.listen(PORT, () => {
  16.   console.log(`️[server]: Server is running at http://localhost:${PORT}`);
  17. });

在以上代码中,我们先通过调用 Router 方法创建了一个 userRouter 对象,然后进行相关路由的配置,接着使用 app.use 方法应用 userRouter 路由。下面我们用一张图来直观感受一下 OvernightJS 与 Express 在使用上的差异:

通过以上对比可知,利用 OvernightJS 提供的装饰器,可以让我们开发起来更加便捷。但大家要记住 OvernightJS 底层还是基于 Express,其内部最终还是通过 Express 提供的 API 来处理路由。

接下来为了能更好理解后续的内容,我们先来简单回顾一下 TypeScript 装饰器。

2.1 TypeScript 装饰器简介
装饰器是一个表达式,该表达式执行后,会返回一个函数。在 TypeScript 中装饰器可以分为以下 4 类:

需要注意的是,若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项:

命令行:

 
 
 
 
  1. tsc --target ES5 --experimentalDecorators

tsconfig.json:

 
 
 
 
  1. {
  2.   "compilerOptions": {
  3.      "experimentalDecorators": true
  4.    }
  5. }

了解完 TypeScript 装饰器的分类,我们来开始分析 OvernightJS 框架中提供的装饰器。

2.2 @Controller 装饰器
在前面创建的简单 Web 服务器中,我们通过以下方式来使用 @Controller 装饰器:

 
 
 
 
  1. @Controller("api/users")
  2. export class UserController {}

很明显该装饰器应用在 UserController 类上,它属于类装饰器。OvernightJS 的项目结构很简单,我们可以很容易找到 @Controller 装饰器的定义:

 
 
 
 
  1. // src/core/lib/decorators/class.ts
  2. export function Controller(path: string): ClassDecorator {
  3.   return (target: TFunction): void => {
  4.     addBasePathToClassMetadata(target.prototype, "/" + path);
  5.   };
  6. }

通过观察以上代码可知,Controller 函数是一个装饰器工厂,即调用该工厂方法之后会返回一个 ClassDecorator 对象。在 ClassDecorator 内部,会继续调用 addBasePathToClassMetadata 方法,把基础路径添加到类的元数据中:

 
 
 
 
  1. // src/core/lib/decorators/class.ts
  2. export function addBasePathToClassMetadata(target: Object, basePath: string): void {
  3.   let metadata: IClassMetadata | undefined = Reflect.getOwnMetadata(classMetadataKey, target);
  4.   if (!metadata) {
  5.       metadata = {};
  6.   }
  7.   metadata.basePath = basePath;
  8.   Reflect.defineMetadata(classMetadataKey, metadata, target);
  9. }

addBasePathToClassMetadata 函数的实现很简单,主要是利用 Reflect API 实现元数据的存取操作。在以上代码中,会先获取 target 对象上已保存的 metadata 对象,如果不存在的话,会创建一个空的对象,然后把参数 basePath 的值添加该对象的 basePath 属性中,元数据设置完成后,在通过 Reflect.defineMetadata 方法进行元数据的保存。

下面我们用一张图来说明一下 @Controller 装饰器的处理流程:

在 OvernightJS 项目中,所使用的 Reflect API 是来自 reflect-metadata 这个第三方库。该库提供了很多 API 用于操作元数据,这里我们只简单介绍几个常用的 API:

 
 
 
 
  1. // define metadata on an object or property
  2. Reflect.defineMetadata(metadataKey, metadataValue, target);
  3. Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
  4. // check for presence of a metadata key on the prototype chain of an object or property
  5. let result = Reflect.hasMetadata(metadataKey, target);
  6. let result = Reflect.hasMetadata(metadataKey, target, propertyKey);
  7. // get metadata value of an own metadata key of an object or property
  8. let result = Reflect.getOwnMetadata(metadataKey, target);
  9. let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
  10. // get metadata value of a metadata key on the prototype chain of an object or property
  11. let result = Reflect.getMetadata(metadataKey, target);
  12. let result = Reflect.getMetadata(metadataKey, target, propertyKey);
  13. // delete metadata from an object or property
  14. let result = Reflect.deleteMetadata(metadataKey, target);
  15. let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

相信看到这里,可能有一些小伙伴会有疑问,通过 Reflect API 保存的元数据什么时候使用呢?这里我们先记住这个问题,后面我们再来分析它,接下来我们来开始分析 @Get 装饰器。

2.3 @Get 装饰器
在前面创建的简单 Web 服务器中,我们通过以下方式来使用 @Get 装饰器,该装饰器用于配置 Get 请求:

 
 
 
 
  1. export class UserController {
  2.   @Get("")
  3.   private getAll(req: Request, res: Response) {
  4.     return res.status(200).json({
  5.       message: "成功获取所有用户",
  6.     });
  7.   }
  8. }

@Get 装饰器应用在 UserController 类的 getAll 方法上,它属于方法装饰器。它的定义如下所示:

 
 
 
 
  1. // src/core/lib/decorators/method.ts
  2. export function Get(path?: string | RegExp): MethodDecorator & PropertyDecorator {
  3.   return helperForRoutes(HttpVerb.GET, path);
  4. }

与 Controller 函数一样,Get 函数也是一个装饰器工厂,调用该函数之后会返回 MethodDecorator & PropertyDecorator 的交叉类型。除了 Get 请求方法之外,常见的 HTTP 请求方法还有 Post、Delete、Put、Patch 和 Head 等。为了统一处理这些请求方法,OvernightJS 内部封装了一个 helperForRoutes 函数,该函数的具体实现如下:

 
 
 
 
  1. // src/core/lib/decorators/method.ts
  2. function helperForRoutes(httpVerb: HttpDecorator, path?: string | RegExp): MethodDecorator & PropertyDecorator {
  3.   return (target: Object, propertyKey: string | symbol): void => {
  4.       let newPath: string | RegExp;
  5.       if (path === undefined) {
  6.           newPath = '';
  7.       } else if (path instanceof RegExp) {
  8.           newPath = addForwardSlashToFrontOfRegex(path);
  9.       } else { // assert (path instanceof string)
  10.           newPath = '/' + path;
  11.       }
  12.       addHttpVerbToMethodMetadata(target, propertyKey, httpVerb, newPath);
  13.     };
  14. }

观察以上代码可知,在 helperForRoutes 方法内部,会继续调用 addHttpVerbToMethodMetadata 方法把请求方法和请求路径这些元数据保存起来。

 
 
 
 
  1. // src/core/lib/decorators/method.ts
  2. export function addHttpVerbToMethodMetadata(target: Object, metadataKey: any, 
  3.   httpDecorator: HttpDecorator, path: string | RegExp): void {
  4.     let metadata: IMethodMetadata | undefined = Reflect.getOwnMetadata(metadataKey, target);
  5.     if (!metadata) {
  6.         metadata = {};
  7.     }
  8.     if (!metadata.httpRoutes) {
  9.         metadata.httpRoutes = [];
  10.     }
  11.     const newArr: IHttpRoute[] = [{
  12.       httpDecorator,
  13.       path,
  14.     }];
  15.     newArr.push(...metadata.httpRoutes);
  16.     metadata.httpRoutes = newArr;
  17.     Reflect.defineMetadata(metadataKey, metadata, target);

在 addHttpVerbToMethodMetadata 方法中,会先获取已保存的元数据,如果 metadata 对象不存在则会创建一个空的对象。然后会继续判断该对象上是否含有 httpRoutes 属性,没有的话会使用 [] 对象来作为该属性的属性值。而请求方法和请求路径这些元数据会以对象的形式保存到数组中,最终在通过 Reflect.defineMetadata 方法进行元数据的保存。

同样,我们用一张图来说明一下 @Get 装饰器的处理流程:

分析完 @Controller 和 @Get 装饰器,我们已经知道元数据是如何进行保存的。下面我们来回答 “通过 Reflect API 保存的元数据什么时候使用呢?” 这个问题。

2.4 元数据的使用
要搞清楚通过 Reflect API 保存的元数据什么时候使用,我们就需要来回顾一下前面开发的 SampleServer 服务器:

 
 
 
 
  1. export class SampleServer extends Server {
  2.   constructor() {
  3.     super(process.env.NODE_ENV === "development");
  4.     this.setupControllers();
  5.   }
  6.   private setupControllers(): void {
  7.     const userController = new UserController();
  8.     super.addControllers([userController]);
  9.   }
  10.   public start(port: number): void {
  11.     this.app.listen(port, () => {
  12.       console.log(`️[server]: Server is running at http://localhost:${PORT}`);
  13.     });
  14.   }
  15. }
  16. const sampleServer = new SampleServer();
  17. sampleServer.start(PORT);

在以上代码中 SampleServer 类继承于 OvernightJS 内置的 Server 类,对应的 UML 类图如下所示:

此外,在 SampleServer 类中我们定义了 setupControllers 和 start 方法,分别用于初始化控制器和启动服务器。我们在自定义的控制器上使用了 @Controller 和 @Get 装饰器,因此接下来我们的重点就是分析 setupControllers 方法。该方法的内部实现很简单,就是手动创建控制器实例,然后调用父类的 addControllers 方法。

下面我们来分析 addControllers 方法,该方法位于 src/core/lib/Server.ts 文件中,具体实现如下:

 
 
 
 
  1. // src/core/lib/Server.ts
  2. export class Server {
  3.   public addControllers(
  4.     controllers: Controller | Controller[],
  5.     routerLib?: RouterLib,
  6.     globalMiddleware?: RequestHandler,
  7.   ): void {
  8.        controllers = (controllers instanceof Array) ? controllers : [controllers];
  9.        // ① 支持动态设置路由库
  10.        const routerLibrary: RouterLib = routerLib || Router; 
  11.        controllers.forEach((controller: Controller) => {
  12.          if (controller) {
  13.              // ② 为每个控制器创建对应的路由对象
  14.              const routerAndPath: IRouterAndPath | null = this.getRouter(routerLibrary, controller);
  15.              // ③ 注册路由
  16.              if (routerAndPath) {
  17.                   if (globalMiddleware) {
  18.                       this.app.use(routerAndPath.basePath, globalMiddleware, routerAndPath.router);
  19.                   } else {
  20.                       this.app.use(routerAndPath.basePath, routerAndPath.router);
  21.                   }
  22.               }
  23.             }
  24.         });
  25.     }
  26. }

addControllers 方法的整个执行过程还是比较清晰,最核心的部分就是 getRouter 方法。在该方法内部就会处理通过装饰器保存的元数据。其实 getRouter 方法内部还会处理其他装饰器保存的元数据,简单起见我们只考虑与 @Controller 和 @Get 装饰器相关的处理逻辑。

 
 
 
 
  1. // src/core/lib/Server.ts
  2. export class Server {
  3.  private getRouter(routerLibrary: RouterLib, controller: Controller): IRouterAndPath | null {
  4.         const prototype: any = Object.getPrototypeOf(controller);
  5.         const classMetadata: IClassMetadata | undefined = Reflect.getOwnMetadata(classMetadataKey, prototype);
  6.         // 省略部分代码
  7.         const { basePath, options, ...}: IClassMetadata = classMetadata;
  8.         // ① 基于配置项创建Router对象
  9.         const router: IRouter = routerLibrary(options);
  10.         // ② 为路由对象添加路径和请求处理器
  11.         let members: any = Object.getOwnPropertyNames(controller);
  12.         members = members.concat(Object.getOwnPropertyNames(prototype));
  13.         members.forEach((member: any) => {
  14.             // ③ 获取方法中保存的元数据
  15.             const methodMetadata: IMethodMetadata | undefined = Reflect.getOwnMetadata(member, prototype);
  16.             if (methodMetadata) {
  17.                 const { httpRoutes, ...}: IMethodMetadata = methodMetadata;
  18.                 let callBack: (...args: any[]) => any = (...args: any[]): any => {
  19.                     return controller[member](...args);
  20.                 };
  21.                 // 省略部分代码
  22.                 if (httpRoutes) { // httpRoutes数组中包含了请求的方法和路径
  23.                     // ④ 处理控制器类中通过@Get、@Post、@Put或@Delete装饰器保存的元数据
  24.                     httpRoutes.forEach((route: IHttpRoute) => {
  25.                         const { httpDecorator, path }: IHttpRoute = route;
  26.                         // ⑤ 为router对象设置对应的路由信息
  27.                         if (middlewares) {
  28.                             router[httpDecorator](path, middlewares, callBack);
  29.                         } else {
  30.                             router[httpDecorator](path, callBack);
  31.                         }
  32.                     });
  33.                 }
  34.             }
  35.         });
  36.         return { basePath, router, };
  37.     }
  38. }

现在我们已经知道 OvernightJS 内部如何利用装饰器来为控制器类配置路由信息,这里阿宝哥用一张图来总结 OvernightJS 的工作流程:

在 OvernightJS 内部除了 @Controller、@Get、@Post、@Delete 等装饰器之外,还提供了用于注册中间件的 @Middleware 装饰器及用于设置异常处理中间件的 @ErrorMiddleware 装饰器。感兴趣的小伙伴可以参考一下阿宝哥的学习思路,自行阅读 OvernightJS 项目的源码。

希望通过这篇文章,可以让小伙伴们对装饰器的应用场景有一些更深刻的理解。如果你还意犹未尽的话,可以阅读阿宝哥之前写的 了不起的 IoC 与 DI 这篇文章,该文章介绍了如何利用 TypeScript 装饰器和 reflect-metadata 这个库提供的 Reflect API 实现一个 IoC 容器。

三、参考资源

  • Github - overnight
  • expressjs.com

网站标题:如何让你的Express飞起来
URL链接:http://www.shufengxianlan.com/qtweb/news32/22932.html

成都网站建设公司_创新互联,为您提供App开发定制开发品牌网站设计关键词优化建站公司品牌网站建设

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联