使用 Express.js 创建简版 Nest.js

date
Mar 16, 2022
slug
use-express-create-mini-nest
status
Published
tags
Nodejs
summary
使用 Typescript + 装饰器创建 Nest.js 中“注入”的行为来编写代码🥴
type
Post
express 用了一段时间,最近看了看nestjs还有ts入门实战,结合自己使用ts+express的经验,对expressjs如何向nestjs演化做一个总结

规划&思路

主要是包装器、中间件的结合使用。对于路由(controller)的定义采用nestjs中注入的方式实现

装饰器

nestjs中处处可见,那么这个东西到底是什么?详情见
这里做一些总结与注意事项:
  1. 装饰器本质就是一个函数,装饰器通过@来使用
  1. 装饰器收集顺序:由上到下,由左向右;
  1. 装饰器执行顺序:由下到上,由右向左
  1. 类的装饰器:
      • 接受参数:constrouctor
      • 执行时机:在类定义后立即执行,不论声明几个实例都只执行一次
  1. 函数的装饰器:
      • 接受参数:
          1. target 普通方法对应的是类的 prototype, 静态方法对应的是类的构造函数
          1. key 方法名
          1. descriptor 类比 Object.defineProperty 的 descriptor
      • 执行时机:在函数定义后立即执行
  1. 参数的装饰器:
      • 接受参数:
          1. target 普通方法对应的是类的 prototype, 静态方法对应的是类的构造函数
          1. key 参数名
          1. paramIndex 参数索引
      • 执行时机: 在参数定义后立即执行
  1. 属性装饰器:
      • 接受参数:
          1. target 普通方法对应的是类的 prototype, 静态方法对应的是类的构造函数
          1. key 属性名
      • 执行时机:在属性定义后立即执行
      • 需要注意的是:因为属性的装饰器函数没有 descriptor 参数,如果要强行改则需自己定义 descriptor 并返回

使用元数据进行依赖收集

import { log } from "../decorator"

@controller("/api")
export default class LoginController {
	@post("/login")
	@middleware(log)
	login() {
		// todo
	}
}
这样子的写法在nestjs、eggjs中很常见,那么如何用ts来自己实现呢?
// controller.ts 收集定义的装饰器(即定义的元数据)

import { RequestHandler } from "express";
import "reflect-metadata";
import { Router } from "express";

const router = Router();

enum HttpMethods {
  get = "get",
  post = "post"
};

export function controller(root: string) {
	// new (...args: any[]) => any 是ts中用来定义构造器的类型写法
  return function (target: new (...args: any[]) => any) {
    // 详情见readme,为什么不用for in或object.keys见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Enumerability_and_ownership_of_properties
    Reflect.ownKeys(target.prototype).forEach((key) => {
			// 收集定义的方法装饰器、中间件
      const path: string = Reflect.getMetadata("path", target.prototype, key);
      const method: HttpMethods = Reflect.getMetadata(
        "method",
        target.prototype,
        key
      );
      const handler = target.prototype[key];
      const middleware: RequestHandler[] = Reflect.getMetadata(
        "middleware",
        target.prototype,
        key
      );
      const fullPath = root === "/" ? path : `${root}${path}`;
      if (fullPath && Reflect.has(router, method) && handler) {
        if (middleware) {
          router[method](fullPath, ...middleware, handler);
        } else {
          router[method](fullPath, handler);
        }
      }
    });
  };
}
定义元数据(get/post、midderware)
// request.ts

import "reflect-metadata";

enum HttpMethods {
  get = "get",
  post = "post"
}

function requestDecoratorFactory(method: HttpMethods) {
  return function (path: string) {
    return function (target: any, key: string) {
			// 定义元数据
      Reflect.defineMetadata("path", path, target, key);
      Reflect.defineMetadata("method", method, target, key);
    };
  };
}

export const get = requestDecoratorFactory(HttpMethods.get);
export const post = requestDecoratorFactory(HttpMethods.post);
下面中间件装饰器定义需要注意的是中间件装饰器的执行顺序,要保证由下到上,而不是由上到下。(因为express中定义路由时第二个参数可以传入固定顺序的中间件数组,为了改变顺序我们不能穿数组)
// middleware.ts

import { RequestHandler } from "express";
import "reflect-metadata";

// export function middleware(middleware: RequestHandler | RequestHandler[]) { // 直接传入数组的方式,中间件顺序与数组索引顺序一致
export function middleware(middleware: RequestHandler) {
  return function (target: any, key: string) {
    // 这种写法会保证中间件执行顺序是由下到上的正常顺序
    const prevMiddleware: RequestHandler[] =
      Reflect.getMetadata("middleware", target, key) || [];
    const middlewares: RequestHandler[] = [...prevMiddleware, middleware];
    Reflect.defineMetadata("middleware", middlewares, target, key);
  };
}
具体代码可以查看

© i7eo 2017 - 2024