制作自己的Nestjs
#typescript #node #nestjs #decorator

首次使用NestJS 时,可能会有些不知所措。让我们尝试了解其中一些组件的工作原理。

在你开始之前

我假设您熟悉Nestjs及其提供的功能。 decoratorsexperimental metadata API可以实现像黑魔法的大多数功能。确保您对这些有基本的了解。

我已经设置了具有必要配置的this project,以运行我将要共享的工作片段。您可以将它们放入playground.ts文件中,并使用命令npm run playground运行它们。完成摘要后,您可以看到它们在lib.ts文件中汇聚在一起。

定义路线

这里的关键是,装饰器可以将元数据附加到类和方法上,并且可以在运行时访问这些元数据。让我们深入研究。

import 'reflect-metadata';

const PATH_KEY = Symbol('path');
const HTTP_METHOD_KEY = Symbol('method');

/**
 * Post is a decorator factory that takes a path and returns a decorator.
 * That decorator attaches the path and the HTTP method post to the class method it is applied to.
 **/
function Post(path: string) {
  return function (target: any, key: string) {
    Reflect.defineMetadata(PATH_KEY, path, target, key);
    Reflect.defineMetadata(HTTP_METHOD_KEY, 'post', target, key);
  };
}
/* 👆 these are codes that the framework might provide */

/* So user can write something like this */
class AuthRoute {
  @Post('/login')
  async login() {
    return 'login success';
  }
}

/* Then the framework can use the class to create the actual routes */
function createApp(ControllerCls: any) {
  // first get all the properties of that class
  const properties = Object.getOwnPropertyNames(ControllerCls.prototype);
  properties
    .filter(
      (
        method // keep the ones that as HTTP method metadata
      ) => Reflect.hasOwnMetadata(HTTP_METHOD_KEY, ControllerCls.prototype, method)
    )
    .forEach(method => {
      const path = Reflect.getMetadata(PATH_KEY, ControllerCls.prototype, method);
      const httpMethod = Reflect.getMetadata(
        HTTP_METHOD_KEY,
        ControllerCls.prototype,
        method
      );
      console.log(`Mapping: ${httpMethod.toUpperCase()} ${path}`);
      // now that we have access to the method name and path at runtime,
      // these could be attached to an express app
    });
}

createApp(AuthRoute);

请注意,在这里使用symbol不是强制性的,只能使用纯字符串作为键。

依赖注入

依赖注入的基本思想是,您将依赖项传递给构造函数,而不是实例化构造函数中类的依赖关系。 Nestjs在这里所做的神秘事物是,您可以使用Shorthand Express在构造函数中定义依赖项,例如constructor(private service: Service)和Nestjs,将执行实例化并将其传递给构造函数。让我们看看类似的事情是可能的。

再次发挥了元数据API。构造函数的参数可在元数据中使用design:paramtypes键。捕获的是,班级必须用至少一个装饰师装饰。否则,Typescript将在转移到可运行的JavaScript时不会记录参数数据。这就是@Injectable()装饰器发挥作用的地方。您可能已经注意到,即使不需要控制器前缀,Nestjs也会让您用Controller()装饰器装饰控制器。这是因为所有班级都需要用至少一个装饰器装饰才能用正确的参数实例化。

import 'reflect-metadata';

function Injectable() {
  return function (target: any) {}; // it doesn't have to do anything
}

class UserRepository {
  async findUser() {
    return 'user exists';
  }
}

@Injectable()
class AuthService {
  constructor(private readonly authService: UserRepository) {}
  login() {
    return this.authService.findUser();
  }
}

function instantiate(ProviderCls: any) {
  const params = Reflect.getMetadata('design:paramtypes', ProviderCls).map(
    DependencyCls => new DependencyCls()
  );

  const provider = new ProviderCls(...params);

  provider.login().then(console.log);
}

instantiate(AuthService);

请注意,它不需要是装饰工厂,因为它没有采用参数。我们可以像function Injectable(target: any) {}一样定义它,然后可以像@Injectable一样使用它,而没有牙套。只是使它看起来像其他装饰器。

将数据传递到路线处理程序怎么样?

使用类似的技术,可以装饰参数以指示该参数中需要哪种数据,然后该框架可以传递适当的数据,以形成基础平台。

import 'reflect-metadata';

const HTTP_METHOD_KEY = Symbol('method');
const PATH_KEY = Symbol('path');
const PARAMS_META_KEY = Symbol('paramsMeta');

// just like the first snippet
function Get(path: string) {
  return function (target: any, key: string) {
    Reflect.defineMetadata(PATH_KEY, path, target, key);
    Reflect.defineMetadata(HTTP_METHOD_KEY, 'get', target, key);
  };
}

// decorator to indicate that data is required from route parameter
export function Param(key: string) {
  return function (target: any, methodName: string, index: number) {
    const paramsMeta = Reflect.getMetadata(PARAMS_META_KEY, target, methodName) ?? {};
    paramsMeta[index] = { key, type: 'route_param' };
    Reflect.defineMetadata(PARAMS_META_KEY, paramsMeta, target, methodName);
  };
}

class AuthRoute {
  @Get('/profile/:id')
  async profile(@Param('id') id: string) {
    return `user: ${id}`;
  }
}

function createApp(ControllerCls: any) {
  Object.getOwnPropertyNames(ControllerCls.prototype)
    .filter(method =>
      Reflect.hasOwnMetadata(HTTP_METHOD_KEY, ControllerCls.prototype, method)
    )
    .forEach(method => {
      const PARAM_DATA = { id: '123' }; // could get from req.params

      const paramsMeta =
        Reflect.getMetadata(PARAMS_META_KEY, ControllerCls.prototype, method) ?? {};

      const paramsToPass = Reflect.getMetadata(
        'design:paramtypes',
        ControllerCls.prototype,
        method
      ).map((_, index) => {
        const { key, type } = paramsMeta[index];
        if (type === 'route_param') return PARAM_DATA[key];
        return null;
      });
      ControllerCls.prototype[method](...paramsToPass).then(console.log);
    });
}

createApp(AuthRoute);

将所有这些放在一起

我试图使摘要尽可能易于理解。您可以看到lib.ts与实际的Express一起将它们放在其中。在index.ts中,有一个使用此“框架”的Web应用程序。他们拥有它,这是一个像Nestjs的类似于Nestjs的框架。这就是行动中的样子:

import { Body, Controller, Get, Injectable, Module, Param, Post, createApp } from './lib';

class LoginDto {
  username: string;
  password: string;
}

@Injectable()
class UserRepository {
  async findOne(id: string) {
    return { userId: id };
  }
}

@Injectable()
class AuthService {
  constructor(private readonly userRepository: UserRepository) {}

  async login({ username }: LoginDto) {
    return `login successful for ${username}`;
  }

  async findUser(id: string) {
    return this.userRepository.findOne(id);
  }
}

@Controller('auth')
class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('/login')
  login(@Body() loginData: LoginDto) {
    console.log({ loginData });
    return this.authService.login(loginData);
  }

  @Get('/profile/:id')
  async profile(@Param('id') id: string) {
    const user = await this.authService.findUser(id);
    return `user: ${user.userId}`;
  }
}

@Module({
  controllers: [AuthController],
  providers: [AuthService],
})
class AppModule {}

const app = createApp(AppModule);

app.listen(3001, () => {
  console.log('listening on port 3001');
});

当然,它没有处理很多事情,但是了解像Nestjs这样的框架在引擎盖下工作的好起点。

感谢您的阅读。 Leave a star如果您喜欢它。